@kungfu-tech/buildchain 3.0.2-alpha.3 → 3.0.2-alpha.4

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.
@@ -4,7 +4,7 @@
4
4
  "product": {
5
5
  "name": "Buildchain",
6
6
  "package": "@kungfu-tech/buildchain",
7
- "version": "3.0.2-alpha.3",
7
+ "version": "3.0.2-alpha.4",
8
8
  "repository": "https://github.com/kungfu-systems/buildchain"
9
9
  },
10
10
  "majorLine": "v3",
@@ -170,7 +170,7 @@
170
170
  "GitHub Release passport and evidence publication is delegated to promote-buildchain-ref after the semver release transaction completes"
171
171
  ],
172
172
  "breakingDigest": "sha256:acd401cfc46450115a3763fd4b679d85f185e8757ebd52510d6262e1533df4cf",
173
- "auditDigest": "sha256:26993d90db926a0e086947a28359242cdaba042f0ceacee75f680216d6143ab7"
173
+ "auditDigest": "sha256:366a389cde040e8c0b1cebe65587565871288941d886c775693fd87c99f42b9d"
174
174
  },
175
175
  {
176
176
  "contractVersion": 1,
@@ -713,7 +713,7 @@
713
713
  "agents can discover supported Node APIs without importing internal file paths"
714
714
  ],
715
715
  "breakingDigest": "sha256:48f925608d3e2131d90936b07dc2a30341204cae3e6e785c0f77d61ad755c945",
716
- "auditDigest": "sha256:b1617817941f332dc666ca941bce82491c2324abef127e3c6e9800d86a01dd6b"
716
+ "auditDigest": "sha256:74b2abc26f004a55e36f0c746925255281b5df3de1ce4efb141f558919bf28fd"
717
717
  },
718
718
  {
719
719
  "contractVersion": 1,
@@ -3311,5 +3311,5 @@
3311
3311
  }
3312
3312
  ],
3313
3313
  "compatibilityDigest": "sha256:008b1c5a90a787cf30106829ac7aca68ab501f6d9c375e415d9c5c6b89039f46",
3314
- "contractDigest": "sha256:33cde8db3883afe7d7695d2c2e332a8c6753a804e14db0c8e3d2e5d2eb492271"
3314
+ "contractDigest": "sha256:d5d86b0bfb22f11daae2e9f6ecc9444e5d3a6ee1b35115d7a9e2692f3364ab73"
3315
3315
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "contract": "kungfu-buildchain-site-bundle",
4
- "generatedAt": "2026-07-27T01:10:48.385Z",
5
- "publishedAt": "2026-07-27T01:10:48.385Z",
4
+ "generatedAt": "2026-07-27T05:08:19.148Z",
5
+ "publishedAt": "2026-07-27T05:08:19.148Z",
6
6
  "reproducible": true,
7
7
  "timestampPolicy": "ci-injected",
8
8
  "deterministicInputs": [
@@ -19,7 +19,7 @@
19
19
  "declared Buildchain surface manifest contract"
20
20
  ],
21
21
  "sourceDateEpoch": "0",
22
- "sourceRevision": "8b1b52cc9dc5eb0a89f2aae3acfd08a22dae6934",
22
+ "sourceRevision": "ba9a63a1960beab3660cacaca76ceed02ea9a0f1",
23
23
  "timestampPolicyDetails": {
24
24
  "contract": "kungfu-buildchain-surface-timestamp-policy",
25
25
  "timestampFields": [
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "package": {
39
39
  "name": "@kungfu-tech/buildchain",
40
- "version": "3.0.2-alpha.3",
40
+ "version": "3.0.2-alpha.4",
41
41
  "versionSource": "package.json#version"
42
42
  },
43
43
  "source": {
@@ -640,7 +640,7 @@
640
640
  ],
641
641
  "maturity": "stable",
642
642
  "sourcePath": "docs/dev-alpha-candidate-patrol.md",
643
- "digest": "sha256:ebaec4d54f0f66fd82ec24b34e227b20f90a4a76cf724cf94d9e912422ee89a2",
643
+ "digest": "sha256:ffd96da21ed6a99e01d60b6545ca4a486d6c22dbf053f717a3a2cce5c3970a28",
644
644
  "headings": [
645
645
  {
646
646
  "level": 1,
@@ -653,7 +653,7 @@
653
653
  "anchor": "reusable-workflow"
654
654
  }
655
655
  ],
656
- "markdown": "---\nstatus: preview\nperiod: ongoing\ntheme: dev-alpha-candidate-patrol\ndoc_type: architecture-and-usage\nsource_level: local-files\nconfidence: high\nsensitivity: public\nevidence_grade: A\nreview_state: self-reviewed\nlast_reviewed: 2026-07-26\nai_provenance:\n model_family: GPT-5\n product: Codex\n generated_at: 2026-07-26\n visible_context: Existing Buildchain stable-candidate source locks, Kungfu exact-source Alpha preflight, Dev Patrol, and repository release governance.\n invisible_context_boundary: No credentials, private logs, or private configuration were used.\n---\n\n# Dev to Alpha Candidate Patrol\n\nBuildchain provides a reusable observation and PR controller for repositories\nthat promote a development branch into a protected Alpha branch. It does not\npublish Alpha. The controller reads the exact heads of both branches and accepts\nthe development head only when:\n\n- the source is strictly ahead of the recorded target head;\n- the latest completed Dev Patrol for that exact source SHA succeeded;\n- the latest completed Alpha preflight for the same SHA succeeded; and\n- both runs are within the caller's evidence age limit.\n\nThe decision is `kungfu-buildchain-channel-candidate-decision/v1`. It records the\nsource and target branches and SHAs, comparison distance, workflow paths, run\nidentities and attempts, completion times, URLs, policy, and a canonical decision\nroot. Missing, stale, failed, duplicate, or source-mismatched evidence fails\nclosed.\n\n## Reusable workflow\n\nCall `.github/workflows/dev-alpha-candidate-patrol.yml` from a thin repository\nworkflow. Start with `dry-run: true`. Once the repository has proven that its\ntwo workflow names and branch topology produce exact same-SHA evidence, it may\nset `create-pull-request: true` and `dry-run: false`.\n\nCandidate mode creates one branch named from the target branch and the first 12\ncharacters of the full source SHA. An existing branch must point to the same\nfull SHA or the run fails. The controller then creates or reuses one open pull\nrequest from that immutable branch to the protected Alpha branch.\n\nThe workflow never moves the Alpha ref directly, merges or auto-merges the pull\nrequest, publishes npm, creates a Git tag or GitHub Release, or changes branch\nprotection. Those remain repository-owned protected settlement actions."
656
+ "markdown": "---\nstatus: preview\nperiod: ongoing\ntheme: dev-alpha-candidate-patrol\ndoc_type: architecture-and-usage\nsource_level: local-files\nconfidence: high\nsensitivity: public\nevidence_grade: A\nreview_state: self-reviewed\nlast_reviewed: 2026-07-26\nai_provenance:\n model_family: GPT-5\n product: Codex\n generated_at: 2026-07-26\n visible_context: Existing Buildchain stable-candidate source locks, Kungfu exact-source Alpha preflight, Dev Patrol, and repository release governance.\n invisible_context_boundary: No credentials, private logs, or private configuration were used.\n---\n\n# Dev to Alpha Candidate Patrol\n\nBuildchain provides a reusable observation and PR controller for repositories\nthat promote a development branch into a protected Alpha branch. It does not\npublish Alpha. The controller reads the exact heads of both branches, walks the\nbounded development history from newest to oldest (stopping early at the Alpha\nhead), and selects the newest commit that satisfies all of these conditions:\n\n- the source is strictly ahead of the recorded target head;\n- the latest completed Dev Patrol for that exact commit SHA succeeded;\n- the latest completed Alpha preflight for the same commit SHA succeeded; and\n- both runs are within the caller's evidence age limit.\n\nThe selected commit can be behind the observed development head when newer\ncommits have not completed both workflows yet. The decision binds the observed\nhead, selected SHA, and count of skipped newer commits. This makes a slow native\nverification lane live under continuous development without silently treating\nan unqualified head as releasable.\n\nHistory discovery is bounded to the newest 1000 development commits. The\ncontroller then compares the selected SHA to the exact Alpha head before it can\nbe eligible, so a bounded scan cannot turn a commit outside the promotion\nancestry into a candidate.\n\nThe decision is `kungfu-buildchain-channel-candidate-decision/v1`. It records the\nsource and target branches and SHAs, comparison distance, workflow paths, run\nidentities and attempts, completion times, URLs, policy, and a canonical decision\nroot. Missing, stale, failed, duplicate, or source-mismatched evidence fails\nclosed.\n\n## Reusable workflow\n\nCall `.github/workflows/dev-alpha-candidate-patrol.yml` from a thin repository\nworkflow. Start with `dry-run: true`. Once the repository has proven that its\ntwo workflow names and branch topology produce exact same-SHA evidence, it may\nset `create-pull-request: true` and `dry-run: false`.\n\nCandidate mode creates one branch named from the target branch and the first 12\ncharacters of the full source SHA. An existing branch must point to the same\nfull SHA or the run fails. The controller then creates or reuses one open pull\nrequest from that immutable branch to the protected Alpha branch.\n\nThe workflow never moves the Alpha ref directly, merges or auto-merges the pull\nrequest, publishes npm, creates a Git tag or GitHub Release, or changes branch\nprotection. Those remain repository-owned protected settlement actions."
657
657
  },
658
658
  {
659
659
  "id": "manual:github-governance-authority",
@@ -21,7 +21,7 @@
21
21
  "contract": "kungfu-buildchain-public-surface-reverse-audit",
22
22
  "path": "dist/site/public-surface-audit.json",
23
23
  "status": "passed",
24
- "sha256": "23b05dbcaf2e80dd05bdc0c908f16ec927bbd6d1cfd3bc5c329fe972ae868195",
24
+ "sha256": "ef5ce46ab398bd64e8ddaf9916d6f107baea6ddd91f21eb904470f14ff4926dd",
25
25
  "summary": {
26
26
  "cliCommandCount": 96,
27
27
  "workflowCount": 54,
@@ -228,7 +228,7 @@
228
228
  "contract": "kungfu-buildchain-public-surface-reverse-audit",
229
229
  "path": "dist/site/public-surface-audit.json",
230
230
  "status": "passed",
231
- "sha256": "23b05dbcaf2e80dd05bdc0c908f16ec927bbd6d1cfd3bc5c329fe972ae868195",
231
+ "sha256": "ef5ce46ab398bd64e8ddaf9916d6f107baea6ddd91f21eb904470f14ff4926dd",
232
232
  "summary": {
233
233
  "cliCommandCount": 96,
234
234
  "workflowCount": 54,
@@ -4,7 +4,7 @@
4
4
  "product": {
5
5
  "id": "kungfu-tech-buildchain",
6
6
  "name": "@kungfu-tech/buildchain",
7
- "version": "3.0.2-alpha.3",
7
+ "version": "3.0.2-alpha.4",
8
8
  "repository": "https://github.com/kungfu-systems/buildchain"
9
9
  },
10
10
  "source": {
@@ -125,7 +125,7 @@
125
125
  "specifier": "@kungfu-tech/buildchain/channel-candidate",
126
126
  "export": "./channel-candidate",
127
127
  "target": "./packages/core/channel-candidate.js",
128
- "digest": "sha256:fe37b5319563d16d5f4f2009c2fc097fe41f0630429c5291fb3e18ae7e7b5ee5",
128
+ "digest": "sha256:4f667440dc1d3df1663fba13afe68358aa3fc0c9316d1393e760bd932cec8a45",
129
129
  "summary": "Exact-source channel candidate decisions, same-SHA workflow evidence validation, and deterministic source-lock reference APIs.",
130
130
  "capabilityGroup": "governance-versioning",
131
131
  "audience": [
@@ -574,7 +574,7 @@
574
574
  ],
575
575
  "maturity": "stable",
576
576
  "sourcePath": "docs/dev-alpha-candidate-patrol.md",
577
- "digest": "sha256:ebaec4d54f0f66fd82ec24b34e227b20f90a4a76cf724cf94d9e912422ee89a2",
577
+ "digest": "sha256:ffd96da21ed6a99e01d60b6545ca4a486d6c22dbf053f717a3a2cce5c3970a28",
578
578
  "headings": [
579
579
  {
580
580
  "level": 1,
@@ -587,7 +587,7 @@
587
587
  "anchor": "reusable-workflow"
588
588
  }
589
589
  ],
590
- "markdown": "---\nstatus: preview\nperiod: ongoing\ntheme: dev-alpha-candidate-patrol\ndoc_type: architecture-and-usage\nsource_level: local-files\nconfidence: high\nsensitivity: public\nevidence_grade: A\nreview_state: self-reviewed\nlast_reviewed: 2026-07-26\nai_provenance:\n model_family: GPT-5\n product: Codex\n generated_at: 2026-07-26\n visible_context: Existing Buildchain stable-candidate source locks, Kungfu exact-source Alpha preflight, Dev Patrol, and repository release governance.\n invisible_context_boundary: No credentials, private logs, or private configuration were used.\n---\n\n# Dev to Alpha Candidate Patrol\n\nBuildchain provides a reusable observation and PR controller for repositories\nthat promote a development branch into a protected Alpha branch. It does not\npublish Alpha. The controller reads the exact heads of both branches and accepts\nthe development head only when:\n\n- the source is strictly ahead of the recorded target head;\n- the latest completed Dev Patrol for that exact source SHA succeeded;\n- the latest completed Alpha preflight for the same SHA succeeded; and\n- both runs are within the caller's evidence age limit.\n\nThe decision is `kungfu-buildchain-channel-candidate-decision/v1`. It records the\nsource and target branches and SHAs, comparison distance, workflow paths, run\nidentities and attempts, completion times, URLs, policy, and a canonical decision\nroot. Missing, stale, failed, duplicate, or source-mismatched evidence fails\nclosed.\n\n## Reusable workflow\n\nCall `.github/workflows/dev-alpha-candidate-patrol.yml` from a thin repository\nworkflow. Start with `dry-run: true`. Once the repository has proven that its\ntwo workflow names and branch topology produce exact same-SHA evidence, it may\nset `create-pull-request: true` and `dry-run: false`.\n\nCandidate mode creates one branch named from the target branch and the first 12\ncharacters of the full source SHA. An existing branch must point to the same\nfull SHA or the run fails. The controller then creates or reuses one open pull\nrequest from that immutable branch to the protected Alpha branch.\n\nThe workflow never moves the Alpha ref directly, merges or auto-merges the pull\nrequest, publishes npm, creates a Git tag or GitHub Release, or changes branch\nprotection. Those remain repository-owned protected settlement actions."
590
+ "markdown": "---\nstatus: preview\nperiod: ongoing\ntheme: dev-alpha-candidate-patrol\ndoc_type: architecture-and-usage\nsource_level: local-files\nconfidence: high\nsensitivity: public\nevidence_grade: A\nreview_state: self-reviewed\nlast_reviewed: 2026-07-26\nai_provenance:\n model_family: GPT-5\n product: Codex\n generated_at: 2026-07-26\n visible_context: Existing Buildchain stable-candidate source locks, Kungfu exact-source Alpha preflight, Dev Patrol, and repository release governance.\n invisible_context_boundary: No credentials, private logs, or private configuration were used.\n---\n\n# Dev to Alpha Candidate Patrol\n\nBuildchain provides a reusable observation and PR controller for repositories\nthat promote a development branch into a protected Alpha branch. It does not\npublish Alpha. The controller reads the exact heads of both branches, walks the\nbounded development history from newest to oldest (stopping early at the Alpha\nhead), and selects the newest commit that satisfies all of these conditions:\n\n- the source is strictly ahead of the recorded target head;\n- the latest completed Dev Patrol for that exact commit SHA succeeded;\n- the latest completed Alpha preflight for the same commit SHA succeeded; and\n- both runs are within the caller's evidence age limit.\n\nThe selected commit can be behind the observed development head when newer\ncommits have not completed both workflows yet. The decision binds the observed\nhead, selected SHA, and count of skipped newer commits. This makes a slow native\nverification lane live under continuous development without silently treating\nan unqualified head as releasable.\n\nHistory discovery is bounded to the newest 1000 development commits. The\ncontroller then compares the selected SHA to the exact Alpha head before it can\nbe eligible, so a bounded scan cannot turn a commit outside the promotion\nancestry into a candidate.\n\nThe decision is `kungfu-buildchain-channel-candidate-decision/v1`. It records the\nsource and target branches and SHAs, comparison distance, workflow paths, run\nidentities and attempts, completion times, URLs, policy, and a canonical decision\nroot. Missing, stale, failed, duplicate, or source-mismatched evidence fails\nclosed.\n\n## Reusable workflow\n\nCall `.github/workflows/dev-alpha-candidate-patrol.yml` from a thin repository\nworkflow. Start with `dry-run: true`. Once the repository has proven that its\ntwo workflow names and branch topology produce exact same-SHA evidence, it may\nset `create-pull-request: true` and `dry-run: false`.\n\nCandidate mode creates one branch named from the target branch and the first 12\ncharacters of the full source SHA. An existing branch must point to the same\nfull SHA or the run fails. The controller then creates or reuses one open pull\nrequest from that immutable branch to the protected Alpha branch.\n\nThe workflow never moves the Alpha ref directly, merges or auto-merges the pull\nrequest, publishes npm, creates a Git tag or GitHub Release, or changes branch\nprotection. Those remain repository-owned protected settlement actions."
591
591
  },
592
592
  {
593
593
  "id": "manual:github-governance-authority",
@@ -4118,7 +4118,7 @@
4118
4118
  "pageRegistryPath": "dist/site/page-registry.json",
4119
4119
  "cliRegistryDigest": "a1c10dd6977d85a7bc793054baa549108b46d6d18098de109733e01b9151a024",
4120
4120
  "workflowRegistryDigest": "adbf4d0f0f67aab550735fac44b544cb5eeeaa94d1604c18953d7e81d3cb0136",
4121
- "pageRegistryDigest": "b8b393b19ef85b18e389837a8d956f5a93050ea2e700de0a02961f6899bfef90"
4121
+ "pageRegistryDigest": "d0af91a01b205954540fe4388cc08dcfaa9182f4eb6cfb162bb0c987fc33eef2"
4122
4122
  },
4123
4123
  "comparison": {
4124
4124
  "missingCliRegistry": [],
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "contract": "kungfu-buildchain-publication-release-registry",
4
- "generatedAt": "2026-07-27T01:10:48.385Z",
5
- "publishedAt": "2026-07-27T01:10:48.385Z",
4
+ "generatedAt": "2026-07-27T05:08:19.148Z",
5
+ "publishedAt": "2026-07-27T05:08:19.148Z",
6
6
  "reproducible": true,
7
7
  "timestampPolicy": "ci-injected",
8
8
  "deterministicInputs": [
@@ -19,7 +19,7 @@
19
19
  "declared Buildchain surface manifest contract"
20
20
  ],
21
21
  "sourceDateEpoch": "0",
22
- "sourceRevision": "8b1b52cc9dc5eb0a89f2aae3acfd08a22dae6934",
22
+ "sourceRevision": "ba9a63a1960beab3660cacaca76ceed02ea9a0f1",
23
23
  "timestampPolicyDetails": {
24
24
  "contract": "kungfu-buildchain-surface-timestamp-policy",
25
25
  "timestampFields": [
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "package": {
34
34
  "name": "@kungfu-tech/buildchain",
35
- "version": "3.0.2-alpha.3",
35
+ "version": "3.0.2-alpha.4",
36
36
  "versionSource": "package.json#version"
37
37
  },
38
38
  "sourceKind": "package-site-bundle",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "contract": "kungfu-buildchain-site-manifest",
4
- "generatedAt": "2026-07-27T01:10:48.385Z",
5
- "publishedAt": "2026-07-27T01:10:48.385Z",
4
+ "generatedAt": "2026-07-27T05:08:19.148Z",
5
+ "publishedAt": "2026-07-27T05:08:19.148Z",
6
6
  "reproducible": true,
7
7
  "timestampPolicy": "ci-injected",
8
8
  "deterministicInputs": [
@@ -19,7 +19,7 @@
19
19
  "declared Buildchain surface manifest contract"
20
20
  ],
21
21
  "sourceDateEpoch": "0",
22
- "sourceRevision": "8b1b52cc9dc5eb0a89f2aae3acfd08a22dae6934",
22
+ "sourceRevision": "ba9a63a1960beab3660cacaca76ceed02ea9a0f1",
23
23
  "timestampPolicyDetails": {
24
24
  "contract": "kungfu-buildchain-surface-timestamp-policy",
25
25
  "timestampFields": [
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "package": {
39
39
  "name": "@kungfu-tech/buildchain",
40
- "version": "3.0.2-alpha.3",
40
+ "version": "3.0.2-alpha.4",
41
41
  "versionSource": "package.json#version"
42
42
  },
43
43
  "entrypoint": "buildchain-site.json",
@@ -21,14 +21,26 @@ ai_provenance:
21
21
 
22
22
  Buildchain provides a reusable observation and PR controller for repositories
23
23
  that promote a development branch into a protected Alpha branch. It does not
24
- publish Alpha. The controller reads the exact heads of both branches and accepts
25
- the development head only when:
24
+ publish Alpha. The controller reads the exact heads of both branches, walks the
25
+ bounded development history from newest to oldest (stopping early at the Alpha
26
+ head), and selects the newest commit that satisfies all of these conditions:
26
27
 
27
28
  - the source is strictly ahead of the recorded target head;
28
- - the latest completed Dev Patrol for that exact source SHA succeeded;
29
- - the latest completed Alpha preflight for the same SHA succeeded; and
29
+ - the latest completed Dev Patrol for that exact commit SHA succeeded;
30
+ - the latest completed Alpha preflight for the same commit SHA succeeded; and
30
31
  - both runs are within the caller's evidence age limit.
31
32
 
33
+ The selected commit can be behind the observed development head when newer
34
+ commits have not completed both workflows yet. The decision binds the observed
35
+ head, selected SHA, and count of skipped newer commits. This makes a slow native
36
+ verification lane live under continuous development without silently treating
37
+ an unqualified head as releasable.
38
+
39
+ History discovery is bounded to the newest 1000 development commits. The
40
+ controller then compares the selected SHA to the exact Alpha head before it can
41
+ be eligible, so a bounded scan cannot turn a commit outside the promotion
42
+ ancestry into a candidate.
43
+
32
44
  The decision is `kungfu-buildchain-channel-candidate-decision/v1`. It records the
33
45
  source and target branches and SHAs, comparison distance, workflow paths, run
34
46
  identities and attempts, completion times, URLs, policy, and a canonical decision
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "3.0.2-alpha.3",
3
+ "version": "3.0.2-alpha.4",
4
4
  "private": false,
5
5
  "description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
6
6
  "repository": "https://github.com/kungfu-systems/buildchain",
@@ -34,28 +34,40 @@ function required(value, name) {
34
34
 
35
35
  function sha(value, name) {
36
36
  const normalized = required(value, name).toLowerCase();
37
- if (!SHA.test(normalized)) throw new Error(`${name} must be an exact 40-character SHA`);
37
+ if (!SHA.test(normalized))
38
+ throw new Error(`${name} must be an exact 40-character SHA`);
38
39
  return normalized;
39
40
  }
40
41
 
41
42
  function qualifyWorkflow(row, { sourceSha, now, maxAgeSeconds }) {
42
- if (!row || typeof row !== "object") throw new Error("workflow evidence is required");
43
+ if (!row || typeof row !== "object")
44
+ throw new Error("workflow evidence is required");
43
45
  const workflowPath = required(row.workflowPath, "workflowPath");
44
46
  if (sha(row.headSha, `${workflowPath} headSha`) !== sourceSha) {
45
- throw new Error(`${workflowPath} evidence does not bind source SHA ${sourceSha}`);
47
+ throw new Error(
48
+ `${workflowPath} evidence does not bind source SHA ${sourceSha}`,
49
+ );
46
50
  }
47
51
  if (row.status !== "completed" || row.conclusion !== "success") {
48
52
  throw new Error(`${workflowPath} is not a completed successful run`);
49
53
  }
50
54
  const completedAt = required(row.completedAt, `${workflowPath} completedAt`);
51
55
  const ageSeconds = (Date.parse(now) - Date.parse(completedAt)) / 1000;
52
- if (!Number.isFinite(ageSeconds) || ageSeconds < 0 || ageSeconds > maxAgeSeconds) {
53
- throw new Error(`${workflowPath} evidence is stale or has an invalid completion time`);
56
+ if (
57
+ !Number.isFinite(ageSeconds) ||
58
+ ageSeconds < 0 ||
59
+ ageSeconds > maxAgeSeconds
60
+ ) {
61
+ throw new Error(
62
+ `${workflowPath} evidence is stale or has an invalid completion time`,
63
+ );
54
64
  }
55
65
  const runId = Number(row.runId);
56
66
  const runAttempt = Number(row.runAttempt || 1);
57
- if (!Number.isSafeInteger(runId) || runId <= 0) throw new Error(`${workflowPath} runId is invalid`);
58
- if (!Number.isSafeInteger(runAttempt) || runAttempt <= 0) throw new Error(`${workflowPath} runAttempt is invalid`);
67
+ if (!Number.isSafeInteger(runId) || runId <= 0)
68
+ throw new Error(`${workflowPath} runId is invalid`);
69
+ if (!Number.isSafeInteger(runAttempt) || runAttempt <= 0)
70
+ throw new Error(`${workflowPath} runAttempt is invalid`);
59
71
  return {
60
72
  workflowPath,
61
73
  workflowName: required(row.workflowName, `${workflowPath} workflowName`),
@@ -79,9 +91,31 @@ export function decideChannelCandidate(input) {
79
91
  const repository = required(input.repository, "repository");
80
92
  const sourceBranch = required(input.sourceBranch, "sourceBranch");
81
93
  const targetBranch = required(input.targetBranch, "targetBranch");
82
- if (sourceBranch === targetBranch) throw new Error("sourceBranch and targetBranch must differ");
94
+ if (sourceBranch === targetBranch)
95
+ throw new Error("sourceBranch and targetBranch must differ");
83
96
  const sourceSha = sha(input.sourceSha, "sourceSha");
84
97
  const targetSha = sha(input.targetSha, "targetSha");
98
+ const selection = input.selection
99
+ ? {
100
+ mode: required(input.selection.mode, "selection.mode"),
101
+ observedSourceHeadSha: sha(
102
+ input.selection.observedSourceHeadSha,
103
+ "selection.observedSourceHeadSha",
104
+ ),
105
+ skippedNewerCommitCount: Number(
106
+ input.selection.skippedNewerCommitCount || 0,
107
+ ),
108
+ }
109
+ : undefined;
110
+ if (
111
+ selection &&
112
+ (!Number.isSafeInteger(selection.skippedNewerCommitCount) ||
113
+ selection.skippedNewerCommitCount < 0)
114
+ ) {
115
+ throw new Error(
116
+ "selection.skippedNewerCommitCount must be a non-negative integer",
117
+ );
118
+ }
85
119
  const now = required(input.now || new Date().toISOString(), "now");
86
120
  const maxAgeSeconds = Number(input.maxAgeSeconds ?? 7 * 24 * 60 * 60);
87
121
  if (!Number.isSafeInteger(maxAgeSeconds) || maxAgeSeconds <= 0) {
@@ -89,7 +123,10 @@ export function decideChannelCandidate(input) {
89
123
  }
90
124
  const comparison = input.comparison || {};
91
125
  if (comparison.status !== "ahead" || Number(comparison.aheadBy) <= 0) {
92
- const reason = comparison.status === "identical" ? "target-already-current" : "source-does-not-lead-target";
126
+ const reason =
127
+ comparison.status === "identical"
128
+ ? "target-already-current"
129
+ : "source-does-not-lead-target";
93
130
  return {
94
131
  schema: CHANNEL_CANDIDATE_DECISION_SCHEMA,
95
132
  eligible: false,
@@ -97,15 +134,25 @@ export function decideChannelCandidate(input) {
97
134
  repository,
98
135
  source: { branch: sourceBranch, sha: sourceSha },
99
136
  target: { branch: targetBranch, sha: targetSha },
100
- comparison: { status: String(comparison.status || "unknown"), aheadBy: Number(comparison.aheadBy || 0) },
137
+ comparison: {
138
+ status: String(comparison.status || "unknown"),
139
+ aheadBy: Number(comparison.aheadBy || 0),
140
+ },
101
141
  decidedAt: now,
102
142
  };
103
143
  }
104
- const rows = Array.isArray(input.workflowEvidence) ? input.workflowEvidence : [];
105
- const expectedPaths = [...new Set((input.requiredWorkflowPaths || []).map(String))];
106
- if (expectedPaths.length === 0) throw new Error("requiredWorkflowPaths must not be empty");
144
+ const rows = Array.isArray(input.workflowEvidence)
145
+ ? input.workflowEvidence
146
+ : [];
147
+ const expectedPaths = [
148
+ ...new Set((input.requiredWorkflowPaths || []).map(String)),
149
+ ];
150
+ if (expectedPaths.length === 0)
151
+ throw new Error("requiredWorkflowPaths must not be empty");
107
152
  if (rows.length !== expectedPaths.length) {
108
- throw new Error(`expected exactly ${expectedPaths.length} workflow evidence rows, got ${rows.length}`);
153
+ throw new Error(
154
+ `expected exactly ${expectedPaths.length} workflow evidence rows, got ${rows.length}`,
155
+ );
109
156
  }
110
157
  const byPath = new Map();
111
158
  for (const row of rows) {
@@ -116,7 +163,8 @@ export function decideChannelCandidate(input) {
116
163
  byPath.set(qualified.workflowPath, qualified);
117
164
  }
118
165
  for (const workflowPath of expectedPaths) {
119
- if (!byPath.has(workflowPath)) throw new Error(`missing workflow evidence: ${workflowPath}`);
166
+ if (!byPath.has(workflowPath))
167
+ throw new Error(`missing workflow evidence: ${workflowPath}`);
120
168
  }
121
169
  const body = {
122
170
  schema: CHANNEL_CANDIDATE_DECISION_SCHEMA,
@@ -126,8 +174,11 @@ export function decideChannelCandidate(input) {
126
174
  source: { branch: sourceBranch, sha: sourceSha },
127
175
  target: { branch: targetBranch, sha: targetSha },
128
176
  comparison: { status: "ahead", aheadBy: Number(comparison.aheadBy) },
177
+ ...(selection ? { selection } : {}),
129
178
  sourceLockRef: channelCandidateSourceLockRef(targetBranch, sourceSha),
130
- workflowEvidence: expectedPaths.map((workflowPath) => byPath.get(workflowPath)),
179
+ workflowEvidence: expectedPaths.map((workflowPath) =>
180
+ byPath.get(workflowPath),
181
+ ),
131
182
  policy: { maxAgeSeconds, requiredWorkflowPaths: expectedPaths },
132
183
  decidedAt: now,
133
184
  };
@@ -16,13 +16,18 @@ function bool(value, fallback = false) {
16
16
 
17
17
  function repository(value) {
18
18
  const normalized = text(value);
19
- if (!/^[^/\s]+\/[^/\s]+$/.test(normalized)) throw new Error(`repository must be owner/repo, got ${value || "<empty>"}`);
19
+ if (!/^[^/\s]+\/[^/\s]+$/.test(normalized))
20
+ throw new Error(`repository must be owner/repo, got ${value || "<empty>"}`);
20
21
  return normalized;
21
22
  }
22
23
 
23
24
  function branch(value, name) {
24
25
  const normalized = text(value).replace(/^refs\/heads\//, "");
25
- if (!normalized || normalized.startsWith("-") || /[\s~^:?*[\\]/.test(normalized)) {
26
+ if (
27
+ !normalized ||
28
+ normalized.startsWith("-") ||
29
+ /[\s~^:?*[\\]/.test(normalized)
30
+ ) {
26
31
  throw new Error(`${name} is not a valid branch name`);
27
32
  }
28
33
  return normalized;
@@ -43,24 +48,69 @@ function integer(value, fallback) {
43
48
 
44
49
  export function normalizeDevAlphaPatrolOptions(options = {}) {
45
50
  return {
46
- repository: repository(options.repository ?? process.env.BUILDCHAIN_CHANNEL_PATROL_REPOSITORY ?? process.env.GITHUB_REPOSITORY),
47
- sourceBranch: branch(options.sourceBranch ?? process.env.BUILDCHAIN_CHANNEL_PATROL_SOURCE_BRANCH ?? "dev/v4/v4.0", "sourceBranch"),
48
- targetBranch: branch(options.targetBranch ?? process.env.BUILDCHAIN_CHANNEL_PATROL_TARGET_BRANCH ?? "alpha/v4/v4.0", "targetBranch"),
49
- devWorkflowPath: workflowPath(options.devWorkflowPath ?? process.env.BUILDCHAIN_CHANNEL_PATROL_DEV_WORKFLOW ?? ".github/workflows/dev-verify-patrol.yml", "devWorkflowPath"),
50
- alphaWorkflowPath: workflowPath(options.alphaWorkflowPath ?? process.env.BUILDCHAIN_CHANNEL_PATROL_ALPHA_WORKFLOW ?? ".github/workflows/alpha-promotion-preflight.yml", "alphaWorkflowPath"),
51
- maxAgeSeconds: integer(options.maxAgeSeconds ?? process.env.BUILDCHAIN_CHANNEL_PATROL_MAX_AGE_SECONDS, 7 * 24 * 60 * 60),
52
- createPullRequest: bool(options.createPullRequest ?? process.env.BUILDCHAIN_CHANNEL_PATROL_CREATE_PR, false),
53
- dryRun: bool(options.dryRun ?? process.env.BUILDCHAIN_CHANNEL_PATROL_DRY_RUN, true),
54
- now: text(options.now ?? process.env.BUILDCHAIN_CHANNEL_PATROL_NOW) || new Date().toISOString(),
55
- outputPath: text(options.outputPath ?? process.env.BUILDCHAIN_CHANNEL_PATROL_OUTPUT_PATH) || ".buildchain/patrol/dev-alpha-candidate.json",
51
+ repository: repository(
52
+ options.repository ??
53
+ process.env.BUILDCHAIN_CHANNEL_PATROL_REPOSITORY ??
54
+ process.env.GITHUB_REPOSITORY,
55
+ ),
56
+ sourceBranch: branch(
57
+ options.sourceBranch ??
58
+ process.env.BUILDCHAIN_CHANNEL_PATROL_SOURCE_BRANCH ??
59
+ "dev/v4/v4.0",
60
+ "sourceBranch",
61
+ ),
62
+ targetBranch: branch(
63
+ options.targetBranch ??
64
+ process.env.BUILDCHAIN_CHANNEL_PATROL_TARGET_BRANCH ??
65
+ "alpha/v4/v4.0",
66
+ "targetBranch",
67
+ ),
68
+ devWorkflowPath: workflowPath(
69
+ options.devWorkflowPath ??
70
+ process.env.BUILDCHAIN_CHANNEL_PATROL_DEV_WORKFLOW ??
71
+ ".github/workflows/dev-verify-patrol.yml",
72
+ "devWorkflowPath",
73
+ ),
74
+ alphaWorkflowPath: workflowPath(
75
+ options.alphaWorkflowPath ??
76
+ process.env.BUILDCHAIN_CHANNEL_PATROL_ALPHA_WORKFLOW ??
77
+ ".github/workflows/alpha-promotion-preflight.yml",
78
+ "alphaWorkflowPath",
79
+ ),
80
+ maxAgeSeconds: integer(
81
+ options.maxAgeSeconds ??
82
+ process.env.BUILDCHAIN_CHANNEL_PATROL_MAX_AGE_SECONDS,
83
+ 7 * 24 * 60 * 60,
84
+ ),
85
+ createPullRequest: bool(
86
+ options.createPullRequest ??
87
+ process.env.BUILDCHAIN_CHANNEL_PATROL_CREATE_PR,
88
+ false,
89
+ ),
90
+ dryRun: bool(
91
+ options.dryRun ?? process.env.BUILDCHAIN_CHANNEL_PATROL_DRY_RUN,
92
+ true,
93
+ ),
94
+ now:
95
+ text(options.now ?? process.env.BUILDCHAIN_CHANNEL_PATROL_NOW) ||
96
+ new Date().toISOString(),
97
+ outputPath:
98
+ text(
99
+ options.outputPath ?? process.env.BUILDCHAIN_CHANNEL_PATROL_OUTPUT_PATH,
100
+ ) || ".buildchain/patrol/dev-alpha-candidate.json",
56
101
  };
57
102
  }
58
103
 
59
104
  function latestWorkflowEvidence(runs, workflowPathValue, sourceSha) {
60
105
  const matching = runs
61
- .filter((run) => run.path === workflowPathValue && run.head_sha === sourceSha)
106
+ .filter(
107
+ (run) => run.path === workflowPathValue && run.head_sha === sourceSha,
108
+ )
62
109
  .sort((left, right) => Number(right.id) - Number(left.id));
63
- if (matching.length === 0) throw new Error(`missing completed same-SHA workflow run: ${workflowPathValue}`);
110
+ if (matching.length === 0)
111
+ throw new Error(
112
+ `missing completed same-SHA workflow run: ${workflowPathValue}`,
113
+ );
64
114
  const run = matching[0];
65
115
  return {
66
116
  workflowPath: workflowPathValue,
@@ -75,25 +125,122 @@ function latestWorkflowEvidence(runs, workflowPathValue, sourceSha) {
75
125
  };
76
126
  }
77
127
 
78
- export async function runDevAlphaCandidatePatrol(optionsInput = {}, clientInput) {
128
+ function workflowEvidenceIsFreshAndSuccessful(run, { now, maxAgeSeconds }) {
129
+ const completedAt = Date.parse(run.updated_at);
130
+ const ageSeconds = (Date.parse(now) - completedAt) / 1000;
131
+ return (
132
+ run.status === "completed" &&
133
+ run.conclusion === "success" &&
134
+ Number.isFinite(ageSeconds) &&
135
+ ageSeconds >= 0 &&
136
+ ageSeconds <= maxAgeSeconds
137
+ );
138
+ }
139
+
140
+ function latestRunsBySha(runs, workflowPathValue) {
141
+ const latest = new Map();
142
+ for (const run of runs.filter((row) => row.path === workflowPathValue)) {
143
+ const current = latest.get(run.head_sha);
144
+ if (!current || Number(run.id) > Number(current.id))
145
+ latest.set(run.head_sha, run);
146
+ }
147
+ return latest;
148
+ }
149
+
150
+ export function selectLatestQualifiedSource({
151
+ sourceHistory,
152
+ workflowRunsByPath,
153
+ requiredWorkflowPaths,
154
+ now,
155
+ maxAgeSeconds,
156
+ }) {
157
+ const latestByPath = new Map(
158
+ requiredWorkflowPaths.map((workflow) => [
159
+ workflow,
160
+ latestRunsBySha(workflowRunsByPath.get(workflow) || [], workflow),
161
+ ]),
162
+ );
163
+ for (let index = 0; index < sourceHistory.length; index += 1) {
164
+ const sourceSha = sourceHistory[index];
165
+ const rows = requiredWorkflowPaths.map((workflow) =>
166
+ latestByPath.get(workflow).get(sourceSha),
167
+ );
168
+ if (
169
+ rows.every((run) =>
170
+ workflowEvidenceIsFreshAndSuccessful(run || {}, { now, maxAgeSeconds }),
171
+ )
172
+ ) {
173
+ return {
174
+ sourceSha,
175
+ skippedNewerCommitCount: index,
176
+ workflowEvidence: requiredWorkflowPaths.map((workflow) =>
177
+ latestWorkflowEvidence(
178
+ workflowRunsByPath.get(workflow) || [],
179
+ workflow,
180
+ sourceSha,
181
+ ),
182
+ ),
183
+ };
184
+ }
185
+ }
186
+ throw new Error(
187
+ "no source commit ahead of target has fresh completed successful same-SHA workflow evidence",
188
+ );
189
+ }
190
+
191
+ export async function runDevAlphaCandidatePatrol(
192
+ optionsInput = {},
193
+ clientInput,
194
+ ) {
79
195
  const options = normalizeDevAlphaPatrolOptions(optionsInput);
80
- if (options.sourceBranch === options.targetBranch) throw new Error("source and target branches must differ");
81
- const client = clientInput || createGitHubChannelCandidateClient({
82
- repository: options.repository,
83
- token: process.env.GITHUB_TOKEN,
84
- });
85
- const [sourceSha, targetSha] = await Promise.all([
196
+ if (options.sourceBranch === options.targetBranch)
197
+ throw new Error("source and target branches must differ");
198
+ const client =
199
+ clientInput ||
200
+ createGitHubChannelCandidateClient({
201
+ repository: options.repository,
202
+ token: process.env.GITHUB_TOKEN,
203
+ });
204
+ const [observedSourceHeadSha, targetSha] = await Promise.all([
86
205
  client.resolveBranch(options.sourceBranch),
87
206
  client.resolveBranch(options.targetBranch),
88
207
  ]);
89
- const comparison = await client.compare(targetSha, sourceSha);
90
- const requiredWorkflowPaths = [options.devWorkflowPath, options.alphaWorkflowPath];
208
+ const headComparison = await client.compare(targetSha, observedSourceHeadSha);
209
+ const requiredWorkflowPaths = [
210
+ options.devWorkflowPath,
211
+ options.alphaWorkflowPath,
212
+ ];
213
+ let sourceSha = observedSourceHeadSha;
214
+ let comparison = headComparison;
91
215
  let workflowEvidence = [];
92
- if (comparison.status === "ahead" && Number(comparison.ahead_by) > 0) {
93
- const runs = await client.listCompletedRuns(sourceSha);
94
- workflowEvidence = requiredWorkflowPaths.map((workflow) =>
95
- latestWorkflowEvidence(runs, workflow, sourceSha),
96
- );
216
+ let skippedNewerCommitCount = 0;
217
+ if (
218
+ headComparison.status === "ahead" &&
219
+ Number(headComparison.ahead_by) > 0
220
+ ) {
221
+ const [sourceHistory, ...workflowRunSets] = await Promise.all([
222
+ client.listBranchHistory(options.sourceBranch, targetSha),
223
+ ...requiredWorkflowPaths.map((workflow) =>
224
+ client.listCompletedWorkflowRuns(workflow, options.sourceBranch),
225
+ ),
226
+ ]);
227
+ const selected = selectLatestQualifiedSource({
228
+ sourceHistory,
229
+ workflowRunsByPath: new Map(
230
+ requiredWorkflowPaths.map((workflow, index) => [
231
+ workflow,
232
+ workflowRunSets[index],
233
+ ]),
234
+ ),
235
+ requiredWorkflowPaths,
236
+ now: options.now,
237
+ maxAgeSeconds: options.maxAgeSeconds,
238
+ });
239
+ sourceSha = selected.sourceSha;
240
+ skippedNewerCommitCount = selected.skippedNewerCommitCount;
241
+ workflowEvidence = selected.workflowEvidence;
242
+ if (sourceSha !== observedSourceHeadSha)
243
+ comparison = await client.compare(targetSha, sourceSha);
97
244
  }
98
245
  const decision = decideChannelCandidate({
99
246
  repository: options.repository,
@@ -102,6 +249,11 @@ export async function runDevAlphaCandidatePatrol(optionsInput = {}, clientInput)
102
249
  sourceSha,
103
250
  targetSha,
104
251
  comparison: { status: comparison.status, aheadBy: comparison.ahead_by },
252
+ selection: {
253
+ mode: "latest-qualified-source-ancestor",
254
+ observedSourceHeadSha,
255
+ skippedNewerCommitCount,
256
+ },
105
257
  workflowEvidence,
106
258
  requiredWorkflowPaths,
107
259
  maxAgeSeconds: options.maxAgeSeconds,
@@ -118,11 +270,14 @@ export async function runDevAlphaCandidatePatrol(optionsInput = {}, clientInput)
118
270
  "Buildchain exact-source channel candidate.",
119
271
  "",
120
272
  `- Source branch: \`${options.sourceBranch}\``,
273
+ `- Observed source HEAD: \`${observedSourceHeadSha}\``,
121
274
  `- Source SHA: \`${sourceSha}\``,
275
+ `- Skipped newer unqualified commits: \`${skippedNewerCommitCount}\``,
122
276
  `- Target branch/head: \`${options.targetBranch}\` / \`${targetSha}\``,
123
277
  `- Decision root: \`${decision.decisionRoot}\``,
124
278
  ...decision.workflowEvidence.map(
125
- (row) => `- ${row.workflowName}: [run ${row.runId} attempt ${row.runAttempt}](${row.url})`,
279
+ (row) =>
280
+ `- ${row.workflowName}: [run ${row.runId} attempt ${row.runAttempt}](${row.url})`,
126
281
  ),
127
282
  "",
128
283
  "The source-lock branch must continue to point at the exact source SHA. This patrol never merges the PR, publishes a package, creates a tag, or creates a release.",
@@ -142,7 +297,11 @@ function encodeRef(value) {
142
297
  return value.split("/").map(encodeURIComponent).join("/");
143
298
  }
144
299
 
145
- export function createGitHubChannelCandidateClient({ repository: repositoryInput, token, fetchImpl = globalThis.fetch }) {
300
+ export function createGitHubChannelCandidateClient({
301
+ repository: repositoryInput,
302
+ token,
303
+ fetchImpl = globalThis.fetch,
304
+ }) {
146
305
  const [owner, repo] = repository(repositoryInput).split("/");
147
306
  const headers = {
148
307
  accept: "application/vnd.github+json",
@@ -150,34 +309,74 @@ export function createGitHubChannelCandidateClient({ repository: repositoryInput
150
309
  "user-agent": "buildchain-dev-alpha-candidate-patrol",
151
310
  "x-github-api-version": "2022-11-28",
152
311
  };
153
- async function api(requestPath, { method = "GET", body, allow404 = false } = {}) {
312
+ async function api(
313
+ requestPath,
314
+ { method = "GET", body, allow404 = false } = {},
315
+ ) {
154
316
  const response = await fetchImpl(`https://api.github.com${requestPath}`, {
155
317
  method,
156
- headers: Object.fromEntries(Object.entries(headers).filter(([, value]) => value)),
318
+ headers: Object.fromEntries(
319
+ Object.entries(headers).filter(([, value]) => value),
320
+ ),
157
321
  body: body === undefined ? undefined : JSON.stringify(body),
158
322
  });
159
323
  const raw = await response.text();
160
324
  const payload = raw ? JSON.parse(raw) : undefined;
161
325
  if (allow404 && response.status === 404) return undefined;
162
- if (!response.ok) throw new Error(`GitHub API ${method} ${requestPath} failed with ${response.status}: ${payload?.message || raw}`);
326
+ if (!response.ok)
327
+ throw new Error(
328
+ `GitHub API ${method} ${requestPath} failed with ${response.status}: ${payload?.message || raw}`,
329
+ );
163
330
  return payload;
164
331
  }
165
332
  return {
166
333
  async resolveBranch(ref) {
167
- const payload = await api(`/repos/${owner}/${repo}/git/ref/heads/${encodeRef(ref)}`);
334
+ const payload = await api(
335
+ `/repos/${owner}/${repo}/git/ref/heads/${encodeRef(ref)}`,
336
+ );
168
337
  return text(payload.object?.sha);
169
338
  },
170
339
  async compare(baseSha, headSha) {
171
340
  return api(`/repos/${owner}/${repo}/compare/${baseSha}...${headSha}`);
172
341
  },
173
- async listCompletedRuns(headSha) {
174
- const payload = await api(`/repos/${owner}/${repo}/actions/runs?head_sha=${encodeURIComponent(headSha)}&status=completed&per_page=100`);
175
- return payload.workflow_runs || [];
342
+ async listCompletedWorkflowRuns(workflowPathValue, sourceBranch) {
343
+ const runs = [];
344
+ for (let page = 1; page <= 10; page += 1) {
345
+ const payload = await api(
346
+ `/repos/${owner}/${repo}/actions/workflows/${encodeURIComponent(workflowPathValue)}/runs?branch=${encodeURIComponent(sourceBranch)}&status=completed&per_page=100&page=${page}`,
347
+ );
348
+ const rows = payload.workflow_runs || [];
349
+ runs.push(...rows);
350
+ if (rows.length < 100) return runs;
351
+ }
352
+ throw new Error(
353
+ `${workflowPathValue} completed workflow history exceeds 1000 runs`,
354
+ );
355
+ },
356
+ async listBranchHistory(sourceBranch, targetSha) {
357
+ const commits = [];
358
+ for (let page = 1; page <= 10; page += 1) {
359
+ const rows = await api(
360
+ `/repos/${owner}/${repo}/commits?sha=${encodeURIComponent(sourceBranch)}&per_page=100&page=${page}`,
361
+ );
362
+ for (const commit of rows) {
363
+ const commitSha = text(commit.sha);
364
+ if (commitSha === targetSha) return commits;
365
+ commits.push(commitSha);
366
+ }
367
+ if (rows.length < 100) return commits;
368
+ }
369
+ return commits;
176
370
  },
177
371
  async ensureImmutableBranch(ref, sourceSha) {
178
- const current = await api(`/repos/${owner}/${repo}/git/ref/heads/${encodeRef(ref)}`, { allow404: true });
372
+ const current = await api(
373
+ `/repos/${owner}/${repo}/git/ref/heads/${encodeRef(ref)}`,
374
+ { allow404: true },
375
+ );
179
376
  if (current && current.object?.sha !== sourceSha) {
180
- throw new Error(`source-lock branch ${ref} points to ${current.object?.sha}, not ${sourceSha}`);
377
+ throw new Error(
378
+ `source-lock branch ${ref} points to ${current.object?.sha}, not ${sourceSha}`,
379
+ );
181
380
  }
182
381
  if (current) return current;
183
382
  return api(`/repos/${owner}/${repo}/git/refs`, {
@@ -186,11 +385,16 @@ export function createGitHubChannelCandidateClient({ repository: repositoryInput
186
385
  });
187
386
  },
188
387
  async ensurePullRequest({ head, base, title, body }) {
189
- const open = await api(`/repos/${owner}/${repo}/pulls?state=open&head=${encodeURIComponent(`${owner}:${head}`)}&base=${encodeURIComponent(base)}&per_page=20`);
190
- return open[0] || api(`/repos/${owner}/${repo}/pulls`, {
191
- method: "POST",
192
- body: { head, base, title, body },
193
- });
388
+ const open = await api(
389
+ `/repos/${owner}/${repo}/pulls?state=open&head=${encodeURIComponent(`${owner}:${head}`)}&base=${encodeURIComponent(base)}&per_page=20`,
390
+ );
391
+ return (
392
+ open[0] ||
393
+ api(`/repos/${owner}/${repo}/pulls`, {
394
+ method: "POST",
395
+ body: { head, base, title, body },
396
+ })
397
+ );
194
398
  },
195
399
  };
196
400
  }
@@ -214,7 +418,8 @@ async function main() {
214
418
  fs.mkdirSync(path.dirname(options.outputPath), { recursive: true });
215
419
  fs.writeFileSync(options.outputPath, `${JSON.stringify(result, null, 2)}\n`);
216
420
  const summary = markdown(result);
217
- if (process.env.GITHUB_STEP_SUMMARY) fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary);
421
+ if (process.env.GITHUB_STEP_SUMMARY)
422
+ fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary);
218
423
  else process.stdout.write(summary);
219
424
  if (process.env.GITHUB_OUTPUT) {
220
425
  const outputs = {
@@ -224,7 +429,12 @@ async function main() {
224
429
  "source-lock-ref": result.decision.sourceLockRef || "",
225
430
  "promotion-pr": result.pullRequest?.html_url || "",
226
431
  };
227
- fs.appendFileSync(process.env.GITHUB_OUTPUT, `${Object.entries(outputs).map(([key, value]) => `${key}=${value}`).join("\n")}\n`);
432
+ fs.appendFileSync(
433
+ process.env.GITHUB_OUTPUT,
434
+ `${Object.entries(outputs)
435
+ .map(([key, value]) => `${key}=${value}`)
436
+ .join("\n")}\n`,
437
+ );
228
438
  }
229
439
  }
230
440