@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.
- package/dist/site/buildchain-contract.json +4 -4
- package/dist/site/buildchain-site.json +6 -6
- package/dist/site/kfd-claims.json +2 -2
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/node-api-registry.json +1 -1
- package/dist/site/page-registry.json +2 -2
- package/dist/site/public-surface-audit.json +1 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +4 -4
- package/docs/dev-alpha-candidate-patrol.md +16 -4
- package/package.json +1 -1
- package/packages/core/channel-candidate.js +67 -16
- package/scripts/dev-alpha-candidate-patrol.mjs +256 -46
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"product": {
|
|
5
5
|
"name": "Buildchain",
|
|
6
6
|
"package": "@kungfu-tech/buildchain",
|
|
7
|
-
"version": "3.0.2-alpha.
|
|
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:
|
|
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:
|
|
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:
|
|
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-
|
|
5
|
-
"publishedAt": "2026-07-
|
|
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": "
|
|
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.
|
|
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:
|
|
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
|
|
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": "
|
|
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": "
|
|
231
|
+
"sha256": "ef5ce46ab398bd64e8ddaf9916d6f107baea6ddd91f21eb904470f14ff4926dd",
|
|
232
232
|
"summary": {
|
|
233
233
|
"cliCommandCount": 96,
|
|
234
234
|
"workflowCount": 54,
|
|
@@ -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:
|
|
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:
|
|
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
|
|
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": "
|
|
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-
|
|
5
|
-
"publishedAt": "2026-07-
|
|
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": "
|
|
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.
|
|
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-
|
|
5
|
-
"publishedAt": "2026-07-
|
|
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": "
|
|
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.
|
|
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
|
|
25
|
-
|
|
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
|
|
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
|
+
"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))
|
|
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")
|
|
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(
|
|
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 (
|
|
53
|
-
|
|
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)
|
|
58
|
-
|
|
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)
|
|
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 =
|
|
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: {
|
|
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)
|
|
105
|
-
|
|
106
|
-
|
|
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(
|
|
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))
|
|
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) =>
|
|
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))
|
|
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 (
|
|
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(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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(
|
|
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)
|
|
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
|
-
|
|
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)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
90
|
-
const requiredWorkflowPaths = [
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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) =>
|
|
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({
|
|
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(
|
|
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(
|
|
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)
|
|
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(
|
|
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
|
|
174
|
-
const
|
|
175
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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)
|
|
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(
|
|
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
|
|