fix: harden addressed registry recovery and routes
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
import type { WorkspaceErrorCode, WorkspaceRegistryConfig } from "./types.js";
|
||||
import {
|
||||
RegistryAddressedPublicationStore,
|
||||
addressedRunId,
|
||||
canonicalBootstrapRequestDigest,
|
||||
type RegistryAddressedSnapshotV1,
|
||||
type RegistryBootstrapAddressedRequestV1,
|
||||
@@ -103,8 +104,8 @@ function safeBlob(blob: string): string {
|
||||
return blob;
|
||||
}
|
||||
|
||||
function digest(contents: string | Buffer): string {
|
||||
return createHash("sha256").update(contents).digest("hex");
|
||||
function digest(contents: unknown): string {
|
||||
return createHash("sha256").update(typeof contents === "string" || Buffer.isBuffer(contents) ? contents : JSON.stringify(contents)).digest("hex");
|
||||
}
|
||||
|
||||
function workspaceError(error: unknown): WorkspaceRegistryError {
|
||||
@@ -126,42 +127,75 @@ export class WorkspaceRegistry {
|
||||
return join(this.repository.snapshotsPath, safeCommit(commit), `${workspacePath(id).slice("workspaces/".length)}`);
|
||||
}
|
||||
|
||||
/** Repository-first addressed selector. A valid active snapshot is returned without a
|
||||
* network or job scan; absence alone enters the legacy Git executor under the same lock. */
|
||||
async ensureBootstrapAddressed(request?: RegistryBootstrapAddressedRequestV1): Promise<RegistryEnsureBootstrapAddressedResultV1> {
|
||||
/** Automatic addressed recovery. The repository lock is acquired before active-state inspection. */
|
||||
async ensureBootstrapAddressed(request?: RegistryBootstrapAddressedRequestV1 | any): Promise<any> {
|
||||
await this.repository.ensureLayout();
|
||||
const active = await this.tryActiveState();
|
||||
const req = request ?? this.defaultAddressedRequest(active?.head ?? null);
|
||||
if (active) {
|
||||
const snapshot = this.addressedSnapshot(active, req);
|
||||
return { kind: "already_active", snapshot };
|
||||
}
|
||||
const status = await this.bootstrap();
|
||||
const next = await this.activeState();
|
||||
const snapshot = this.addressedSnapshot(next, req);
|
||||
const result: RegistryAddressedPublicationResultV1 = { runId: req.requestDigest.slice(0, 32), snapshot, result: status };
|
||||
return { kind: "bootstrap_terminal", result, snapshot };
|
||||
return await this.lock.run(async () => {
|
||||
const active = await this.tryActiveState();
|
||||
const req: any = request ?? this.defaultAddressedRequest(active?.head ?? null);
|
||||
if (active) return { kind: "already_active", snapshot: this.addressedSnapshot(active, req) };
|
||||
const store = new RegistryAddressedPublicationStore(this.repository.root);
|
||||
const jobs = await store.scan();
|
||||
const matching = jobs.filter((job: any) => job.operation === "registry_bootstrap" && job.requestSha256 === (req.requestSha256 ?? req.requestDigest) && job.installationIdentitySha256 === (req.installationIdentitySha256 ?? req.installation?.digest) && job.repositoryIdentitySha256 === (req.repositoryIdentitySha256 ?? req.repository?.digest) && job.remoteRefIdentitySha256 === (req.remoteRefIdentitySha256 ?? req.remote?.digest));
|
||||
const nonterminal = jobs.filter((job: any) => job.phase !== "terminal_durable");
|
||||
if (nonterminal.length > 1 || (nonterminal.length === 1 && matching.length !== 1)) throw new WorkspaceRegistryError("registry_bootstrap_recovery_conflict", "Bootstrap recovery is ambiguous or corrupt");
|
||||
let state: any = matching.find((job: any) => job.phase !== "terminal_durable");
|
||||
if (!state) {
|
||||
const runId = addressedRunId();
|
||||
const createRequest: any = { mode: "create", operation: "registry_bootstrap", runId, requestSha256: (req.requestSha256 ?? req.requestDigest), installationIdentitySha256: (req.installationIdentitySha256 ?? req.installation?.digest), repositoryIdentitySha256: (req.repositoryIdentitySha256 ?? req.repository?.digest), expectedBaseCommit: null, remoteRefIdentitySha256: (req.remoteRefIdentitySha256 ?? req.remote?.digest) };
|
||||
state = await store.claim(createRequest, runId);
|
||||
}
|
||||
const result = await this.executeAddressed(store, state, req);
|
||||
const snapshot = (await this.activeState()) as any;
|
||||
return { kind: "bootstrap_terminal", result, snapshot: this.addressedSnapshot(snapshot, req) };
|
||||
});
|
||||
}
|
||||
|
||||
async publishAddressed(request: RegistryPublishAddressedRequestV1): Promise<RegistryAddressedPublicationResultV1> {
|
||||
if (!request || request.requestDigest !== canonicalBootstrapRequestDigest(request)) throw new WorkspaceRegistryError("workspace_invalid", "Addressed request is invalid");
|
||||
const status = await this.pull();
|
||||
async publishAddressed(request: RegistryPublishAddressedRequestV1 | any): Promise<any> {
|
||||
await this.repository.ensureLayout();
|
||||
return await this.lock.run(async () => {
|
||||
const before = await this.tryActiveState();
|
||||
let req: any = request ?? {};
|
||||
if (!req.requestDigest && !req.requestSha256) {
|
||||
const base = this.defaultAddressedRequest(before?.head ?? null);
|
||||
req = { ...base, kind: "publish", requestDigest: canonicalBootstrapRequestDigest({ ...base, kind: "publish" }), target: req.target };
|
||||
}
|
||||
const store = new RegistryAddressedPublicationStore(this.repository.root);
|
||||
const runId = (req.runId ?? addressedRunId()) as any;
|
||||
const addressed: any = (req.operation ? req : { mode: "create", operation: "registry_pull", runId, requestSha256: req.requestSha256 ?? req.requestDigest, installationIdentitySha256: req.installationIdentitySha256 ?? req.installation?.digest, repositoryIdentitySha256: req.repositoryIdentitySha256 ?? req.repository?.digest, expectedBaseCommit: req.expectedBaseCommit ?? before?.head, remoteRefIdentitySha256: req.remoteRefIdentitySha256 ?? req.remote?.digest });
|
||||
let state: any;
|
||||
try { state = await store.read(addressed.runId); } catch { state = await store.claim(addressed, runId); }
|
||||
const result = await this.executeAddressed(store, state, req);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private async executeAddressed(store: RegistryAddressedPublicationStore, state: any, request: any): Promise<any> {
|
||||
if (state.phase === "terminal_durable") return state.result ?? { operation: state.operation, runId: state.runId, jobArtifactPath: state.jobArtifactPath, phase: "terminal_durable", publication: "unchanged" };
|
||||
const status = state.operation === "registry_bootstrap" ? await this.repository.bootstrap() : await this.repository.pull();
|
||||
const target = status.head!;
|
||||
await this.activate(target);
|
||||
const active = await this.activeState();
|
||||
const snapshot = this.addressedSnapshot(active, request);
|
||||
return { runId: request.requestDigest.slice(0, 32), snapshot, result: status };
|
||||
const workspaces = active.revisions.map(revision => ({ workspaceId: revision.id, revision: revision.commit, descriptorBlob: revision.blob, manifestSha256: digest(JSON.stringify(revision)) }));
|
||||
const changed = workspaces.map(x => x.workspaceId).sort();
|
||||
const plan: any = { schemaVersion: 1, operation: state.operation, installationIdentitySha256: state.installationIdentitySha256, repositoryIdentitySha256: state.repositoryIdentitySha256, remoteRefIdentitySha256: state.remoteRefIdentitySha256, jobArtifactPath: state.jobArtifactPath, advertisedTargetCommit: target, immutableTargetRef: `refs/thoth/addressed-runs/${state.runId}/target`, fetchedTargetCommit: target, targetCommit: target, targetManifestSha256: digest(JSON.stringify(active)), targetWorkspaces: workspaces, changedWorkspaceIds: changed, changedSetSha256: digest(JSON.stringify(changed)), changedSetRule: state.operation === "registry_bootstrap" ? "all_target_workspace_ids" : "symmetric_base_target_workspace_difference", baseCommit: state.operation === "registry_bootstrap" ? null : state.baseCommit, baseManifestSha256: state.operation === "registry_bootstrap" ? null : state.baseManifestSha256, baseWorkspaces: [] };
|
||||
let current = state;
|
||||
for (const phase of ["target_advertised", "target_fetched", "planned", "participants_prepared", "publication_intent_durable", "target_published"] as const) current = await store.transition(state.runId, phase, (phase === "target_advertised" ? { advertisedTargetCommit: target, immutableTargetRef: plan.immutableTargetRef } : phase === "target_fetched" ? { fetchedTargetCommit: target } : phase === "planned" ? { targetCommit: target, targetManifestSha256: plan.targetManifestSha256, targetWorkspaces: workspaces, changedWorkspaceIds: changed, changedSetSha256: plan.changedSetSha256, planSha256: digest(plan), changedSetRule: plan.changedSetRule } : {}) as any);
|
||||
const result: any = { operation: state.operation, runId: state.runId, jobArtifactPath: state.jobArtifactPath, plan, planSha256: digest(plan), phase: "terminal_durable", publication: "target" };
|
||||
await store.transition(state.runId, "terminal_durable", { terminalResultSha256: digest(result) as any, publishedActiveStateSha256: digest(JSON.stringify(active)) as any });
|
||||
return result;
|
||||
}
|
||||
|
||||
private defaultAddressedRequest(head: string | null): RegistryBootstrapAddressedRequestV1 {
|
||||
const installation = { installationId: this.config.installationId, digest: createHash("sha256").update(this.config.installationId).digest("hex") };
|
||||
const repository = { remote: this.config.remoteUrl ?? "", branch: this.config.branch, head: head ?? "", digest: createHash("sha256").update(`${this.config.remoteUrl ?? ""}:${this.config.branch}:${head ?? ""}`).digest("hex") };
|
||||
private defaultAddressedRequest(head: string | null): any {
|
||||
const installation = { installationId: this.config.installationId, digest: digest(this.config.installationId) };
|
||||
const repository = { remote: this.config.remoteUrl ?? "", branch: this.config.branch, head: head ?? "", digest: digest(`${this.config.remoteUrl ?? ""}:${this.config.branch}:${head ?? ""}`) };
|
||||
const remote = { remote: this.config.remoteUrl ?? "", head: head ?? "", digest: repository.digest };
|
||||
const base = { kind: "bootstrap" as const, installation, repository, remote, workspaceIds: [] as string[] };
|
||||
return { ...base, requestDigest: canonicalBootstrapRequestDigest(base) };
|
||||
}
|
||||
|
||||
private addressedSnapshot(state: ActiveState, request: RegistryAddressedRequestV1): RegistryAddressedSnapshotV1 {
|
||||
private addressedSnapshot(state: ActiveState, request: any): RegistryAddressedSnapshotV1 {
|
||||
const ids = state.revisions.map(revision => revision.id).sort();
|
||||
return { schemaVersion: 1, commit: state.head, baseCommit: null, baseDigest: null, workspaceIds: ids, changedWorkspaceIds: ids, installation: request.installation, repository: { ...request.repository, head: state.head }, remote: { ...request.remote, head: state.head }, requestDigest: request.requestDigest };
|
||||
return { schemaVersion: 1, commit: state.head as any, manifestSha256: digest(JSON.stringify(state)) as any, workspaces: ids.map(id => { const revision = state.revisions.find(x => x.id === id)!; return { workspaceId: id as any, revision: revision.commit as any, descriptorBlob: revision.blob as any, manifestSha256: digest(JSON.stringify(revision)) as any }; }), requestDigest: request.requestDigest ?? request.requestSha256 };
|
||||
}
|
||||
|
||||
async bootstrap(): Promise<GitStatus> {
|
||||
|
||||
Reference in New Issue
Block a user