Consolidate Reader source ownership and pinned application instances
Add exact recursive source checkout, independent Git/jj developer workbenches and source-only backup/recovery commands, with Workspace instance binding outside source. Qualify two empty Reader Graphs, packaged source commands, Agent VM compilation, exact Gitea read grants and reconciled Reader settings/annotation stability. Publish checked Reader Package/root revisions to canonical review branches and retention tags, retain all 57 repositories independently in private versioned storage, verify downloaded-backup recovery and advance the example pin. Add bounded Central pin acquisition and selection-specific read-only identities, qualified against all 57 canonical repositories without write authority. Replace regex fork dependency discovery with compiler-parsed lock imports and rewrite proposals; preserve source identities and unrelated files, passing instance binding through packed and ordinary host checks. Hosted run-pin versus authoring preparation and maintained duplicate removal remain pending. Live source selections, Template defaults and default source branches are unchanged.
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
import { execFile as callback } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { lstat, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { retentionTagForCommit } from "../resource-lock/index.js";
|
||||
import { materializeWorkspaceSources, workspaceSourcePin, type WorkspaceSourcePin } from "./source-closure.js";
|
||||
|
||||
const exec = promisify(callback);
|
||||
type SourceBackup = {
|
||||
formatVersion: 1;
|
||||
root: WorkspaceSourcePin;
|
||||
repositories: Array<{ repository: string; commits: string[]; bundle: string; sha256: string }>;
|
||||
};
|
||||
async function hash(filename: string) {
|
||||
const digest = createHash("sha256");
|
||||
for await (const chunk of createReadStream(filename)) digest.update(chunk);
|
||||
return digest.digest("hex");
|
||||
}
|
||||
|
||||
/** Source-only backup: no Graph state, checkout instructions or credentials.
|
||||
* Git bundles retain exact commits and their ancestors without needing a forge.
|
||||
* Storage/retention of the resulting directory belongs to the operator. */
|
||||
export async function backupWorkspaceSources(options: {
|
||||
source: WorkspaceSourcePin;
|
||||
directory: string;
|
||||
output: string;
|
||||
locations?: Record<string, string>;
|
||||
}): Promise<SourceBackup> {
|
||||
const output = path.resolve(options.output);
|
||||
if (
|
||||
await lstat(output).catch((error: NodeJS.ErrnoException) => {
|
||||
if (error.code !== "ENOENT") throw error;
|
||||
return null;
|
||||
})
|
||||
)
|
||||
throw Error("Source backup destination must not exist");
|
||||
const closure = await materializeWorkspaceSources(options);
|
||||
const grouped = new Map<string, Map<string, string>>();
|
||||
for (const entry of closure.resources) {
|
||||
const commits = grouped.get(entry.repository) ?? new Map<string, string>();
|
||||
commits.set(entry.commit, path.join(path.resolve(options.directory), entry.directory));
|
||||
grouped.set(entry.repository, commits);
|
||||
}
|
||||
await mkdir(path.dirname(output), { recursive: true });
|
||||
const staging = await mkdtemp(path.join(path.dirname(output), ".source-backup-"));
|
||||
const manifest: SourceBackup = { formatVersion: 1, root: closure.root, repositories: [] };
|
||||
try {
|
||||
for (const [repository, commits] of [...grouped].sort(([a], [b]) => a.localeCompare(b))) {
|
||||
const bundle = createHash("sha256").update(repository).digest("hex") + ".bundle";
|
||||
const gitDirectory = await mkdtemp(path.join(staging, ".git-"));
|
||||
try {
|
||||
await exec("git", ["init", "--bare", gitDirectory]);
|
||||
const refs: string[] = [];
|
||||
for (const [commit, checkout] of commits) {
|
||||
const ref = retentionTagForCommit(commit);
|
||||
await exec("git", ["-C", gitDirectory, "fetch", "--no-tags", "--", checkout, `${ref}:${ref}`]);
|
||||
refs.push(ref);
|
||||
}
|
||||
await exec("git", ["-C", gitDirectory, "bundle", "create", path.join(staging, bundle), ...refs]);
|
||||
} finally {
|
||||
await rm(gitDirectory, { recursive: true, force: true });
|
||||
}
|
||||
manifest.repositories.push({
|
||||
repository,
|
||||
commits: [...commits.keys()].sort(),
|
||||
bundle,
|
||||
sha256: await hash(path.join(staging, bundle)),
|
||||
});
|
||||
}
|
||||
await writeFile(path.join(staging, "backup.json"), JSON.stringify(manifest, null, 2) + "\n", { mode: 0o600 });
|
||||
await rename(staging, output);
|
||||
return manifest;
|
||||
} finally {
|
||||
await rm(staging, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
export async function restoreWorkspaceSources(options: { backup: string; directory: string }) {
|
||||
const backup = path.resolve(options.backup);
|
||||
const filename = path.join(backup, "backup.json");
|
||||
const info = await lstat(filename);
|
||||
if (!info.isFile() || info.isSymbolicLink() || info.size > 1024 * 1024) throw Error("Invalid source backup manifest");
|
||||
const manifest = JSON.parse(await readFile(filename, "utf8")) as SourceBackup;
|
||||
if (
|
||||
manifest.formatVersion !== 1 ||
|
||||
!Array.isArray(manifest.repositories) ||
|
||||
!manifest.repositories.length ||
|
||||
manifest.repositories.length > 256
|
||||
)
|
||||
throw Error("Invalid source backup manifest");
|
||||
const root = workspaceSourcePin(manifest.root);
|
||||
const locations: Record<string, string> = {};
|
||||
for (const entry of manifest.repositories) {
|
||||
if (!entry || !Array.isArray(entry.commits) || !entry.commits.length || entry.commits.length > 256)
|
||||
throw Error("Invalid source backup entry");
|
||||
for (const commit of entry.commits) workspaceSourcePin({ repository: entry.repository, commit });
|
||||
if (Object.hasOwn(locations, entry.repository)) throw Error("Duplicate source backup repository");
|
||||
const expected = createHash("sha256").update(entry.repository).digest("hex") + ".bundle";
|
||||
if (entry.bundle !== expected || !/^[a-f0-9]{64}$/.test(entry.sha256))
|
||||
throw Error("Invalid source backup bundle identity");
|
||||
const bundle = path.join(backup, expected);
|
||||
const stat = await lstat(bundle);
|
||||
if (!stat.isFile() || stat.isSymbolicLink() || (await hash(bundle)) !== entry.sha256)
|
||||
throw Error("Source backup bundle integrity mismatch");
|
||||
locations[entry.repository] = pathToFileURL(bundle).href;
|
||||
}
|
||||
if (
|
||||
!manifest.repositories.some((entry) => entry.repository === root.repository && entry.commits.includes(root.commit))
|
||||
)
|
||||
throw Error("Source backup does not retain its root commit");
|
||||
// Every discovered dependency must use the backup. Never fall through to the
|
||||
// original forge if a manifest omits a required repository.
|
||||
return materializeWorkspaceSources({ source: root, directory: options.directory, locations, requireLocations: true });
|
||||
}
|
||||
Reference in New Issue
Block a user