@kungfu-tech/buildchain 3.0.2-alpha.5 → 3.0.2-alpha.7
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 +6 -6
- package/dist/site/buildchain-site.json +6 -6
- package/dist/site/kfd-claims.json +4 -3
- 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 +9 -4
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +4 -4
- package/dist/site/workflow-registry.json +7 -2
- package/docs/dev-alpha-candidate-patrol.md +51 -13
- package/package.json +1 -1
- package/packages/core/release-passport.js +9 -3
- package/scripts/dev-alpha-candidate-patrol.mjs +471 -71
- package/scripts/generate-channel-promotion-workflow.mjs +6 -0
|
@@ -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.7",
|
|
8
8
|
"repository": "https://github.com/kungfu-systems/buildchain"
|
|
9
9
|
},
|
|
10
10
|
"majorLine": "v3",
|
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
"GitHub Release passport and evidence publication is delegated to promote-buildchain-ref after the semver release transaction completes"
|
|
176
176
|
],
|
|
177
177
|
"breakingDigest": "sha256:acd401cfc46450115a3763fd4b679d85f185e8757ebd52510d6262e1533df4cf",
|
|
178
|
-
"auditDigest": "sha256:
|
|
178
|
+
"auditDigest": "sha256:68f3ad7a1d880a26be48263d9d1d12c38ff89beb08e4f744509b1ca196a889c9"
|
|
179
179
|
},
|
|
180
180
|
{
|
|
181
181
|
"contractVersion": 1,
|
|
@@ -498,7 +498,7 @@
|
|
|
498
498
|
"release-state SHA is recorded as a durable audit entrance"
|
|
499
499
|
],
|
|
500
500
|
"breakingDigest": "sha256:b627e1b2ece9b6049517a942c5139e342e6077cdd3d8962d61cef8481b1f70ad",
|
|
501
|
-
"auditDigest": "sha256:
|
|
501
|
+
"auditDigest": "sha256:aad90c2b39ce3fd7d2d4df5833f388c3090f5eeaecfd97d03276d375eed9459b"
|
|
502
502
|
},
|
|
503
503
|
{
|
|
504
504
|
"contractVersion": 1,
|
|
@@ -547,7 +547,7 @@
|
|
|
547
547
|
"prose-only public claims downgrade the release trust passport audit"
|
|
548
548
|
],
|
|
549
549
|
"breakingDigest": "sha256:f0b636eb925ddbf45ff1a8872454ab7b4dd98857cac12bb75217830130e62a16",
|
|
550
|
-
"auditDigest": "sha256:
|
|
550
|
+
"auditDigest": "sha256:aad90c2b39ce3fd7d2d4df5833f388c3090f5eeaecfd97d03276d375eed9459b"
|
|
551
551
|
},
|
|
552
552
|
{
|
|
553
553
|
"contractVersion": 1,
|
|
@@ -719,7 +719,7 @@
|
|
|
719
719
|
"agents can discover supported Node APIs without importing internal file paths"
|
|
720
720
|
],
|
|
721
721
|
"breakingDigest": "sha256:48f925608d3e2131d90936b07dc2a30341204cae3e6e785c0f77d61ad755c945",
|
|
722
|
-
"auditDigest": "sha256:
|
|
722
|
+
"auditDigest": "sha256:f9d8d42ea1aaa81e127ad0aeaacf846f944369f49d06f5ee749078aae320e0ee"
|
|
723
723
|
},
|
|
724
724
|
{
|
|
725
725
|
"contractVersion": 1,
|
|
@@ -3362,5 +3362,5 @@
|
|
|
3362
3362
|
}
|
|
3363
3363
|
],
|
|
3364
3364
|
"compatibilityDigest": "sha256:008b1c5a90a787cf30106829ac7aca68ab501f6d9c375e415d9c5c6b89039f46",
|
|
3365
|
-
"contractDigest": "sha256:
|
|
3365
|
+
"contractDigest": "sha256:a92b0189000ef05785d4c5097189e20856838adf94307c4174f36d5271616d16"
|
|
3366
3366
|
}
|
|
@@ -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-27T12:02:17.639Z",
|
|
5
|
+
"publishedAt": "2026-07-27T12:02:17.639Z",
|
|
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": "c0ccc4b25d65ad0e007e36d8545dfdf06853bb89",
|
|
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.7",
|
|
41
41
|
"versionSource": "package.json#version"
|
|
42
42
|
},
|
|
43
43
|
"source": {
|
|
@@ -662,7 +662,7 @@
|
|
|
662
662
|
],
|
|
663
663
|
"maturity": "stable",
|
|
664
664
|
"sourcePath": "docs/dev-alpha-candidate-patrol.md",
|
|
665
|
-
"digest": "sha256:
|
|
665
|
+
"digest": "sha256:2ec29098775cdf7f4eb80014261e437f026e05ea7a2bd6086273cdf0273bf2c2",
|
|
666
666
|
"headings": [
|
|
667
667
|
{
|
|
668
668
|
"level": 1,
|
|
@@ -675,7 +675,7 @@
|
|
|
675
675
|
"anchor": "reusable-workflow"
|
|
676
676
|
}
|
|
677
677
|
],
|
|
678
|
-
"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-
|
|
678
|
+
"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-27\nai_provenance:\n model_family: GPT-5\n product: Codex\n generated_at: 2026-07-27\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 single-flight PR controller for\nrepositories that promote a development branch into a protected Alpha branch.\nIt does not publish Alpha. The read-only observer reads the exact heads of both\nbranches, 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 as an auditable `blocked` or `stale` observation and cannot enter\nsettlement.\n\nThe companion state is\n`kungfu-buildchain-dev-alpha-candidate-state/v1`. Its current state is one of:\n\n- `observed`: no exact candidate is currently settleable;\n- `eligible-for-settlement`: a qualified candidate exists and no managed Alpha\n candidate PR is active;\n- `active`: exactly one managed candidate PR is open;\n- `retained-next`: an active PR remains authoritative and the newest different\n qualified SHA is retained as `nextCandidate`;\n- `stale`: the available exact-SHA evidence pair is outside policy age; or\n- `blocked`: qualification or reconciliation failed closed.\n\nWhen a newer qualified SHA replaces an earlier `nextCandidate`, the state also\nrecords that earlier SHA as `supersededCandidate`. Every state carries exact\nrepository, source/target refs and SHAs, workflow-run evidence through the\ncandidate decision, and canonical decision/state roots.\n\n## Reusable workflow\n\nCall `.github/workflows/dev-alpha-candidate-patrol.yml` from a thin repository\nworkflow. Start with `dry-run: true`. The reusable workflow always runs an\n`observe` job with only Actions/content/pull-request read permissions. Once the\nrepository has proven that its two workflow names and branch topology produce\nexact same-SHA evidence, it may set `settlement-authorized: true` and\n`dry-run: false`. The older `create-pull-request` input remains a compatibility\nalias for settlement authorization.\n\nThe separately permissioned `settle` job re-runs the exact observation before\nany write. With no active managed candidate, it creates one branch named from\nthe target branch and the first 12 characters of the full source SHA. An\nexisting branch must point to the same full SHA or the run fails. With one\nactive managed candidate, it only updates the machine-readable state marker in\nthat PR body so repeated events and rapid dev progress cannot create another\ncandidate PR or another heavy candidate build. Foreign human-authored Alpha PRs\nare ignored. More than one open Buildchain-managed candidate fails closed.\n\nThe PR body is the bounded durable controller state: it preserves the active\ncandidate and newest retained `nextCandidate` without introducing an always-on\nservice. Once the active PR settles or is abandoned, the next execution\nrecomputes current exact-SHA qualification and creates only the newest still\nfresh candidate. It never trusts a `workflow_run` trigger SHA as evidence.\n\nConsumers should invoke this workflow after relevant qualification workflow\ncompletion and from an offset periodic fallback. GitHub may delay scheduled\nruns, so the event path supplies low latency while the fallback supplies\nrecovery. Workflow concurrency plus the server-side open-PR reconciliation\nmakes duplicate or delayed events idempotent.\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."
|
|
679
679
|
},
|
|
680
680
|
{
|
|
681
681
|
"id": "manual:github-artifact-attestation",
|
|
@@ -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": "6d778d2a4d04a1b93ed21622719e08b3fa1e149af2941e09675c66582a3ebcdd",
|
|
25
25
|
"summary": {
|
|
26
26
|
"cliCommandCount": 97,
|
|
27
27
|
"workflowCount": 55,
|
|
@@ -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": "6d778d2a4d04a1b93ed21622719e08b3fa1e149af2941e09675c66582a3ebcdd",
|
|
232
232
|
"summary": {
|
|
233
233
|
"cliCommandCount": 97,
|
|
234
234
|
"workflowCount": 55,
|
|
@@ -3913,7 +3913,7 @@
|
|
|
3913
3913
|
"visibility": "public",
|
|
3914
3914
|
"participantFacing": true,
|
|
3915
3915
|
"public": true,
|
|
3916
|
-
"inputCount":
|
|
3916
|
+
"inputCount": 10,
|
|
3917
3917
|
"inputs": [
|
|
3918
3918
|
"alpha-workflow-path",
|
|
3919
3919
|
"buildchain-ref",
|
|
@@ -3922,6 +3922,7 @@
|
|
|
3922
3922
|
"dev-workflow-path",
|
|
3923
3923
|
"dry-run",
|
|
3924
3924
|
"max-age-seconds",
|
|
3925
|
+
"settlement-authorized",
|
|
3925
3926
|
"source-branch",
|
|
3926
3927
|
"target-branch"
|
|
3927
3928
|
],
|
|
@@ -450,7 +450,7 @@
|
|
|
450
450
|
"specifier": "@kungfu-tech/buildchain/release-passport",
|
|
451
451
|
"export": "./release-passport",
|
|
452
452
|
"target": "./packages/core/release-passport.js",
|
|
453
|
-
"digest": "sha256:
|
|
453
|
+
"digest": "sha256:aad90c2b39ce3fd7d2d4df5833f388c3090f5eeaecfd97d03276d375eed9459b",
|
|
454
454
|
"summary": "Release passport collection, verification, explanation, and evidence APIs.",
|
|
455
455
|
"capabilityGroup": "release-passport-trust",
|
|
456
456
|
"audience": [
|
|
@@ -596,7 +596,7 @@
|
|
|
596
596
|
],
|
|
597
597
|
"maturity": "stable",
|
|
598
598
|
"sourcePath": "docs/dev-alpha-candidate-patrol.md",
|
|
599
|
-
"digest": "sha256:
|
|
599
|
+
"digest": "sha256:2ec29098775cdf7f4eb80014261e437f026e05ea7a2bd6086273cdf0273bf2c2",
|
|
600
600
|
"headings": [
|
|
601
601
|
{
|
|
602
602
|
"level": 1,
|
|
@@ -609,7 +609,7 @@
|
|
|
609
609
|
"anchor": "reusable-workflow"
|
|
610
610
|
}
|
|
611
611
|
],
|
|
612
|
-
"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-
|
|
612
|
+
"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-27\nai_provenance:\n model_family: GPT-5\n product: Codex\n generated_at: 2026-07-27\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 single-flight PR controller for\nrepositories that promote a development branch into a protected Alpha branch.\nIt does not publish Alpha. The read-only observer reads the exact heads of both\nbranches, 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 as an auditable `blocked` or `stale` observation and cannot enter\nsettlement.\n\nThe companion state is\n`kungfu-buildchain-dev-alpha-candidate-state/v1`. Its current state is one of:\n\n- `observed`: no exact candidate is currently settleable;\n- `eligible-for-settlement`: a qualified candidate exists and no managed Alpha\n candidate PR is active;\n- `active`: exactly one managed candidate PR is open;\n- `retained-next`: an active PR remains authoritative and the newest different\n qualified SHA is retained as `nextCandidate`;\n- `stale`: the available exact-SHA evidence pair is outside policy age; or\n- `blocked`: qualification or reconciliation failed closed.\n\nWhen a newer qualified SHA replaces an earlier `nextCandidate`, the state also\nrecords that earlier SHA as `supersededCandidate`. Every state carries exact\nrepository, source/target refs and SHAs, workflow-run evidence through the\ncandidate decision, and canonical decision/state roots.\n\n## Reusable workflow\n\nCall `.github/workflows/dev-alpha-candidate-patrol.yml` from a thin repository\nworkflow. Start with `dry-run: true`. The reusable workflow always runs an\n`observe` job with only Actions/content/pull-request read permissions. Once the\nrepository has proven that its two workflow names and branch topology produce\nexact same-SHA evidence, it may set `settlement-authorized: true` and\n`dry-run: false`. The older `create-pull-request` input remains a compatibility\nalias for settlement authorization.\n\nThe separately permissioned `settle` job re-runs the exact observation before\nany write. With no active managed candidate, it creates one branch named from\nthe target branch and the first 12 characters of the full source SHA. An\nexisting branch must point to the same full SHA or the run fails. With one\nactive managed candidate, it only updates the machine-readable state marker in\nthat PR body so repeated events and rapid dev progress cannot create another\ncandidate PR or another heavy candidate build. Foreign human-authored Alpha PRs\nare ignored. More than one open Buildchain-managed candidate fails closed.\n\nThe PR body is the bounded durable controller state: it preserves the active\ncandidate and newest retained `nextCandidate` without introducing an always-on\nservice. Once the active PR settles or is abandoned, the next execution\nrecomputes current exact-SHA qualification and creates only the newest still\nfresh candidate. It never trusts a `workflow_run` trigger SHA as evidence.\n\nConsumers should invoke this workflow after relevant qualification workflow\ncompletion and from an offset periodic fallback. GitHub may delay scheduled\nruns, so the event path supplies low latency while the fallback supplies\nrecovery. Workflow concurrency plus the server-side open-PR reconciliation\nmakes duplicate or delayed events idempotent.\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."
|
|
613
613
|
},
|
|
614
614
|
{
|
|
615
615
|
"id": "manual:github-artifact-attestation",
|
|
@@ -1638,22 +1638,27 @@
|
|
|
1638
1638
|
"dev-workflow-path",
|
|
1639
1639
|
"dry-run",
|
|
1640
1640
|
"max-age-seconds",
|
|
1641
|
+
"settlement-authorized",
|
|
1641
1642
|
"source-branch",
|
|
1642
1643
|
"target-branch"
|
|
1643
1644
|
],
|
|
1644
|
-
"inputCount":
|
|
1645
|
+
"inputCount": 10,
|
|
1645
1646
|
"secrets": [
|
|
1646
1647
|
"promotion-token"
|
|
1647
1648
|
],
|
|
1648
1649
|
"secretCount": 1,
|
|
1649
1650
|
"outputs": [
|
|
1651
|
+
"active-candidate-pr",
|
|
1652
|
+
"controller-state",
|
|
1650
1653
|
"eligible",
|
|
1654
|
+
"next-candidate-sha",
|
|
1651
1655
|
"promotion-pr",
|
|
1652
1656
|
"result-path",
|
|
1653
1657
|
"selected-sha",
|
|
1658
|
+
"settlement-action",
|
|
1654
1659
|
"source-lock-ref"
|
|
1655
1660
|
],
|
|
1656
|
-
"outputCount":
|
|
1661
|
+
"outputCount": 9
|
|
1657
1662
|
},
|
|
1658
1663
|
{
|
|
1659
1664
|
"id": "dev-merge-queue-governance",
|
|
@@ -4227,8 +4232,8 @@
|
|
|
4227
4232
|
"workflowRegistryPath": "dist/site/workflow-registry.json",
|
|
4228
4233
|
"pageRegistryPath": "dist/site/page-registry.json",
|
|
4229
4234
|
"cliRegistryDigest": "0be723caa314a36a1a36b03522527eb5b13e9b741e6566ff7f6ee06958df02ca",
|
|
4230
|
-
"workflowRegistryDigest": "
|
|
4231
|
-
"pageRegistryDigest": "
|
|
4235
|
+
"workflowRegistryDigest": "18405fac6ddfebd647c2ba7d3927ec666e6d43dc1913043b0334c91e2bf143f1",
|
|
4236
|
+
"pageRegistryDigest": "b453877a28760e2446d5b58ee2bcbb3edd1059731a4046166ce1a4fc6649624b"
|
|
4232
4237
|
},
|
|
4233
4238
|
"comparison": {
|
|
4234
4239
|
"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-27T12:02:17.639Z",
|
|
5
|
+
"publishedAt": "2026-07-27T12:02:17.639Z",
|
|
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": "c0ccc4b25d65ad0e007e36d8545dfdf06853bb89",
|
|
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.7",
|
|
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-27T12:02:17.639Z",
|
|
5
|
+
"publishedAt": "2026-07-27T12:02:17.639Z",
|
|
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": "c0ccc4b25d65ad0e007e36d8545dfdf06853bb89",
|
|
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.7",
|
|
41
41
|
"versionSource": "package.json#version"
|
|
42
42
|
},
|
|
43
43
|
"entrypoint": "buildchain-site.json",
|
|
@@ -1238,22 +1238,27 @@
|
|
|
1238
1238
|
"dev-workflow-path",
|
|
1239
1239
|
"dry-run",
|
|
1240
1240
|
"max-age-seconds",
|
|
1241
|
+
"settlement-authorized",
|
|
1241
1242
|
"source-branch",
|
|
1242
1243
|
"target-branch"
|
|
1243
1244
|
],
|
|
1244
|
-
"inputCount":
|
|
1245
|
+
"inputCount": 10,
|
|
1245
1246
|
"secrets": [
|
|
1246
1247
|
"promotion-token"
|
|
1247
1248
|
],
|
|
1248
1249
|
"secretCount": 1,
|
|
1249
1250
|
"outputs": [
|
|
1251
|
+
"active-candidate-pr",
|
|
1252
|
+
"controller-state",
|
|
1250
1253
|
"eligible",
|
|
1254
|
+
"next-candidate-sha",
|
|
1251
1255
|
"promotion-pr",
|
|
1252
1256
|
"result-path",
|
|
1253
1257
|
"selected-sha",
|
|
1258
|
+
"settlement-action",
|
|
1254
1259
|
"source-lock-ref"
|
|
1255
1260
|
],
|
|
1256
|
-
"outputCount":
|
|
1261
|
+
"outputCount": 9,
|
|
1257
1262
|
"surface": "repository-patrol",
|
|
1258
1263
|
"capabilityGroup": "governance-versioning",
|
|
1259
1264
|
"status": "active"
|
|
@@ -8,20 +8,21 @@ confidence: high
|
|
|
8
8
|
sensitivity: public
|
|
9
9
|
evidence_grade: A
|
|
10
10
|
review_state: self-reviewed
|
|
11
|
-
last_reviewed: 2026-07-
|
|
11
|
+
last_reviewed: 2026-07-27
|
|
12
12
|
ai_provenance:
|
|
13
13
|
model_family: GPT-5
|
|
14
14
|
product: Codex
|
|
15
|
-
generated_at: 2026-07-
|
|
15
|
+
generated_at: 2026-07-27
|
|
16
16
|
visible_context: Existing Buildchain stable-candidate source locks, Kungfu exact-source Alpha preflight, Dev Patrol, and repository release governance.
|
|
17
17
|
invisible_context_boundary: No credentials, private logs, or private configuration were used.
|
|
18
18
|
---
|
|
19
19
|
|
|
20
20
|
# Dev to Alpha Candidate Patrol
|
|
21
21
|
|
|
22
|
-
Buildchain provides a reusable observation and PR controller for
|
|
23
|
-
that promote a development branch into a protected Alpha branch.
|
|
24
|
-
publish Alpha. The
|
|
22
|
+
Buildchain provides a reusable observation and single-flight PR controller for
|
|
23
|
+
repositories that promote a development branch into a protected Alpha branch.
|
|
24
|
+
It does not publish Alpha. The read-only observer reads the exact heads of both
|
|
25
|
+
branches, walks the
|
|
25
26
|
bounded development history from newest to oldest (stopping early at the Alpha
|
|
26
27
|
head), and selects the newest commit that satisfies all of these conditions:
|
|
27
28
|
|
|
@@ -45,19 +46,56 @@ The decision is `kungfu-buildchain-channel-candidate-decision/v1`. It records th
|
|
|
45
46
|
source and target branches and SHAs, comparison distance, workflow paths, run
|
|
46
47
|
identities and attempts, completion times, URLs, policy, and a canonical decision
|
|
47
48
|
root. Missing, stale, failed, duplicate, or source-mismatched evidence fails
|
|
48
|
-
closed
|
|
49
|
+
closed as an auditable `blocked` or `stale` observation and cannot enter
|
|
50
|
+
settlement.
|
|
51
|
+
|
|
52
|
+
The companion state is
|
|
53
|
+
`kungfu-buildchain-dev-alpha-candidate-state/v1`. Its current state is one of:
|
|
54
|
+
|
|
55
|
+
- `observed`: no exact candidate is currently settleable;
|
|
56
|
+
- `eligible-for-settlement`: a qualified candidate exists and no managed Alpha
|
|
57
|
+
candidate PR is active;
|
|
58
|
+
- `active`: exactly one managed candidate PR is open;
|
|
59
|
+
- `retained-next`: an active PR remains authoritative and the newest different
|
|
60
|
+
qualified SHA is retained as `nextCandidate`;
|
|
61
|
+
- `stale`: the available exact-SHA evidence pair is outside policy age; or
|
|
62
|
+
- `blocked`: qualification or reconciliation failed closed.
|
|
63
|
+
|
|
64
|
+
When a newer qualified SHA replaces an earlier `nextCandidate`, the state also
|
|
65
|
+
records that earlier SHA as `supersededCandidate`. Every state carries exact
|
|
66
|
+
repository, source/target refs and SHAs, workflow-run evidence through the
|
|
67
|
+
candidate decision, and canonical decision/state roots.
|
|
49
68
|
|
|
50
69
|
## Reusable workflow
|
|
51
70
|
|
|
52
71
|
Call `.github/workflows/dev-alpha-candidate-patrol.yml` from a thin repository
|
|
53
|
-
workflow. Start with `dry-run: true`.
|
|
54
|
-
|
|
55
|
-
|
|
72
|
+
workflow. Start with `dry-run: true`. The reusable workflow always runs an
|
|
73
|
+
`observe` job with only Actions/content/pull-request read permissions. Once the
|
|
74
|
+
repository has proven that its two workflow names and branch topology produce
|
|
75
|
+
exact same-SHA evidence, it may set `settlement-authorized: true` and
|
|
76
|
+
`dry-run: false`. The older `create-pull-request` input remains a compatibility
|
|
77
|
+
alias for settlement authorization.
|
|
78
|
+
|
|
79
|
+
The separately permissioned `settle` job re-runs the exact observation before
|
|
80
|
+
any write. With no active managed candidate, it creates one branch named from
|
|
81
|
+
the target branch and the first 12 characters of the full source SHA. An
|
|
82
|
+
existing branch must point to the same full SHA or the run fails. With one
|
|
83
|
+
active managed candidate, it only updates the machine-readable state marker in
|
|
84
|
+
that PR body so repeated events and rapid dev progress cannot create another
|
|
85
|
+
candidate PR or another heavy candidate build. Foreign human-authored Alpha PRs
|
|
86
|
+
are ignored. More than one open Buildchain-managed candidate fails closed.
|
|
87
|
+
|
|
88
|
+
The PR body is the bounded durable controller state: it preserves the active
|
|
89
|
+
candidate and newest retained `nextCandidate` without introducing an always-on
|
|
90
|
+
service. Once the active PR settles or is abandoned, the next execution
|
|
91
|
+
recomputes current exact-SHA qualification and creates only the newest still
|
|
92
|
+
fresh candidate. It never trusts a `workflow_run` trigger SHA as evidence.
|
|
56
93
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
94
|
+
Consumers should invoke this workflow after relevant qualification workflow
|
|
95
|
+
completion and from an offset periodic fallback. GitHub may delay scheduled
|
|
96
|
+
runs, so the event path supplies low latency while the fallback supplies
|
|
97
|
+
recovery. Workflow concurrency plus the server-side open-PR reconciliation
|
|
98
|
+
makes duplicate or delayed events idempotent.
|
|
61
99
|
|
|
62
100
|
The workflow never moves the Alpha ref directly, merges or auto-merges the pull
|
|
63
101
|
request, publishes npm, creates a Git tag or GitHub Release, or changes branch
|
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.7",
|
|
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",
|
|
@@ -1170,6 +1170,7 @@ export function createReleasePassport({
|
|
|
1170
1170
|
controllerReceipts = [],
|
|
1171
1171
|
controllerReceiptReferences = [],
|
|
1172
1172
|
githubArtifactAttestations = [],
|
|
1173
|
+
checkedAt = "",
|
|
1173
1174
|
} = {}) {
|
|
1174
1175
|
const normalizedTag = nonEmptyString(tag, "tag");
|
|
1175
1176
|
const artifactEvidence = createArtifactEvidence({ assets, repository, tag: normalizedTag, sourceSha, workflow });
|
|
@@ -1189,7 +1190,7 @@ export function createReleasePassport({
|
|
|
1189
1190
|
? normalizeEvidenceDocument(distTagPromotionEvidence, "distTagPromotionEvidence")
|
|
1190
1191
|
: undefined;
|
|
1191
1192
|
const normalizedImpact = normalizeImpactLedger(impact, { tag: normalizedTag, line });
|
|
1192
|
-
const generatedAt = nowIso();
|
|
1193
|
+
const generatedAt = optionalString(checkedAt) || nowIso();
|
|
1193
1194
|
const kfd1Metadata = resolveKfd1Metadata();
|
|
1194
1195
|
const normalizedKfd1 = kfd1?.passportSection ? kfd1 : undefined;
|
|
1195
1196
|
const normalizedKfd3 = kfd3?.passportSection ? kfd3 : undefined;
|
|
@@ -1465,6 +1466,7 @@ export function collectGitHubReleasePassport({
|
|
|
1465
1466
|
releaseJsonExtra = "",
|
|
1466
1467
|
publishJson = "",
|
|
1467
1468
|
workflow = {},
|
|
1469
|
+
checkedAt = "",
|
|
1468
1470
|
} = {}) {
|
|
1469
1471
|
const release = parseJsonInput(releaseJson, {}, { cwd, label: "releaseJson" });
|
|
1470
1472
|
const releaseExtra = parseJsonInput(releaseJsonExtra, {}, { cwd, label: "releaseJsonExtra" });
|
|
@@ -1558,6 +1560,7 @@ export function collectGitHubReleasePassport({
|
|
|
1558
1560
|
];
|
|
1559
1561
|
const resolvedTag = tag || release.tag_name || release.name || "";
|
|
1560
1562
|
const resolvedOutputDir = path.resolve(cwd, outputDir);
|
|
1563
|
+
const resolvedCheckedAt = optionalString(checkedAt) || nowIso();
|
|
1561
1564
|
const productMechanism = defaultProductMechanism({ repository, productName });
|
|
1562
1565
|
const artifactEvidence = createArtifactEvidence({ assets, repository, tag: resolvedTag, sourceSha, workflow });
|
|
1563
1566
|
const bundledPublishEvidencePath = publishEvidenceMeta.value ? "evidence.json" : "";
|
|
@@ -1585,7 +1588,7 @@ export function collectGitHubReleasePassport({
|
|
|
1585
1588
|
matrixRoot: kfdSupportMatrixMeta.sha256 ? `sha256:${kfdSupportMatrixMeta.sha256}` : "",
|
|
1586
1589
|
gateResults: kfdProductGateMetas.map((meta) => meta.value),
|
|
1587
1590
|
expectedSourceSha: sourceSha,
|
|
1588
|
-
checkedAt:
|
|
1591
|
+
checkedAt: resolvedCheckedAt,
|
|
1589
1592
|
})
|
|
1590
1593
|
: undefined;
|
|
1591
1594
|
const passport = mergeAuthoritativePassportBase(createReleasePassport({
|
|
@@ -1644,6 +1647,7 @@ export function collectGitHubReleasePassport({
|
|
|
1644
1647
|
publishEvidencePath: publishEvidenceMeta.path ? path.relative(resolvedOutputDir, publishEvidenceMeta.path).split(path.sep).join("/") : "",
|
|
1645
1648
|
transactionStatePath: transactionMeta.path ? path.relative(resolvedOutputDir, transactionMeta.path).split(path.sep).join("/") : "",
|
|
1646
1649
|
workflow,
|
|
1650
|
+
checkedAt: resolvedCheckedAt,
|
|
1647
1651
|
}), basePassportMeta.value, { requireKfd: requireBaseKfd });
|
|
1648
1652
|
if (bundledPublishEvidencePath) {
|
|
1649
1653
|
passport.evidence.publishEvidence = bundledPublishEvidencePath;
|
|
@@ -1657,7 +1661,7 @@ export function collectGitHubReleasePassport({
|
|
|
1657
1661
|
productMechanism,
|
|
1658
1662
|
kfdAgentHubEvidence: kfdAgentHubEvidenceMeta.value,
|
|
1659
1663
|
kfdSupportEvidence: kfdSupport,
|
|
1660
|
-
checkedAt:
|
|
1664
|
+
checkedAt: resolvedCheckedAt,
|
|
1661
1665
|
});
|
|
1662
1666
|
const files = {
|
|
1663
1667
|
"product-mechanism.json": productMechanism,
|
|
@@ -2473,6 +2477,7 @@ export async function verifyReleasePassport({
|
|
|
2473
2477
|
productMechanismLocation = "",
|
|
2474
2478
|
kfdAgentHubEvidenceLocation = "",
|
|
2475
2479
|
kfdSupportEvidenceLocation = "",
|
|
2480
|
+
checkedAt = nowIso(),
|
|
2476
2481
|
} = {}) {
|
|
2477
2482
|
const passport = await readJsonFromLocation(passportLocation);
|
|
2478
2483
|
const basePath = /^https?:\/\//.test(passportLocation) ? passportLocation : path.resolve(passportLocation);
|
|
@@ -2513,6 +2518,7 @@ export async function verifyReleasePassport({
|
|
|
2513
2518
|
productMechanism,
|
|
2514
2519
|
kfdAgentHubEvidence,
|
|
2515
2520
|
kfdSupportEvidence,
|
|
2521
|
+
checkedAt,
|
|
2516
2522
|
});
|
|
2517
2523
|
}
|
|
2518
2524
|
|
|
@@ -3,8 +3,16 @@
|
|
|
3
3
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import crypto from "node:crypto";
|
|
6
7
|
import { decideChannelCandidate } from "../packages/core/channel-candidate.js";
|
|
7
8
|
|
|
9
|
+
export const DEV_ALPHA_CANDIDATE_STATE_SCHEMA =
|
|
10
|
+
"kungfu-buildchain-dev-alpha-candidate-state/v1";
|
|
11
|
+
const STATE_MARKER_START = "<!-- buildchain-dev-alpha-candidate-state";
|
|
12
|
+
const STATE_MARKER_END = "-->";
|
|
13
|
+
const LEGACY_BODY_MARKER = "Buildchain exact-source channel candidate.";
|
|
14
|
+
const EXACT_SHA = /^[0-9a-f]{40}$/u;
|
|
15
|
+
|
|
8
16
|
function text(value = "") {
|
|
9
17
|
return String(value ?? "").trim();
|
|
10
18
|
}
|
|
@@ -46,7 +54,127 @@ function integer(value, fallback) {
|
|
|
46
54
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
47
55
|
}
|
|
48
56
|
|
|
57
|
+
function canonical(value) {
|
|
58
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
59
|
+
if (value && typeof value === "object") {
|
|
60
|
+
return Object.fromEntries(
|
|
61
|
+
Object.entries(value)
|
|
62
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
63
|
+
.map(([key, item]) => [key, canonical(item)]),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function evidenceRoot(value) {
|
|
70
|
+
return `sha256:${crypto
|
|
71
|
+
.createHash("sha256")
|
|
72
|
+
.update(JSON.stringify(canonical(value)))
|
|
73
|
+
.digest("hex")}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function candidateStateMarker(state) {
|
|
77
|
+
return `${STATE_MARKER_START}\n${JSON.stringify(state)}\n${STATE_MARKER_END}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function parseCandidateStateMarker(bodyInput) {
|
|
81
|
+
const body = String(bodyInput || "");
|
|
82
|
+
const start = body.indexOf(STATE_MARKER_START);
|
|
83
|
+
if (start < 0) return undefined;
|
|
84
|
+
const jsonStart = body.indexOf("\n", start);
|
|
85
|
+
const end = body.indexOf(STATE_MARKER_END, jsonStart + 1);
|
|
86
|
+
if (jsonStart < 0 || end < 0)
|
|
87
|
+
throw new Error("malformed Buildchain candidate state marker");
|
|
88
|
+
const state = JSON.parse(body.slice(jsonStart + 1, end).trim());
|
|
89
|
+
if (state.schema !== DEV_ALPHA_CANDIDATE_STATE_SCHEMA)
|
|
90
|
+
throw new Error(
|
|
91
|
+
`unsupported candidate state schema ${state.schema || "<empty>"}`,
|
|
92
|
+
);
|
|
93
|
+
return state;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function replaceCandidateStateMarker(bodyInput, state) {
|
|
97
|
+
const body = String(bodyInput || "").trimEnd();
|
|
98
|
+
const start = body.indexOf(STATE_MARKER_START);
|
|
99
|
+
if (start < 0) return `${body}\n\n${candidateStateMarker(state)}\n`;
|
|
100
|
+
const end = body.indexOf(STATE_MARKER_END, start);
|
|
101
|
+
if (end < 0) throw new Error("malformed Buildchain candidate state marker");
|
|
102
|
+
return `${body.slice(0, start).trimEnd()}\n\n${candidateStateMarker(state)}\n`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function sourceShaFromLegacyBody(bodyInput) {
|
|
106
|
+
return String(bodyInput || "").match(/- Source SHA: `([0-9a-f]{40})`/u)?.[1];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function targetSlug(targetBranch) {
|
|
110
|
+
return targetBranch.replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function managedCandidateFromPullRequest(pullRequest, targetBranch) {
|
|
114
|
+
const body = String(pullRequest.body || "");
|
|
115
|
+
const marker = parseCandidateStateMarker(body);
|
|
116
|
+
const expectedPrefix = `buildchain/candidate/${targetSlug(targetBranch)}/`;
|
|
117
|
+
const headRef = text(pullRequest.head?.ref);
|
|
118
|
+
const baseRef = text(pullRequest.base?.ref || targetBranch);
|
|
119
|
+
if (baseRef !== targetBranch) return undefined;
|
|
120
|
+
if (marker) {
|
|
121
|
+
if (marker.targetBranch !== targetBranch)
|
|
122
|
+
throw new Error(
|
|
123
|
+
`candidate PR #${pullRequest.number} state targets ${marker.targetBranch}, not ${targetBranch}`,
|
|
124
|
+
);
|
|
125
|
+
if (!headRef.startsWith(expectedPrefix))
|
|
126
|
+
throw new Error(
|
|
127
|
+
`candidate PR #${pullRequest.number} head ${headRef} is outside ${expectedPrefix}`,
|
|
128
|
+
);
|
|
129
|
+
const sourceSha = text(marker.activeCandidate?.sourceSha);
|
|
130
|
+
if (
|
|
131
|
+
!EXACT_SHA.test(sourceSha) ||
|
|
132
|
+
headRef !== `${expectedPrefix}${sourceSha.slice(0, 12)}`
|
|
133
|
+
) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`candidate PR #${pullRequest.number} state does not bind its exact source-lock head`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
number: Number(pullRequest.number),
|
|
140
|
+
url: text(pullRequest.html_url),
|
|
141
|
+
body,
|
|
142
|
+
sourceSha,
|
|
143
|
+
sourceLockRef: headRef,
|
|
144
|
+
decisionRoot: text(marker.activeCandidate?.decisionRoot),
|
|
145
|
+
nextCandidate: marker.nextCandidate || null,
|
|
146
|
+
state: marker,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (!body.includes(LEGACY_BODY_MARKER) || !headRef.startsWith(expectedPrefix))
|
|
150
|
+
return undefined;
|
|
151
|
+
const sourceSha = sourceShaFromLegacyBody(body);
|
|
152
|
+
if (!sourceSha)
|
|
153
|
+
throw new Error(
|
|
154
|
+
`legacy candidate PR #${pullRequest.number} has no exact source SHA`,
|
|
155
|
+
);
|
|
156
|
+
if (headRef !== `${expectedPrefix}${sourceSha.slice(0, 12)}`)
|
|
157
|
+
throw new Error(
|
|
158
|
+
`legacy candidate PR #${pullRequest.number} does not bind its exact source-lock head`,
|
|
159
|
+
);
|
|
160
|
+
return {
|
|
161
|
+
number: Number(pullRequest.number),
|
|
162
|
+
url: text(pullRequest.html_url),
|
|
163
|
+
body,
|
|
164
|
+
sourceSha,
|
|
165
|
+
sourceLockRef: headRef,
|
|
166
|
+
decisionRoot: "",
|
|
167
|
+
nextCandidate: null,
|
|
168
|
+
state: null,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
49
172
|
export function normalizeDevAlphaPatrolOptions(options = {}) {
|
|
173
|
+
const createPullRequest = bool(
|
|
174
|
+
options.createPullRequest ??
|
|
175
|
+
process.env.BUILDCHAIN_CHANNEL_PATROL_CREATE_PR,
|
|
176
|
+
false,
|
|
177
|
+
);
|
|
50
178
|
return {
|
|
51
179
|
repository: repository(
|
|
52
180
|
options.repository ??
|
|
@@ -82,10 +210,11 @@ export function normalizeDevAlphaPatrolOptions(options = {}) {
|
|
|
82
210
|
process.env.BUILDCHAIN_CHANNEL_PATROL_MAX_AGE_SECONDS,
|
|
83
211
|
7 * 24 * 60 * 60,
|
|
84
212
|
),
|
|
85
|
-
createPullRequest
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
213
|
+
createPullRequest,
|
|
214
|
+
settlementAuthorized: bool(
|
|
215
|
+
options.settlementAuthorized ??
|
|
216
|
+
process.env.BUILDCHAIN_CHANNEL_PATROL_SETTLEMENT_AUTHORIZED,
|
|
217
|
+
createPullRequest,
|
|
89
218
|
),
|
|
90
219
|
dryRun: bool(
|
|
91
220
|
options.dryRun ?? process.env.BUILDCHAIN_CHANNEL_PATROL_DRY_RUN,
|
|
@@ -183,11 +312,118 @@ export function selectLatestQualifiedSource({
|
|
|
183
312
|
};
|
|
184
313
|
}
|
|
185
314
|
}
|
|
315
|
+
const staleSuccessfulPair = sourceHistory.some((sourceSha) => {
|
|
316
|
+
const rows = requiredWorkflowPaths.map((workflow) =>
|
|
317
|
+
latestByPath.get(workflow).get(sourceSha),
|
|
318
|
+
);
|
|
319
|
+
return (
|
|
320
|
+
rows.every(
|
|
321
|
+
(run) =>
|
|
322
|
+
run?.status === "completed" &&
|
|
323
|
+
run?.conclusion === "success" &&
|
|
324
|
+
run?.head_sha === sourceSha,
|
|
325
|
+
) &&
|
|
326
|
+
rows.some(
|
|
327
|
+
(run) =>
|
|
328
|
+
!workflowEvidenceIsFreshAndSuccessful(run, { now, maxAgeSeconds }),
|
|
329
|
+
)
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
if (staleSuccessfulPair)
|
|
333
|
+
throw new Error(
|
|
334
|
+
"same-SHA workflow evidence is stale for every qualified source commit",
|
|
335
|
+
);
|
|
186
336
|
throw new Error(
|
|
187
337
|
"no source commit ahead of target has fresh completed successful same-SHA workflow evidence",
|
|
188
338
|
);
|
|
189
339
|
}
|
|
190
340
|
|
|
341
|
+
function blockedCandidateDecision({
|
|
342
|
+
options,
|
|
343
|
+
sourceSha,
|
|
344
|
+
targetSha,
|
|
345
|
+
comparison,
|
|
346
|
+
reason,
|
|
347
|
+
}) {
|
|
348
|
+
const body = {
|
|
349
|
+
schema: "kungfu-buildchain-channel-candidate-decision/v1",
|
|
350
|
+
eligible: false,
|
|
351
|
+
reason: "qualification-evidence-blocked",
|
|
352
|
+
repository: options.repository,
|
|
353
|
+
source: { branch: options.sourceBranch, sha: sourceSha },
|
|
354
|
+
target: { branch: options.targetBranch, sha: targetSha },
|
|
355
|
+
comparison: {
|
|
356
|
+
status: text(comparison.status || "unknown"),
|
|
357
|
+
aheadBy: Number(comparison.ahead_by || 0),
|
|
358
|
+
},
|
|
359
|
+
blockReason: text(reason),
|
|
360
|
+
decidedAt: options.now,
|
|
361
|
+
};
|
|
362
|
+
return { ...body, decisionRoot: evidenceRoot(body) };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function candidateFromDecision(decision) {
|
|
366
|
+
if (!decision.eligible) return null;
|
|
367
|
+
return {
|
|
368
|
+
sourceSha: decision.source.sha,
|
|
369
|
+
sourceLockRef: decision.sourceLockRef,
|
|
370
|
+
decisionRoot: decision.decisionRoot,
|
|
371
|
+
workflowEvidence: decision.workflowEvidence,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function candidateStateBody({
|
|
376
|
+
options,
|
|
377
|
+
targetSha,
|
|
378
|
+
decision,
|
|
379
|
+
activeCandidate,
|
|
380
|
+
nextCandidate,
|
|
381
|
+
supersededCandidate,
|
|
382
|
+
}) {
|
|
383
|
+
const body = {
|
|
384
|
+
schema: DEV_ALPHA_CANDIDATE_STATE_SCHEMA,
|
|
385
|
+
repository: options.repository,
|
|
386
|
+
sourceBranch: options.sourceBranch,
|
|
387
|
+
targetBranch: options.targetBranch,
|
|
388
|
+
targetSha,
|
|
389
|
+
activeCandidate,
|
|
390
|
+
nextCandidate,
|
|
391
|
+
observationDecisionRoot: decision.decisionRoot || null,
|
|
392
|
+
observedAt: options.now,
|
|
393
|
+
...(supersededCandidate ? { supersededCandidate } : {}),
|
|
394
|
+
};
|
|
395
|
+
return { ...body, stateRoot: evidenceRoot(body) };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function pullRequestBody({
|
|
399
|
+
options,
|
|
400
|
+
observedSourceHeadSha,
|
|
401
|
+
sourceSha,
|
|
402
|
+
skippedNewerCommitCount,
|
|
403
|
+
targetSha,
|
|
404
|
+
decision,
|
|
405
|
+
state,
|
|
406
|
+
}) {
|
|
407
|
+
return [
|
|
408
|
+
LEGACY_BODY_MARKER,
|
|
409
|
+
"",
|
|
410
|
+
`- Source branch: \`${options.sourceBranch}\``,
|
|
411
|
+
`- Observed source HEAD: \`${observedSourceHeadSha}\``,
|
|
412
|
+
`- Source SHA: \`${sourceSha}\``,
|
|
413
|
+
`- Skipped newer unqualified commits: \`${skippedNewerCommitCount}\``,
|
|
414
|
+
`- Target branch/head: \`${options.targetBranch}\` / \`${targetSha}\``,
|
|
415
|
+
`- Decision root: \`${decision.decisionRoot}\``,
|
|
416
|
+
...decision.workflowEvidence.map(
|
|
417
|
+
(row) =>
|
|
418
|
+
`- ${row.workflowName}: [run ${row.runId} attempt ${row.runAttempt}](${row.url})`,
|
|
419
|
+
),
|
|
420
|
+
"",
|
|
421
|
+
"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.",
|
|
422
|
+
"",
|
|
423
|
+
candidateStateMarker(state),
|
|
424
|
+
].join("\n");
|
|
425
|
+
}
|
|
426
|
+
|
|
191
427
|
export async function runDevAlphaCandidatePatrol(
|
|
192
428
|
optionsInput = {},
|
|
193
429
|
clientInput,
|
|
@@ -214,6 +450,7 @@ export async function runDevAlphaCandidatePatrol(
|
|
|
214
450
|
let comparison = headComparison;
|
|
215
451
|
let workflowEvidence = [];
|
|
216
452
|
let skippedNewerCommitCount = 0;
|
|
453
|
+
let qualificationError;
|
|
217
454
|
if (
|
|
218
455
|
headComparison.status === "ahead" &&
|
|
219
456
|
Number(headComparison.ahead_by) > 0
|
|
@@ -224,71 +461,189 @@ export async function runDevAlphaCandidatePatrol(
|
|
|
224
461
|
client.listCompletedWorkflowRuns(workflow, options.sourceBranch),
|
|
225
462
|
),
|
|
226
463
|
]);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
workflow,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
464
|
+
try {
|
|
465
|
+
const selected = selectLatestQualifiedSource({
|
|
466
|
+
sourceHistory,
|
|
467
|
+
workflowRunsByPath: new Map(
|
|
468
|
+
requiredWorkflowPaths.map((workflow, index) => [
|
|
469
|
+
workflow,
|
|
470
|
+
workflowRunSets[index],
|
|
471
|
+
]),
|
|
472
|
+
),
|
|
473
|
+
requiredWorkflowPaths,
|
|
474
|
+
now: options.now,
|
|
475
|
+
maxAgeSeconds: options.maxAgeSeconds,
|
|
476
|
+
});
|
|
477
|
+
sourceSha = selected.sourceSha;
|
|
478
|
+
skippedNewerCommitCount = selected.skippedNewerCommitCount;
|
|
479
|
+
workflowEvidence = selected.workflowEvidence;
|
|
480
|
+
if (sourceSha !== observedSourceHeadSha)
|
|
481
|
+
comparison = await client.compare(targetSha, sourceSha);
|
|
482
|
+
} catch (error) {
|
|
483
|
+
qualificationError = error;
|
|
484
|
+
}
|
|
244
485
|
}
|
|
245
|
-
const decision =
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
486
|
+
const decision = qualificationError
|
|
487
|
+
? blockedCandidateDecision({
|
|
488
|
+
options,
|
|
489
|
+
sourceSha: observedSourceHeadSha,
|
|
490
|
+
targetSha,
|
|
491
|
+
comparison: headComparison,
|
|
492
|
+
reason: qualificationError.message,
|
|
493
|
+
})
|
|
494
|
+
: decideChannelCandidate({
|
|
495
|
+
repository: options.repository,
|
|
496
|
+
sourceBranch: options.sourceBranch,
|
|
497
|
+
targetBranch: options.targetBranch,
|
|
498
|
+
sourceSha,
|
|
499
|
+
targetSha,
|
|
500
|
+
comparison: { status: comparison.status, aheadBy: comparison.ahead_by },
|
|
501
|
+
selection: {
|
|
502
|
+
mode: "latest-qualified-source-ancestor",
|
|
503
|
+
observedSourceHeadSha,
|
|
504
|
+
skippedNewerCommitCount,
|
|
505
|
+
},
|
|
506
|
+
workflowEvidence,
|
|
507
|
+
requiredWorkflowPaths,
|
|
508
|
+
maxAgeSeconds: options.maxAgeSeconds,
|
|
509
|
+
now: options.now,
|
|
510
|
+
});
|
|
511
|
+
const openPullRequests = await client.listOpenPullRequests(
|
|
512
|
+
options.targetBranch,
|
|
513
|
+
);
|
|
514
|
+
const managedCandidates = openPullRequests
|
|
515
|
+
.map((pullRequest) =>
|
|
516
|
+
managedCandidateFromPullRequest(pullRequest, options.targetBranch),
|
|
517
|
+
)
|
|
518
|
+
.filter(Boolean);
|
|
519
|
+
for (const candidate of managedCandidates) {
|
|
520
|
+
if (
|
|
521
|
+
candidate.state &&
|
|
522
|
+
(candidate.state.repository !== options.repository ||
|
|
523
|
+
candidate.state.sourceBranch !== options.sourceBranch)
|
|
524
|
+
) {
|
|
525
|
+
throw new Error(
|
|
526
|
+
`candidate PR #${candidate.number} state does not bind ${options.repository} ${options.sourceBranch}`,
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (managedCandidates.length > 1) {
|
|
531
|
+
throw new Error(
|
|
532
|
+
`multiple open Buildchain candidate PRs target ${options.targetBranch}: ${managedCandidates
|
|
533
|
+
.map((candidate) => `#${candidate.number}`)
|
|
534
|
+
.join(", ")}`,
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
let activeCandidate = managedCandidates[0] || null;
|
|
538
|
+
const observedCandidate = candidateFromDecision(decision);
|
|
539
|
+
let nextCandidate = null;
|
|
540
|
+
let supersededCandidate = null;
|
|
541
|
+
let controllerState = decision.eligible
|
|
542
|
+
? "eligible-for-settlement"
|
|
543
|
+
: qualificationError
|
|
544
|
+
? /stale/u.test(qualificationError.message)
|
|
545
|
+
? "stale"
|
|
546
|
+
: "blocked"
|
|
547
|
+
: "observed";
|
|
548
|
+
if (activeCandidate) {
|
|
549
|
+
controllerState = "active";
|
|
550
|
+
if (
|
|
551
|
+
observedCandidate &&
|
|
552
|
+
observedCandidate.sourceSha !== activeCandidate.sourceSha
|
|
553
|
+
) {
|
|
554
|
+
nextCandidate = observedCandidate;
|
|
555
|
+
controllerState = "retained-next";
|
|
556
|
+
}
|
|
557
|
+
if (
|
|
558
|
+
activeCandidate.nextCandidate &&
|
|
559
|
+
activeCandidate.nextCandidate.sourceSha !== nextCandidate?.sourceSha
|
|
560
|
+
) {
|
|
561
|
+
supersededCandidate = activeCandidate.nextCandidate;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
let state = candidateStateBody({
|
|
565
|
+
options,
|
|
250
566
|
targetSha,
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
567
|
+
decision,
|
|
568
|
+
activeCandidate: activeCandidate
|
|
569
|
+
? {
|
|
570
|
+
sourceSha: activeCandidate.sourceSha,
|
|
571
|
+
sourceLockRef: activeCandidate.sourceLockRef,
|
|
572
|
+
decisionRoot: activeCandidate.decisionRoot || null,
|
|
573
|
+
pullRequestNumber: activeCandidate.number,
|
|
574
|
+
pullRequestUrl: activeCandidate.url,
|
|
575
|
+
}
|
|
576
|
+
: null,
|
|
577
|
+
nextCandidate,
|
|
578
|
+
supersededCandidate,
|
|
261
579
|
});
|
|
262
580
|
let pullRequest;
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
581
|
+
let settlementAction = "none";
|
|
582
|
+
if (options.settlementAuthorized && !options.dryRun) {
|
|
583
|
+
if (activeCandidate) {
|
|
584
|
+
const nextBody = replaceCandidateStateMarker(activeCandidate.body, state);
|
|
585
|
+
if (nextBody !== activeCandidate.body) {
|
|
586
|
+
await client.updatePullRequestBody(activeCandidate.number, nextBody);
|
|
587
|
+
settlementAction = nextCandidate
|
|
588
|
+
? supersededCandidate
|
|
589
|
+
? "supersede-next-candidate"
|
|
590
|
+
: "retain-next-candidate"
|
|
591
|
+
: "reconcile-active-candidate";
|
|
592
|
+
}
|
|
593
|
+
pullRequest = {
|
|
594
|
+
number: activeCandidate.number,
|
|
595
|
+
html_url: activeCandidate.url,
|
|
596
|
+
};
|
|
597
|
+
} else if (decision.eligible) {
|
|
598
|
+
await client.ensureImmutableBranch(decision.sourceLockRef, sourceSha);
|
|
599
|
+
state = candidateStateBody({
|
|
600
|
+
options,
|
|
601
|
+
targetSha,
|
|
602
|
+
decision,
|
|
603
|
+
activeCandidate: observedCandidate,
|
|
604
|
+
nextCandidate: null,
|
|
605
|
+
supersededCandidate: null,
|
|
606
|
+
});
|
|
607
|
+
pullRequest = await client.ensurePullRequest({
|
|
608
|
+
head: decision.sourceLockRef,
|
|
609
|
+
base: options.targetBranch,
|
|
610
|
+
title: `Promote qualified ${options.sourceBranch} candidate ${sourceSha.slice(0, 12)} to ${options.targetBranch}`,
|
|
611
|
+
body: pullRequestBody({
|
|
612
|
+
options,
|
|
613
|
+
observedSourceHeadSha,
|
|
614
|
+
sourceSha,
|
|
615
|
+
skippedNewerCommitCount,
|
|
616
|
+
targetSha,
|
|
617
|
+
decision,
|
|
618
|
+
state,
|
|
619
|
+
}),
|
|
620
|
+
});
|
|
621
|
+
settlementAction = "create-active-candidate";
|
|
622
|
+
controllerState = "active";
|
|
623
|
+
activeCandidate = {
|
|
624
|
+
number: Number(pullRequest.number || 0),
|
|
625
|
+
url: text(pullRequest.html_url),
|
|
626
|
+
sourceSha,
|
|
627
|
+
sourceLockRef: decision.sourceLockRef,
|
|
628
|
+
decisionRoot: decision.decisionRoot,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
286
631
|
}
|
|
287
632
|
return {
|
|
288
633
|
schema: "kungfu-buildchain-dev-alpha-candidate-patrol/v1",
|
|
289
634
|
dryRun: options.dryRun,
|
|
290
635
|
createPullRequest: options.createPullRequest,
|
|
636
|
+
settlementAuthorized: options.settlementAuthorized,
|
|
291
637
|
decision,
|
|
638
|
+
controller: {
|
|
639
|
+
schema: DEV_ALPHA_CANDIDATE_STATE_SCHEMA,
|
|
640
|
+
state: controllerState,
|
|
641
|
+
activeCandidate,
|
|
642
|
+
nextCandidate,
|
|
643
|
+
supersededCandidate,
|
|
644
|
+
settlementAction,
|
|
645
|
+
stateRoot: state.stateRoot,
|
|
646
|
+
},
|
|
292
647
|
pullRequest: pullRequest || null,
|
|
293
648
|
};
|
|
294
649
|
}
|
|
@@ -301,6 +656,8 @@ export function createGitHubChannelCandidateClient({
|
|
|
301
656
|
repository: repositoryInput,
|
|
302
657
|
token,
|
|
303
658
|
fetchImpl = globalThis.fetch,
|
|
659
|
+
sleepImpl = (milliseconds) =>
|
|
660
|
+
new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
304
661
|
}) {
|
|
305
662
|
const [owner, repo] = repository(repositoryInput).split("/");
|
|
306
663
|
const headers = {
|
|
@@ -313,21 +670,37 @@ export function createGitHubChannelCandidateClient({
|
|
|
313
670
|
requestPath,
|
|
314
671
|
{ method = "GET", body, allow404 = false } = {},
|
|
315
672
|
) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
Object.
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
673
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
674
|
+
const response = await fetchImpl(`https://api.github.com${requestPath}`, {
|
|
675
|
+
method,
|
|
676
|
+
headers: Object.fromEntries(
|
|
677
|
+
Object.entries(headers).filter(([, value]) => value),
|
|
678
|
+
),
|
|
679
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
680
|
+
});
|
|
681
|
+
const raw = await response.text();
|
|
682
|
+
const payload = raw ? JSON.parse(raw) : undefined;
|
|
683
|
+
if (allow404 && response.status === 404) return undefined;
|
|
684
|
+
if (response.ok) return payload;
|
|
685
|
+
const retryable = response.status === 429 || response.status >= 500;
|
|
686
|
+
if (retryable && attempt < 3) {
|
|
687
|
+
const retryAfterHeader = response.headers?.get?.("retry-after");
|
|
688
|
+
const retryAfter =
|
|
689
|
+
retryAfterHeader === null || retryAfterHeader === undefined
|
|
690
|
+
? Number.NaN
|
|
691
|
+
: Number(retryAfterHeader);
|
|
692
|
+
await sleepImpl(
|
|
693
|
+
Number.isFinite(retryAfter) && retryAfter >= 0
|
|
694
|
+
? Math.min(retryAfter * 1000, 10_000)
|
|
695
|
+
: attempt * 250,
|
|
696
|
+
);
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
327
699
|
throw new Error(
|
|
328
700
|
`GitHub API ${method} ${requestPath} failed with ${response.status}: ${payload?.message || raw}`,
|
|
329
701
|
);
|
|
330
|
-
|
|
702
|
+
}
|
|
703
|
+
throw new Error(`GitHub API ${method} ${requestPath} exhausted retries`);
|
|
331
704
|
}
|
|
332
705
|
return {
|
|
333
706
|
async resolveBranch(ref) {
|
|
@@ -368,6 +741,19 @@ export function createGitHubChannelCandidateClient({
|
|
|
368
741
|
}
|
|
369
742
|
return commits;
|
|
370
743
|
},
|
|
744
|
+
async listOpenPullRequests(base) {
|
|
745
|
+
const pullRequests = [];
|
|
746
|
+
for (let page = 1; page <= 10; page += 1) {
|
|
747
|
+
const rows = await api(
|
|
748
|
+
`/repos/${owner}/${repo}/pulls?state=open&base=${encodeURIComponent(base)}&per_page=100&page=${page}`,
|
|
749
|
+
);
|
|
750
|
+
pullRequests.push(...rows);
|
|
751
|
+
if (rows.length < 100) return pullRequests;
|
|
752
|
+
}
|
|
753
|
+
throw new Error(
|
|
754
|
+
`open pull request history for ${base} exceeds 1000 rows`,
|
|
755
|
+
);
|
|
756
|
+
},
|
|
371
757
|
async ensureImmutableBranch(ref, sourceSha) {
|
|
372
758
|
const current = await api(
|
|
373
759
|
`/repos/${owner}/${repo}/git/ref/heads/${encodeRef(ref)}`,
|
|
@@ -396,6 +782,12 @@ export function createGitHubChannelCandidateClient({
|
|
|
396
782
|
})
|
|
397
783
|
);
|
|
398
784
|
},
|
|
785
|
+
async updatePullRequestBody(number, body) {
|
|
786
|
+
return api(`/repos/${owner}/${repo}/pulls/${number}`, {
|
|
787
|
+
method: "PATCH",
|
|
788
|
+
body: { body },
|
|
789
|
+
});
|
|
790
|
+
},
|
|
399
791
|
};
|
|
400
792
|
}
|
|
401
793
|
|
|
@@ -406,6 +798,10 @@ function markdown(result) {
|
|
|
406
798
|
`Eligible: \`${result.decision.eligible}\` (${result.decision.reason})`,
|
|
407
799
|
`Source: \`${result.decision.source.branch}@${result.decision.source.sha}\``,
|
|
408
800
|
`Target: \`${result.decision.target.branch}@${result.decision.target.sha}\``,
|
|
801
|
+
`Controller state: \`${result.controller.state}\``,
|
|
802
|
+
`Active candidate: ${result.controller.activeCandidate?.url || "none"}`,
|
|
803
|
+
`Next candidate: \`${result.controller.nextCandidate?.sourceSha || "none"}\``,
|
|
804
|
+
`Settlement action: \`${result.controller.settlementAction}\``,
|
|
409
805
|
`Dry run: \`${result.dryRun}\``,
|
|
410
806
|
`Pull request: ${result.pullRequest?.html_url || "not created"}`,
|
|
411
807
|
"",
|
|
@@ -428,6 +824,10 @@ async function main() {
|
|
|
428
824
|
"selected-sha": result.decision.source.sha,
|
|
429
825
|
"source-lock-ref": result.decision.sourceLockRef || "",
|
|
430
826
|
"promotion-pr": result.pullRequest?.html_url || "",
|
|
827
|
+
"controller-state": result.controller.state,
|
|
828
|
+
"active-candidate-pr": result.controller.activeCandidate?.url || "",
|
|
829
|
+
"next-candidate-sha": result.controller.nextCandidate?.sourceSha || "",
|
|
830
|
+
"settlement-action": result.controller.settlementAction,
|
|
431
831
|
};
|
|
432
832
|
fs.appendFileSync(
|
|
433
833
|
process.env.GITHUB_OUTPUT,
|
|
@@ -205,6 +205,8 @@ ${publicOutputs(outputs)}
|
|
|
205
205
|
|
|
206
206
|
permissions:
|
|
207
207
|
actions: write
|
|
208
|
+
artifact-metadata: write
|
|
209
|
+
attestations: write
|
|
208
210
|
checks: write
|
|
209
211
|
contents: write
|
|
210
212
|
id-token: write
|
|
@@ -406,6 +408,8 @@ jobs:
|
|
|
406
408
|
uses: kungfu-systems/buildchain/${alphaRoute.workflowPath}@${alphaRoute.callRef}
|
|
407
409
|
permissions:
|
|
408
410
|
actions: write
|
|
411
|
+
artifact-metadata: write
|
|
412
|
+
attestations: write
|
|
409
413
|
checks: write
|
|
410
414
|
contents: write
|
|
411
415
|
id-token: write
|
|
@@ -422,6 +426,8 @@ ${alphaForwarded}
|
|
|
422
426
|
uses: kungfu-systems/buildchain/${stableRoute.workflowPath}@${stableRoute.callRef}
|
|
423
427
|
permissions:
|
|
424
428
|
actions: write
|
|
429
|
+
artifact-metadata: write
|
|
430
|
+
attestations: write
|
|
425
431
|
checks: write
|
|
426
432
|
contents: write
|
|
427
433
|
id-token: write
|