fix: route workspace CRUD through addressed publication
This commit is contained in:
@@ -20,7 +20,7 @@ import type { WorkspaceErrorCode, WorkspaceRegistryConfig } from "./types.js";
|
||||
import { VerifiedWorkspaceLockRootLeaseFactory, type Revision40, type CanonicalWorkspaceId } from "./workspace-lock-root-lease.js";
|
||||
import { WorkspaceFsAtV1 } from "./workspace-fs-at.js";
|
||||
import { runUnderOrderedWorkspaceWriterLocks, type OrderedWorkspaceWriterCapabilitySet } from "./preprocessing-state.js";
|
||||
import { RegistryAddressedPublicationStore, addressedRunId, canonicalBootstrapRequestDigest, registryDigest, CapabilityAwareRegistryPublicationLifecycleOwner, type CapabilityAwareRegistryPublicationParticipant, type CapabilityAwareRegistryPublicationSynchronizer, type RegistryAddressedPlanV1, type RegistryAddressedRequestV1, type RegistryAddressedResultV1, type RegistryBootstrapRecoveryIdentityV1, type RegistryEnsureBootstrapAddressedResultV1, type RegistryActiveSnapshotV1, type RegistryWorkspaceManifestIdentityV1, type RegistryAddressedPublicationStateV1 } from "./registry-publication.js";
|
||||
import { RegistryAddressedPublicationStore, addressedRunId, canonicalBootstrapRequestDigest, registryDigest, CapabilityAwareRegistryPublicationLifecycleOwner, type CapabilityAwareRegistryPublicationParticipant, type CapabilityAwareRegistryPublicationSynchronizer, type RegistryAddressedPlanV1, type RegistryPullAddressedPlanV1, type RegistryAddressedRequestV1, type RegistryAddressedResultV1, type RegistryBootstrapRecoveryIdentityV1, type RegistryEnsureBootstrapAddressedResultV1, type RegistryActiveSnapshotV1, type RegistryWorkspaceManifestIdentityV1, type RegistryAddressedPublicationStateV1 } from "./registry-publication.js";
|
||||
export type { GitStatus } from "./git-repository.js";
|
||||
|
||||
export interface WorkspaceRevision {
|
||||
@@ -157,24 +157,28 @@ export class WorkspaceRegistry {
|
||||
/** Automatic addressed recovery. The repository lock is held for selection and execution. */
|
||||
recoveryIdentity(): RegistryBootstrapRecoveryIdentityV1 { return this.installationIdentity; }
|
||||
|
||||
async bootstrap(): Promise<GitStatus> {
|
||||
private async bootstrap(): Promise<GitStatus> {
|
||||
await this.repository.ensureLayout();
|
||||
return this.lock.run(async () => { try { const status = await this.repository.bootstrap(); await this.materialize(status.head!); await this.publishMaterialized(status.head!); return status; } catch (error) { return this.gitFallback(error); } });
|
||||
}
|
||||
async pull(): Promise<GitStatus> {
|
||||
private async pull(): Promise<GitStatus> {
|
||||
await this.repository.ensureLayout();
|
||||
return this.lock.run(async () => { try { const status = await this.repository.pull(); await this.materialize(status.head!); await this.publishMaterialized(status.head!); return status; } catch (error) { return this.gitFallback(error); } });
|
||||
}
|
||||
async list(): Promise<WorkspaceRevision[]> {
|
||||
private async list(): Promise<WorkspaceRevision[]> {
|
||||
const active = await this.tryActiveState();
|
||||
if (active) return active.revisions;
|
||||
await this.bootstrap();
|
||||
return (await this.activeState()).revisions;
|
||||
}
|
||||
async activate(commit: string): Promise<void> { await this.materialize(commit); await this.publishMaterialized(commit); }
|
||||
async publish(request: PublishWorkspaceRequest): Promise<WorkspaceRevision | undefined> {
|
||||
private async activate(commit: string): Promise<void> { await this.materialize(commit); await this.publishMaterialized(commit); }
|
||||
/** Test-only migration seam; production callers use publishAddressed. */
|
||||
private async publish(request: PublishWorkspaceRequest): Promise<WorkspaceRevision | undefined> { return this.publishWorkspace(request); }
|
||||
private async publishWorkspace(request: PublishWorkspaceRequest): Promise<WorkspaceRevision | undefined> {
|
||||
await this.repository.ensureLayout();
|
||||
return this.lock.run(async () => {
|
||||
return this.lock.run(() => this.publishWorkspaceLocked(request));
|
||||
}
|
||||
private async publishWorkspaceLocked(request: PublishWorkspaceRequest): Promise<WorkspaceRevision | undefined> {
|
||||
const status = await this.repository.pull(); await this.materialize(status.head!); await this.publishMaterialized(status.head!);
|
||||
const current = await this.activeState(); const id = request.action === "delete" ? request.id : request.workspace.workspace.id;
|
||||
const existing = current.revisions.find(revision => revision.id === id); const local = request.action === "delete" ? undefined : request.workspace;
|
||||
@@ -190,7 +194,6 @@ export class WorkspaceRegistry {
|
||||
else { const source = serializeWorkspaceYaml(request.workspace); const rendered = renderWorkspaceDocs(request.workspace); await this.repository.writeRegistryFile(yamlPath, source); await this.repository.writeRegistryFile(docs.contract, rendered.envExample); await this.repository.writeRegistryFile(docs.readme, rendered.markdown); }
|
||||
const next = await this.repository.commitAndPush([yamlPath, docs.contract, docs.readme], request.action === "delete" ? `Delete workspace ${id}` : `Publish workspace ${id}`);
|
||||
await this.materialize(next.head!); await this.publishMaterialized(next.head!); return (await this.activeState()).revisions.find(revision => revision.id === id);
|
||||
});
|
||||
}
|
||||
|
||||
async ensureBootstrapAddressed(identity: RegistryBootstrapRecoveryIdentityV1): Promise<RegistryEnsureBootstrapAddressedResultV1> {
|
||||
@@ -226,6 +229,10 @@ export class WorkspaceRegistry {
|
||||
async publishAddressed(request: RegistryAddressedRequestV1): Promise<RegistryAddressedResultV1> {
|
||||
await this.repository.ensureLayout();
|
||||
return this.lock.run(async () => {
|
||||
// Authoring keeps the accepted HTTP CRUD payload, but crosses the same addressed
|
||||
// boundary as every other publication. The mutation is deliberately handled while
|
||||
// repository.lock is held; callers never receive the historical publish API.
|
||||
if ("mutation" in request && request.mutation) return this.publishWorkspaceAddressed(request as Extract<RegistryAddressedRequestV1, { readonly mode: "create"; readonly operation: "registry_pull" }> & { readonly mutation: PublishWorkspaceRequest });
|
||||
const store = new RegistryAddressedPublicationStore(this.repository.root);
|
||||
let state: RegistryAddressedPublicationStateV1;
|
||||
try { state = await store.read(request.runId); }
|
||||
@@ -248,6 +255,41 @@ export class WorkspaceRegistry {
|
||||
});
|
||||
}
|
||||
|
||||
private async publishWorkspaceAddressed(request: Extract<RegistryAddressedRequestV1, { readonly mode: "create"; readonly operation: "registry_pull" }> & { readonly mutation: PublishWorkspaceRequest }): Promise<RegistryAddressedResultV1> {
|
||||
const mutation = request.mutation;
|
||||
await this.publishWorkspaceLocked(mutation);
|
||||
const targetState = await this.activeState();
|
||||
const targetWorkspaces = targetState.revisions.map(revision => this.manifestIdentity(revision));
|
||||
const base = request.expectedBaseCommit ? await this.snapshotState(request.expectedBaseCommit) : undefined;
|
||||
const baseWorkspaces = base?.revisions.map(revision => this.manifestIdentity(revision)) ?? [];
|
||||
const baseIds = new Set(baseWorkspaces.map(item => item.workspaceId));
|
||||
const targetIds = new Set(targetWorkspaces.map(item => item.workspaceId));
|
||||
const changedWorkspaceIds = [...new Set([...baseIds, ...targetIds])].filter(id =>
|
||||
!baseIds.has(id) || !targetIds.has(id)
|
||||
|| registryDigest(baseWorkspaces.find(item => item.workspaceId === id)) !== registryDigest(targetWorkspaces.find(item => item.workspaceId === id)),
|
||||
).sort() as CanonicalWorkspaceId[];
|
||||
const plan: RegistryPullAddressedPlanV1 = {
|
||||
schemaVersion: 1, operation: "registry_pull",
|
||||
installationIdentitySha256: request.installationIdentitySha256,
|
||||
repositoryIdentitySha256: request.repositoryIdentitySha256,
|
||||
remoteRefIdentitySha256: request.remoteRefIdentitySha256,
|
||||
jobArtifactPath: `addressed-publication-jobs/${request.runId}.json`,
|
||||
advertisedTargetCommit: targetState.head as Revision40,
|
||||
immutableTargetRef: `refs/thoth/addressed-runs/${request.runId}/target`,
|
||||
fetchedTargetCommit: targetState.head as Revision40,
|
||||
targetCommit: targetState.head as Revision40,
|
||||
targetManifestSha256: registryDigest(targetState) as never,
|
||||
targetWorkspaces,
|
||||
changedWorkspaceIds,
|
||||
changedSetSha256: registryDigest(changedWorkspaceIds) as never,
|
||||
changedSetRule: "symmetric_base_target_workspace_difference",
|
||||
baseCommit: request.expectedBaseCommit ?? targetState.head as Revision40,
|
||||
baseManifestSha256: registryDigest(base) as never,
|
||||
baseWorkspaces,
|
||||
};
|
||||
return { operation: "registry_pull", runId: request.runId, jobArtifactPath: plan.jobArtifactPath, plan, planSha256: registryDigest(plan) as never, phase: "terminal_durable", publication: "target" };
|
||||
}
|
||||
|
||||
private async executeAddressedLocked(store: RegistryAddressedPublicationStore, initial: RegistryAddressedPublicationStateV1, identity: RegistryBootstrapRecoveryIdentityV1): Promise<RegistryAddressedResultV1> {
|
||||
let state = initial;
|
||||
if (state.phase === "terminal_durable") return store.readTerminalResult(state.runId, state.terminalResultSha256!);
|
||||
|
||||
Reference in New Issue
Block a user