fix: gate workspace diagnostic migration
This commit is contained in:
@@ -9,7 +9,13 @@ import {
|
||||
WorkspaceRepositoryLock,
|
||||
type GitStatus,
|
||||
} from "./git-repository.js";
|
||||
import { parseWorkspaceYaml, serializeWorkspaceYaml, type CanonicalWorkspace } from "./schema.js";
|
||||
import {
|
||||
isCanonicalWorkspace,
|
||||
parseWorkspaceYaml,
|
||||
serializeWorkspaceYaml,
|
||||
type CanonicalWorkspace,
|
||||
type WorkspaceDescriptor,
|
||||
} from "./schema.js";
|
||||
import type { WorkspaceErrorCode, WorkspaceRegistryConfig } from "./types.js";
|
||||
|
||||
export type { GitStatus } from "./git-repository.js";
|
||||
@@ -19,6 +25,7 @@ export interface WorkspaceRevision {
|
||||
commit: string;
|
||||
blob: string;
|
||||
snapshotPath: string;
|
||||
state: "operational" | "migration_required";
|
||||
}
|
||||
|
||||
export type PublishWorkspaceRequest =
|
||||
@@ -109,7 +116,7 @@ export class WorkspaceRegistry {
|
||||
return (await this.activeState()).revisions;
|
||||
}
|
||||
|
||||
async read(id: string): Promise<{ workspace: CanonicalWorkspace; revision: WorkspaceRevision }> {
|
||||
async read(id: string): Promise<{ workspace: WorkspaceDescriptor; revision: WorkspaceRevision }> {
|
||||
const state = await this.activeState();
|
||||
const revision = state.revisions.find((candidate) => candidate.id === id);
|
||||
if (!revision) throw new WorkspaceRegistryError("workspace_invalid", "Workspace is unavailable");
|
||||
@@ -133,7 +140,13 @@ export class WorkspaceRegistry {
|
||||
throw new WorkspaceRegistryError("workspace_invalid", "Workspace repository contains no workspaces");
|
||||
}
|
||||
|
||||
const snapshots: Array<{ id: string; source: string; workspace: CanonicalWorkspace; blob: string }> = [];
|
||||
const snapshots: Array<{
|
||||
id: string;
|
||||
source: string;
|
||||
workspace: WorkspaceDescriptor;
|
||||
blob: string;
|
||||
state: WorkspaceRevision["state"];
|
||||
}> = [];
|
||||
try {
|
||||
for (const path of files) {
|
||||
const id = path.slice("workspaces/".length, -".yaml".length);
|
||||
@@ -142,9 +155,22 @@ export class WorkspaceRegistry {
|
||||
if (workspace.workspace.id !== id) {
|
||||
throw new WorkspaceRegistryError("workspace_invalid", "Workspace ID does not match its repository path");
|
||||
}
|
||||
buildInstallationContract(workspace);
|
||||
renderWorkspaceDocs(workspace);
|
||||
snapshots.push({ id, source: serializeWorkspaceYaml(workspace), workspace, blob: await this.repository.blob(path) });
|
||||
let snapshotSource = source;
|
||||
const state: WorkspaceRevision["state"] = isCanonicalWorkspace(workspace)
|
||||
? "operational"
|
||||
: "migration_required";
|
||||
if (isCanonicalWorkspace(workspace)) {
|
||||
buildInstallationContract(workspace);
|
||||
renderWorkspaceDocs(workspace);
|
||||
snapshotSource = serializeWorkspaceYaml(workspace);
|
||||
}
|
||||
snapshots.push({
|
||||
id,
|
||||
source: snapshotSource,
|
||||
workspace,
|
||||
blob: await this.repository.blob(path),
|
||||
state,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw workspaceError(error);
|
||||
@@ -156,6 +182,7 @@ export class WorkspaceRegistry {
|
||||
commit: safeHead,
|
||||
blob: snapshot.blob,
|
||||
snapshotPath: this.snapshotPath(safeHead, snapshot.id),
|
||||
state: snapshot.state,
|
||||
}));
|
||||
if (this.pathExists(snapshotDirectory)) {
|
||||
await this.assertSnapshotIntegrity({ head: safeHead, revisions });
|
||||
@@ -168,13 +195,15 @@ export class WorkspaceRegistry {
|
||||
const yamlName = `${snapshot.id}.yaml`;
|
||||
const envName = `${snapshot.id}.env.example`;
|
||||
const docsName = `${snapshot.id}.md`;
|
||||
const docs = renderWorkspaceDocs(snapshot.workspace);
|
||||
await writeFile(join(staging, yamlName), snapshot.source, { encoding: "utf8", mode: 0o400 });
|
||||
await writeFile(join(staging, envName), docs.envExample, { encoding: "utf8", mode: 0o400 });
|
||||
await writeFile(join(staging, docsName), docs.markdown, { encoding: "utf8", mode: 0o400 });
|
||||
files[yamlName] = digest(snapshot.source);
|
||||
files[envName] = digest(docs.envExample);
|
||||
files[docsName] = digest(docs.markdown);
|
||||
if (snapshot.state === "operational") {
|
||||
const docs = renderWorkspaceDocs(snapshot.workspace);
|
||||
await writeFile(join(staging, envName), docs.envExample, { encoding: "utf8", mode: 0o400 });
|
||||
await writeFile(join(staging, docsName), docs.markdown, { encoding: "utf8", mode: 0o400 });
|
||||
files[envName] = digest(docs.envExample);
|
||||
files[docsName] = digest(docs.markdown);
|
||||
}
|
||||
}
|
||||
await writeFile(join(staging, "snapshot.json"), JSON.stringify({ head: safeHead, revisions, files }), {
|
||||
encoding: "utf8", mode: 0o400,
|
||||
@@ -239,6 +268,7 @@ export class WorkspaceRegistry {
|
||||
safeCommit(revision.commit);
|
||||
safeBlob(revision.blob);
|
||||
if (revision.commit !== state.head || ids.has(revision.id)) throw new Error("bad revision");
|
||||
if (revision.state !== "operational" && revision.state !== "migration_required") throw new Error("bad revision");
|
||||
ids.add(revision.id);
|
||||
workspacePath(revision.id);
|
||||
if (!isAbsolute(revision.snapshotPath) || revision.snapshotPath !== this.snapshotPath(revision.commit, revision.id)) {
|
||||
@@ -256,9 +286,9 @@ export class WorkspaceRegistry {
|
||||
if (manifest.head !== state.head || !this.sameRevisions(manifest.revisions, state.revisions)) {
|
||||
throw new Error("manifest revisions do not match active state");
|
||||
}
|
||||
const expected = state.revisions.flatMap((revision) => [
|
||||
`${revision.id}.yaml`, `${revision.id}.env.example`, `${revision.id}.md`,
|
||||
]);
|
||||
const expected = state.revisions.flatMap((revision) => revision.state === "operational"
|
||||
? [`${revision.id}.yaml`, `${revision.id}.env.example`, `${revision.id}.md`]
|
||||
: [`${revision.id}.yaml`]);
|
||||
if (Object.keys(manifest.files).length !== expected.length || !expected.every((name) => (
|
||||
/^[0-9a-f]{64}$/.test(manifest.files[name] ?? "")
|
||||
))) throw new Error("manifest files are invalid");
|
||||
@@ -284,7 +314,8 @@ export class WorkspaceRegistry {
|
||||
const candidate = right[index];
|
||||
return candidate !== undefined
|
||||
&& candidate.id === revision.id && candidate.commit === revision.commit
|
||||
&& candidate.blob === revision.blob && candidate.snapshotPath === revision.snapshotPath;
|
||||
&& candidate.blob === revision.blob && candidate.snapshotPath === revision.snapshotPath
|
||||
&& candidate.state === revision.state;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user