@kungfu-tech/buildchain 4.1.3-alpha.4 → 4.1.3-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/AGENTS.md +41 -75
  2. package/CONTRIBUTING.md +19 -26
  3. package/architecture/agent-change-map.md +8 -12
  4. package/architecture/capability-state-machine-manifest.json +1 -4
  5. package/architecture/ci-lane-change-budget.json +35 -1026
  6. package/architecture/core-mechanisms.json +3 -6
  7. package/architecture/internal-capabilities.json +1 -5
  8. package/architecture/maintainability-debt.json +44 -36
  9. package/architecture/maintainability-policy.json +8 -8
  10. package/architecture/minimal-consumer-migration.json +80 -1977
  11. package/architecture/platform-stage-checkpoints.json +31 -7
  12. package/architecture/release-topology.json +64 -84
  13. package/architecture/stage-capsule-qualification.json +3 -1
  14. package/architecture/universal-workflow-bootstrap.json +3 -1
  15. package/architecture/universal-workflow-capability-policy.json +4 -4
  16. package/architecture/universal-workflow-fault-campaign.json +3 -1
  17. package/architecture/workflow-taxonomy.json +24 -341
  18. package/dist/site/buildchain-contract.json +13 -67
  19. package/dist/site/buildchain-site.json +33 -23
  20. package/dist/site/capability-registry.json +2 -2
  21. package/dist/site/controller-registry.json +1 -47
  22. package/dist/site/kfd-claims.json +78 -448
  23. package/dist/site/kfd-upstream-aggregate.json +1 -1
  24. package/dist/site/manual-registry.json +3 -3
  25. package/dist/site/node-api-registry.json +24 -24
  26. package/dist/site/page-registry.json +26 -16
  27. package/dist/site/public-surface-audit.json +35 -349
  28. package/dist/site/publication-authority-registry.json +51 -511
  29. package/dist/site/publication-registry.json +4 -4
  30. package/dist/site/site-manifest.json +7 -7
  31. package/dist/site/workflow-registry.json +263 -1128
  32. package/docs/code-organization.md +9 -6
  33. package/docs/node-api-reference.md +57 -57
  34. package/docs/release-governance.md +10 -12
  35. package/docs/runtime-entry.md +31 -3
  36. package/docs/runtime-train-validation.md +39 -11
  37. package/docs/stable-candidate-patrol.md +6 -5
  38. package/docs/workflow-catalog.md +3 -32
  39. package/package.json +1 -1
  40. package/packages/core/adoption/buildchain-kfd-claims.js +7 -7
  41. package/packages/core/dev-delivery/dev-delivery-provider-heartbeat.js +18 -15
  42. package/packages/core/dev-delivery/native/actions.js +2 -0
  43. package/packages/core/dev-delivery/native/process-entry.js +19 -0
  44. package/packages/core/dev-delivery/queue/landing-action.js +49 -1
  45. package/packages/core/dev-delivery/warrant/failure-settlement.js +8 -6
  46. package/packages/core/dev-delivery/warrant/service.js +7 -0
  47. package/packages/core/governance/buildchain-publication-authority.js +2 -23
  48. package/packages/core/governance/github-governance-authority.js +1 -0
  49. package/packages/core/observability/controller-evidence.js +0 -8
  50. package/packages/core/providers/github/pipeline-entry-runs.js +8 -1
  51. package/packages/core/providers/github/pipeline-events.js +79 -0
  52. package/packages/core/providers/github/pipeline-source.js +24 -7
  53. package/packages/core/publication/pipeline/pack.js +31 -8
  54. package/packages/core/workflow/pipeline/actions.js +0 -23
  55. package/packages/core/workflow/pipeline/build-evidence.js +123 -5
  56. package/packages/core/workflow/pipeline/build-result.js +1 -0
  57. package/packages/core/workflow/pipeline/channel-control.js +25 -4
  58. package/packages/core/workflow/pipeline/controller.js +9 -1
  59. package/packages/core/workflow/pipeline/delivery-control.js +54 -9
  60. package/packages/core/workflow/pipeline/delivery-observation.js +16 -10
  61. package/packages/core/workflow/pipeline/group-control.js +1 -0
  62. package/packages/core/workflow/pipeline/guard-build.js +37 -0
  63. package/packages/core/workflow/pipeline/guard.js +4 -2
  64. package/packages/core/workflow/pipeline/host.js +23 -1
  65. package/packages/core/workflow/pipeline/integration-status.js +113 -0
  66. package/packages/core/workflow/pipeline/record-action.js +27 -0
  67. package/packages/core/workflow/pipeline/recorder-status.js +69 -0
  68. package/packages/core/workflow/workflow-taxonomy.mjs +67 -47
  69. package/scripts/check-inventory.mjs +7 -70
  70. package/scripts/check-pipeline-publication-topology.mjs +2 -0
  71. package/scripts/check-public-dogfood-contract.mjs +38 -51
  72. package/scripts/check-release-topology.mjs +8 -7
  73. package/scripts/check-self-consumer-contract.mjs +133 -0
  74. package/scripts/check-universal-workflow-bootstrap.mjs +4 -6
  75. package/scripts/generate-minimal-consumer-contract.mjs +1 -1
  76. package/scripts/inventory/binary.mjs +9 -19
  77. package/scripts/inventory/promotion.mjs +2 -13
  78. package/scripts/prepare-product-toolchain.mjs +10 -0
  79. package/scripts/verify-product-platform.mjs +65 -0
@@ -1,5 +1,84 @@
1
1
  import { readBusinessAttempt } from "../../workflow/attempt/reader.js";
2
2
 
3
+ // The current provider timeline survives webhook/runtime handoff. Compare it
4
+ // with this candidate's lifetime so an earlier removal cannot cancel recovery.
5
+ export function githubPipelineQueueExit(graphql, repository) {
6
+ if (!/^[\w.-]+\/[\w.-]+$/u.test(repository || ""))
7
+ throw new Error("Invalid pipeline queue repository");
8
+ const [owner, repo] = repository.split("/");
9
+ return async (current, candidate) => {
10
+ if (!candidate || candidate.terminal) return null;
11
+ const started = Date.parse(candidate.enqueuedAt);
12
+ if (!Number.isFinite(started))
13
+ throw new Error("Queue candidate timestamp is missing");
14
+ const number = current.intent.source.pullRequest;
15
+ const result = await graphql(
16
+ `
17
+ query ($owner: String!, $repo: String!, $number: Int!) {
18
+ repository(owner: $owner, name: $repo) {
19
+ pullRequest(number: $number) {
20
+ number
21
+ headRefOid
22
+ baseRefName
23
+ state
24
+ mergeQueueEntry {
25
+ id
26
+ }
27
+ timelineItems(
28
+ last: 2
29
+ itemTypes: [
30
+ ADDED_TO_MERGE_QUEUE_EVENT
31
+ REMOVED_FROM_MERGE_QUEUE_EVENT
32
+ ]
33
+ ) {
34
+ nodes {
35
+ __typename
36
+ ... on AddedToMergeQueueEvent {
37
+ id
38
+ createdAt
39
+ }
40
+ ... on RemovedFromMergeQueueEvent {
41
+ id
42
+ createdAt
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
49
+ `,
50
+ { owner, repo, number },
51
+ );
52
+ const pr = result.repository?.pullRequest;
53
+ if (
54
+ pr?.number !== number ||
55
+ pr.headRefOid !== current.generation.source.commit ||
56
+ pr.baseRefName !== current.intent.source.targetBranch
57
+ )
58
+ throw new Error("Queue exit source identity changed; reobserve");
59
+ if (pr.state !== "OPEN" || pr.mergeQueueEntry) return null;
60
+ const nodes = pr.timelineItems?.nodes;
61
+ if (!Array.isArray(nodes) || nodes.length > 2)
62
+ throw new Error("Queue exit timeline readback is missing or ambiguous");
63
+ const event = nodes.at(-1);
64
+ if (event?.__typename !== "RemovedFromMergeQueueEvent") return null;
65
+ const queued = nodes.at(-2);
66
+ if (queued?.__typename !== "AddedToMergeQueueEvent")
67
+ throw new Error("Queue removal has no matching admission readback");
68
+ const removed = Date.parse(event.createdAt);
69
+ const entered = Date.parse(queued.createdAt);
70
+ if (
71
+ !event.id ||
72
+ !queued.id ||
73
+ !Number.isFinite(removed) ||
74
+ !Number.isFinite(entered) ||
75
+ entered > removed
76
+ )
77
+ throw new Error("Queue removal identity or timestamp is missing");
78
+ return entered > started ? event : null;
79
+ };
80
+ }
81
+
3
82
  // Branch/terminal notifications wake existing intents only. They cannot create
4
83
  // historical releases or infer an execution request from an untracked old PR.
5
84
  export function githubPipelineEvents(request, journal, repository) {
@@ -10,17 +10,34 @@ function exactSha(value) {
10
10
  return value;
11
11
  }
12
12
 
13
+ function sourceRefBinding(reference) {
14
+ return {
15
+ ref: reference?.ref ?? null,
16
+ sha: reference?.sha ?? null,
17
+ repository: {
18
+ id: reference?.repo?.id ?? null,
19
+ nodeId: reference?.repo?.node_id ?? null,
20
+ fullName: reference?.repo?.full_name ?? null,
21
+ },
22
+ };
23
+ }
24
+
25
+ function mergedCommit(pr) {
26
+ return pr.merged === true ? pr.merge_commit_sha : null;
27
+ }
28
+
13
29
  function pullRequestBinding(pr) {
30
+ // Provider timestamps and repository metadata can change during our own
31
+ // evidence writes. Bind source and delivery state, not those unrelated fields.
14
32
  return {
15
33
  number: pr.number,
16
34
  state: pr.state,
17
35
  merged: pr.merged,
18
- mergeCommit: pr.merge_commit_sha,
19
- head: pr.head,
20
- base: pr.base,
36
+ mergeCommit: mergedCommit(pr),
37
+ head: sourceRefBinding(pr.head),
38
+ base: sourceRefBinding(pr.base),
21
39
  draft: pr.draft,
22
- labels: pr.labels,
23
- updatedAt: pr.updated_at,
40
+ ready: pr.labels.some((label) => label.name === "ready"),
24
41
  };
25
42
  }
26
43
 
@@ -113,7 +130,7 @@ export function githubPipelineSource(request, repository) {
113
130
  baseCommit,
114
131
  state: pr.state,
115
132
  merged: pr.merged === true,
116
- mergeCommit: pr.merge_commit_sha,
133
+ mergeCommit: mergedCommit(pr),
117
134
  draft: pr.draft === true,
118
135
  ready: pr.labels.some((label) => label.name === "ready"),
119
136
  },
@@ -141,7 +158,7 @@ export function githubPipelineSource(request, repository) {
141
158
  targetBranch: pr.base.ref,
142
159
  state: pr.state,
143
160
  merged: pr.merged === true,
144
- mergeCommit: pr.merge_commit_sha,
161
+ mergeCommit: mergedCommit(pr),
145
162
  source: sameHead ? generation.source : null,
146
163
  baseCommit: null,
147
164
  ready: pr.labels.some((label) => label.name === "ready"),
@@ -44,6 +44,33 @@ function packageArtifact(directory, output, expected, version, environment) {
44
44
  };
45
45
  }
46
46
 
47
+ function archiveNames(file, suffix) {
48
+ const options = {
49
+ cwd: path.dirname(file),
50
+ encoding: "utf8",
51
+ maxBuffer: 16 * 1024 * 1024,
52
+ timeout: 30000,
53
+ stdio: ["ignore", "pipe", "pipe"],
54
+ };
55
+ if (suffix === ".zip") {
56
+ const script = [
57
+ "import json,sys,zipfile",
58
+ "with zipfile.ZipFile(sys.argv[1]) as archive:",
59
+ " print(json.dumps(archive.namelist()))",
60
+ ].join("\n");
61
+ return JSON.parse(
62
+ execFileSync(
63
+ process.platform === "win32" ? "python" : "python3",
64
+ ["-c", script, path.basename(file)],
65
+ options,
66
+ ),
67
+ );
68
+ }
69
+ return execFileSync("tar", ["-tf", path.basename(file)], options)
70
+ .trim()
71
+ .split(/\r?\n/u);
72
+ }
73
+
47
74
  export function inspectPipelineFileArtifact(directory, expected) {
48
75
  const file = publicationPath(directory, expected.path);
49
76
  if (expected.kind === "pdf") {
@@ -60,18 +87,14 @@ export function inspectPipelineFileArtifact(directory, expected) {
60
87
  );
61
88
  if (!suffix)
62
89
  throw new Error("Declared binary output must be a standard archive");
63
- const names = execFileSync("tar", ["-tf", path.basename(file)], {
64
- cwd: path.dirname(file),
65
- encoding: "utf8",
66
- maxBuffer: 16 * 1024 * 1024,
67
- })
68
- .trim()
69
- .split(/\r?\n/u);
90
+ const names = archiveNames(file, suffix);
70
91
  if (
71
92
  !names[0] ||
72
93
  names.some(
73
94
  (name) =>
74
- name.startsWith("/") ||
95
+ !name.trim() ||
96
+ /[\u0000-\u001f\u007f]/u.test(name) ||
97
+ name.replaceAll("\\", "/").startsWith("/") ||
75
98
  /^[A-Za-z]:/u.test(name) ||
76
99
  name.replaceAll("\\", "/").split("/").includes(".."),
77
100
  )
@@ -5,12 +5,7 @@ import { normalInputs } from "../../consumer/contract/entries.js";
5
5
  import { pipelineHost } from "./host.js";
6
6
  import { controlPipeline } from "./controller.js";
7
7
  import { buildPipelineSource } from "./build.js";
8
- import { recordPipelineBuild } from "./build-control.js";
9
8
  import { wakePipelineStableWait } from "../../publication/pipeline/stable-wait.js";
10
- import {
11
- RECOVERY_BUILD_CONTEXT,
12
- recordRecoveryBuild,
13
- } from "./recovery-build-control.js";
14
9
 
15
10
  export async function controlPipelineAction(core, env) {
16
11
  const { configPath } = normalInputs({
@@ -39,24 +34,6 @@ export async function buildPipelineAction(core, env) {
39
34
  );
40
35
  }
41
36
 
42
- export async function recordPipelineBuildAction(core, env) {
43
- const host = await pipelineHost(core, env, "Record product build");
44
- const context = JSON.parse(core.getInput("context", { required: true }));
45
- if (context.schema === "buildchain.pipeline-group-build-context/v1")
46
- return host.groupRecord(context);
47
- const result =
48
- context.schema === RECOVERY_BUILD_CONTEXT
49
- ? await recordRecoveryBuild(context, host)
50
- : await recordPipelineBuild(context, host);
51
- if (result.outcome !== "success")
52
- throw new Error("Product build did not succeed");
53
- core.info(
54
- result.wakePending
55
- ? "Build retained; next event will retry delivery wake"
56
- : "Build retained and attempt woken",
57
- );
58
- }
59
-
60
37
  export async function wakePipelineAction(core, env) {
61
38
  const waiting = core.getInput("stable-wait");
62
39
  const host = await pipelineHost(
@@ -5,6 +5,83 @@ import {
5
5
  } from "./build-qualification.js";
6
6
  import { pipelinePlatforms } from "./platforms.js";
7
7
 
8
+ const RECOVERY_CHECK = "Buildchain recovery verification";
9
+
10
+ // Dispatch-created Actions checks do not satisfy protected PR checks. Preserve
11
+ // their evidence under a diagnostic name instead of shadowing normal PR checks.
12
+ export async function observePipelineBuildChecks(source, request, repository) {
13
+ if (
14
+ source.repository !== repository ||
15
+ !/^[0-9a-f]{40}$/u.test(source.commit)
16
+ )
17
+ throw new Error("Recovery check repository or source identity drift");
18
+ const base = `/repos/${repository}`,
19
+ renames = [],
20
+ eligible = [];
21
+ for (let page = 1; page <= 20; page++) {
22
+ const result = await request(
23
+ `${base}/commits/${source.commit}/check-runs?filter=all&per_page=100&page=${page}`,
24
+ );
25
+ if (!Array.isArray(result.check_runs) || result.check_runs.length > 100)
26
+ throw new Error("Recovery check inventory is incomplete");
27
+ for (const check of result.check_runs) {
28
+ const match = /^buildchain:(attempt-[0-9a-f]{64}):(\d+):(\d+)$/u.exec(
29
+ check.external_id || "",
30
+ );
31
+ if (
32
+ check.name !== "check" ||
33
+ check.app?.slug !== "github-actions" ||
34
+ !match
35
+ )
36
+ continue;
37
+ const runId = Number(match[2]),
38
+ runAttempt = Number(match[3]);
39
+ if (
40
+ check.head_sha !== source.commit ||
41
+ ![check.id, runId, runAttempt].every(
42
+ (value) => Number.isSafeInteger(value) && value > 0,
43
+ )
44
+ )
45
+ throw new Error("Recovery check provider identity drift");
46
+ const run = await request(
47
+ `${base}/actions/runs/${runId}/attempts/${runAttempt}`,
48
+ );
49
+ if (
50
+ run.id !== runId ||
51
+ run.run_attempt !== runAttempt ||
52
+ run.repository?.full_name !== repository
53
+ )
54
+ throw new Error("Recovery check execution identity drift");
55
+ if (["workflow_dispatch", "repository_dispatch"].includes(run.event))
56
+ renames.push({
57
+ id: check.id,
58
+ externalId: check.external_id,
59
+ sourceHead: source.commit,
60
+ runId,
61
+ runAttempt,
62
+ name: RECOVERY_CHECK,
63
+ });
64
+ else if (
65
+ [
66
+ "push",
67
+ "pull_request",
68
+ "pull_request_review",
69
+ "pull_request_target",
70
+ ].includes(run.event) &&
71
+ run.head_sha === source.commit
72
+ )
73
+ eligible.push({
74
+ id: check.id,
75
+ runId,
76
+ runAttempt,
77
+ conclusion: check.conclusion,
78
+ });
79
+ }
80
+ if (result.check_runs.length < 100) return { renames, eligible };
81
+ }
82
+ throw new Error("Recovery check inventory exceeds its complete-read bound");
83
+ }
84
+
8
85
  // The archive reference is already validated against the current attempt. A
9
86
  // build result is still re-read from the provider before it can admit delivery.
10
87
  export async function pipelineBuildEvidence(session, host) {
@@ -74,18 +151,59 @@ export async function publishPipelineBuildCheck(
74
151
  "Product check source differs from independently verified build",
75
152
  );
76
153
  const externalId = `buildchain:${context.attempt}:${context.runId}:${context.runAttempt}`;
154
+ const recovered = readback.schema === PIPELINE_BUILD_QUALIFICATION;
155
+ let checkName = recovered ? RECOVERY_CHECK : "check";
156
+ const output = {
157
+ title: recovered
158
+ ? "Buildchain recovered product verification"
159
+ : "Buildchain product verification",
160
+ summary: `Exact product jobs: ${readback.root}\nAttempt: ${context.attempt}`,
161
+ };
162
+ if (recovered) {
163
+ const checks = await observePipelineBuildChecks(
164
+ context.source,
165
+ request,
166
+ repository,
167
+ );
168
+ for (const change of checks.renames)
169
+ await request(`/repos/${repository}/check-runs/${change.id}`, {
170
+ method: "PATCH",
171
+ body: { name: change.name },
172
+ });
173
+ const original = checks.eligible.find((check) =>
174
+ readback.segments.some(
175
+ (segment) =>
176
+ segment.readback.runId === check.runId &&
177
+ segment.readback.runAttempt === check.runAttempt,
178
+ ),
179
+ );
180
+ // A complete replacement has no reusable original check to project onto.
181
+ // Retain that failed execution and publish this independently qualified
182
+ // aggregate as the required check. Protected landing still requalifies it
183
+ // and publishes the App-bound commit status before queue admission.
184
+ if (!original) checkName = "check";
185
+ // Keep the eligible normal-event check identity. Only a fully requalified
186
+ // source aggregate can repair its failed projection; original runs and
187
+ // immutable build receipts still retain their original failure outcomes.
188
+ if (
189
+ original &&
190
+ original.conclusion !== "success" &&
191
+ readback.outcome === "success"
192
+ )
193
+ await request(`/repos/${repository}/check-runs/${original.id}`, {
194
+ method: "PATCH",
195
+ body: { status: "completed", conclusion: "success", output },
196
+ });
197
+ }
77
198
  return request(`/repos/${repository}/check-runs`, {
78
199
  method: "POST",
79
200
  body: {
80
- name: "check",
201
+ name: checkName,
81
202
  head_sha: context.source.commit,
82
203
  external_id: externalId,
83
204
  status: "completed",
84
205
  conclusion: readback.outcome === "success" ? "success" : "failure",
85
- output: {
86
- title: "Buildchain product verification",
87
- summary: `Exact product jobs: ${readback.root}\nAttempt: ${context.attempt}`,
88
- },
206
+ output,
89
207
  details_url: `https://github.com/${repository}/actions/runs/${context.runId}/attempts/${context.runAttempt}`,
90
208
  },
91
209
  });
@@ -61,6 +61,7 @@ export async function retainPipelineBuildResult(
61
61
  host.request,
62
62
  host.repository,
63
63
  );
64
+ await host.publishBuildStatus?.(context, readback);
64
65
  if (
65
66
  readback.schema === PIPELINE_BUILD_QUALIFICATION &&
66
67
  readback.outcome === "success"
@@ -3,17 +3,38 @@ import { pipelineBuildEvidence } from "./build-evidence.js";
3
3
 
4
4
  async function record(session, phase, state, receipt, host, reason = "") {
5
5
  const observed = await session.journal.read();
6
+ if (observed.attempt !== session.observed.attempt)
7
+ throw new Error("Channel receipt belongs to a superseded attempt");
6
8
  const root = recordDigest(receipt);
7
- const reference = await host
8
- .materialStore(session)
9
- .retain(`${phase}/${root.slice(7)}`, receipt, "provider-readback");
9
+ const eventKey = `${phase}:${state}:${root}`;
10
+ const store = host.materialStore(session);
11
+ const prior = observed.history
12
+ .at(-1)
13
+ .events.find((event) => event.payload.eventKey === eventKey);
14
+ if (prior) {
15
+ if (
16
+ prior.node !== phase ||
17
+ prior.payload.state !== state ||
18
+ prior.payload.reason !== reason ||
19
+ prior.payload.materials.length !== 1 ||
20
+ recordDigest(await store.read(prior.payload.materials[0])) !== root
21
+ )
22
+ throw new Error("Channel retry changed its immutable receipt");
23
+ return observed;
24
+ }
25
+ const reference = await store.retain(
26
+ `${phase}/${root.slice(7)}`,
27
+ receipt,
28
+ "provider-readback",
29
+ );
10
30
  return session.progress.progress({
11
31
  attempt: observed.attempt,
12
32
  phase,
13
33
  state,
14
- eventKey: `${phase}:${state}:${root}`,
34
+ eventKey,
15
35
  reason,
16
36
  materials: [reference],
37
+ expectedHead: observed.head,
17
38
  });
18
39
  }
19
40
 
@@ -2,7 +2,10 @@ import { pipelineEvent } from "./events.js";
2
2
  import { selectPipelineSession } from "./selection.js";
3
3
  import { beginPipelineBuild } from "./build-control.js";
4
4
  import { controlPipelineDelivery } from "./delivery-control.js";
5
- import { githubPipelineEvents } from "../../providers/github/pipeline-events.js";
5
+ import {
6
+ githubPipelineEvents,
7
+ githubPipelineQueueExit,
8
+ } from "../../providers/github/pipeline-events.js";
6
9
  import { pipelinePlatforms } from "./platforms.js";
7
10
  import { recordDigest } from "../../release/discussion/envelope.js";
8
11
 
@@ -95,6 +98,11 @@ export async function controlPipeline(name, payload, inputs, host) {
95
98
  attempt: session.observed.attempt,
96
99
  };
97
100
  }
101
+ if (session.intent.expectedNodes.includes("warrant"))
102
+ host.queueExit ||= githubPipelineQueueExit(
103
+ host.github.graphql,
104
+ host.repository,
105
+ );
98
106
  const operation = session.intent.expectedNodes.includes("warrant")
99
107
  ? await controlPipelineDelivery(session, inputs, host)
100
108
  : await host.channel(session, admission, inputs);
@@ -3,6 +3,7 @@ import { preparePipelineDelivery } from "./delivery-request.js";
3
3
  import { pipelineBuildEvidence } from "./build-evidence.js";
4
4
  import {
5
5
  observePipelineWorker,
6
+ pipelineDeliveryExecution,
6
7
  pipelineDeliveryStore,
7
8
  } from "./delivery-observation.js";
8
9
  import { reconcilePipeline, pipelineCandidate } from "./reconcile.js";
@@ -21,8 +22,14 @@ export async function observePipelineDelivery(session, inputs, host, delivery) {
21
22
  inputs,
22
23
  );
23
24
  admission.live.terminalOnly = host.terminalOnly === true;
24
- admission.live.dequeued = host.eventAction === "dequeued";
25
25
  const queue = await delivery.read();
26
+ admission.live.queueExit =
27
+ admission.live.state === "open" &&
28
+ admission.live.source?.commit === current.generation.source.commit &&
29
+ admission.live.targetBranch === session.intent.source.targetBranch
30
+ ? await host.queueExit(current, pipelineCandidate(queue, current))
31
+ : null;
32
+ admission.live.dequeued = Boolean(admission.live.queueExit);
26
33
  const worker = await observePipelineWorker(session, current, queue, host);
27
34
  const input = { current, live: admission.live, queue, worker, evidence: {} };
28
35
  return {
@@ -68,6 +75,30 @@ async function retainDeliveryRequest(fresh, session, build, host) {
68
75
  ...scheduling.history.at(-1),
69
76
  intent: session.intent,
70
77
  };
78
+ // The provider execution owns the delivery slot before it acquires a
79
+ // Warrant. A later wake must not replace its retained coordinates while its
80
+ // queued or running job is still entitled to perform final admission.
81
+ const executionOwner = await pipelineDeliveryExecution(
82
+ session,
83
+ schedulingCurrent,
84
+ host,
85
+ );
86
+ if (executionOwner) {
87
+ const { run } = await host.runs.read(
88
+ executionOwner.runId,
89
+ executionOwner.runAttempt,
90
+ );
91
+ if (
92
+ run.id !== executionOwner.runId ||
93
+ run.run_attempt !== executionOwner.runAttempt
94
+ )
95
+ throw new Error("Retained delivery execution provider identity drift");
96
+ if (run.status !== "completed")
97
+ return {
98
+ operation: "wait",
99
+ reason: "admitted-delivery-execution-running",
100
+ };
101
+ }
71
102
  const active = await observePipelineWorker(
72
103
  session,
73
104
  schedulingCurrent,
@@ -145,14 +176,28 @@ export async function controlPipelineDelivery(session, inputs, host) {
145
176
  !["cancelled", "failure", "superseded", "complete"].includes(
146
177
  fresh.observed.status,
147
178
  )
148
- )
149
- await session.progress.progress({
150
- attempt: fresh.attempt,
151
- phase: fresh.phase,
152
- state: "superseded",
153
- eventKey: `supersede:${recordDigest(fresh.live)}`,
154
- reason: fresh.decision.reason,
155
- });
179
+ ) {
180
+ try {
181
+ await session.progress.progress({
182
+ attempt: fresh.attempt,
183
+ phase: fresh.phase,
184
+ state: "superseded",
185
+ eventKey: `supersede:${recordDigest(fresh.live)}`,
186
+ reason: fresh.decision.reason,
187
+ });
188
+ } catch (error) {
189
+ // Concurrent notifications can finish the same attempt after readback.
190
+ // Its immutable terminal result is sufficient; never rewrite that phase.
191
+ const latest = await session.journal.read();
192
+ if (
193
+ latest.attempt !== fresh.attempt ||
194
+ !["cancelled", "failure", "superseded", "complete"].includes(
195
+ latest.status,
196
+ )
197
+ )
198
+ throw error;
199
+ }
200
+ }
156
201
  return { operation: "successor", reason: fresh.decision.reason };
157
202
  }
158
203
  if (fresh.live.merged) return { operation: "settle", fresh, delivery };
@@ -13,16 +13,7 @@ function workerJob(jobs, name) {
13
13
  return selected[0];
14
14
  }
15
15
 
16
- export async function observePipelineWorker(session, current, queue, host) {
17
- const warrant = queue.activeWarrant;
18
- const candidate = pipelineCandidate(queue, current);
19
- if (
20
- !warrant ||
21
- candidate?.candidateId !== warrant.candidateId ||
22
- warrant.sourceHead !== current.generation.source.commit ||
23
- warrant.pullRequestNumber !== current.intent.source.pullRequest
24
- )
25
- return null;
16
+ export async function pipelineDeliveryExecution(session, current, host) {
26
17
  const phase = [...current.events]
27
18
  .reverse()
28
19
  .find((event) =>
@@ -43,6 +34,21 @@ export async function observePipelineWorker(session, current, queue, host) {
43
34
  execution.generation !== current.generation.id
44
35
  )
45
36
  throw new Error("Native worker execution belongs to another attempt");
37
+ return execution;
38
+ }
39
+
40
+ export async function observePipelineWorker(session, current, queue, host) {
41
+ const warrant = queue.activeWarrant;
42
+ const candidate = pipelineCandidate(queue, current);
43
+ if (
44
+ !warrant ||
45
+ candidate?.candidateId !== warrant.candidateId ||
46
+ warrant.sourceHead !== current.generation.source.commit ||
47
+ warrant.pullRequestNumber !== current.intent.source.pullRequest
48
+ )
49
+ return null;
50
+ const execution = await pipelineDeliveryExecution(session, current, host);
51
+ if (!execution) return null;
46
52
  const { run, jobs } = await host.runs.read(
47
53
  execution.runId,
48
54
  execution.runAttempt,
@@ -38,6 +38,7 @@ export async function recordPipelineGroup(context, host) {
38
38
  );
39
39
  if (readback.outcome === "success")
40
40
  await host.qualifyChannel(context.source, context.branch);
41
+ await host.publishBuildStatus?.(context, readback);
41
42
  await host.request(`/repos/${host.repository}/check-runs`, {
42
43
  method: "POST",
43
44
  body: {
@@ -35,3 +35,40 @@ export async function guardPipelineBuild(
35
35
  );
36
36
  return build;
37
37
  }
38
+
39
+ // A commit status is source-bound and survives later workflow suites. Publish
40
+ // only the independently reread build at the protected landing boundary, whose
41
+ // existing Actions credential owns statuses:write. Retained build receipts
42
+ // preserve the original executions and their outcomes.
43
+ export async function publishPipelineBuildStatus(
44
+ input,
45
+ build,
46
+ request,
47
+ repository,
48
+ ) {
49
+ if (
50
+ build.readback.source.repository !== repository ||
51
+ build.readback.source.commit !== input["expected-head-sha"] ||
52
+ build.sourceHead !== input["expected-head-sha"] ||
53
+ build.run.id !== Number(input["source-workflow-run-id"]) ||
54
+ build.readback.runId !== build.run.id ||
55
+ build.readback.runAttempt !== build.run.run_attempt ||
56
+ build.outcome !== "success" ||
57
+ build.readback.outcome !== "success"
58
+ )
59
+ throw new Error(
60
+ "Landing status differs from its qualified exact-source build",
61
+ );
62
+ return request(`/repos/${repository}/statuses/${build.sourceHead}`, {
63
+ method: "POST",
64
+ body: {
65
+ context: "check",
66
+ state: "success",
67
+ description: `Qualified product build: ${build.readback.root}`.slice(
68
+ 0,
69
+ 140,
70
+ ),
71
+ target_url: `https://github.com/${repository}/actions/runs/${build.run.id}/attempts/${build.run.run_attempt}`,
72
+ },
73
+ });
74
+ }
@@ -105,13 +105,15 @@ export async function guardPipelineAdmission(input, connection) {
105
105
  throw new Error(
106
106
  "Pipeline protected review or required checks changed before delivery effect",
107
107
  );
108
- await guardPipelineBuild(input, observed, {
108
+ const build = await guardPipelineBuild(input, observed, {
109
109
  material,
110
110
  source,
111
111
  request,
112
112
  repository: connection.repository,
113
113
  });
114
- return guardPipelineDelivery(input, connection);
114
+ const fresh = await guardPipelineDelivery(input, connection);
115
+ await connection.publishBuildStatus?.(build);
116
+ return fresh;
115
117
  }
116
118
 
117
119
  export async function guardPipelineDelivery(input, connection, lookup) {