@kungfu-tech/buildchain 2.12.0 → 2.12.1-alpha.2

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.
@@ -489,6 +489,19 @@
489
489
  "capabilityGroup": "release-passport-trust",
490
490
  "status": "active"
491
491
  },
492
+ {
493
+ "id": "buildchain-stable-candidate-patrol",
494
+ "path": ".github/workflows/buildchain-stable-candidate-patrol.yml",
495
+ "inputs": [
496
+ "buildchain-ref",
497
+ "dry-run",
498
+ "release-now"
499
+ ],
500
+ "inputCount": 3,
501
+ "surface": "repository-patrol",
502
+ "capabilityGroup": "governance-versioning",
503
+ "status": "active"
504
+ },
492
505
  {
493
506
  "id": "candidate-lab",
494
507
  "path": ".github/workflows/candidate-lab.yml",
@@ -805,6 +818,31 @@
805
818
  "capabilityGroup": "api-cli-reference",
806
819
  "status": "compatibility-fixture"
807
820
  },
821
+ {
822
+ "id": "stable-candidate-patrol",
823
+ "path": ".github/workflows/stable-candidate-patrol.yml",
824
+ "inputs": [
825
+ "auto-approve",
826
+ "auto-merge",
827
+ "auto-promote",
828
+ "buildchain-ref",
829
+ "buildchain-repository",
830
+ "dry-run",
831
+ "hold",
832
+ "hold-reason",
833
+ "ledger-ref",
834
+ "minimum-soak-seconds",
835
+ "release-now",
836
+ "required-checks",
837
+ "revoke-reason",
838
+ "revoked-versions",
839
+ "target-branch"
840
+ ],
841
+ "inputCount": 15,
842
+ "surface": "repository-patrol",
843
+ "capabilityGroup": "governance-versioning",
844
+ "status": "active"
845
+ },
808
846
  {
809
847
  "id": "verify",
810
848
  "path": ".github/workflows/verify.yml",
package/docs/MAP.md CHANGED
@@ -99,6 +99,7 @@ replace them.
99
99
  | How do self-hosted runners relay large artifacts through S3 before GitHub artifacts? | [`reusable-build-surface.md`](reusable-build-surface.md#artifact-transfer-relay) | use | stable |
100
100
  | How do self-hosted runners reuse local Git checkout caches without weakening source locks? | [`reusable-build-surface.md`](reusable-build-surface.md#locked-source-checkout-cache) | use | stable |
101
101
  | How do I validate an unreleased Buildchain runtime train while keeping `@v2`? | [`runtime-train-validation.md`](runtime-train-validation.md) | use | stable |
102
+ | How do I automatically qualify alpha candidates and publish the newest non-revoked qualified candidate at a fixed window? | [`stable-candidate-patrol.md`](stable-candidate-patrol.md) | use | preview |
102
103
  | How do I deploy a site/app preview, staging, or production surface? | [`web-surface-deployments.md`](web-surface-deployments.md) | use | stable |
103
104
  | How do I publish observed infrastructure contracts for downstream consumers? | [`infra-contract.md`](infra-contract.md) | use | preview |
104
105
  | How do I use the active actions directly? | [`../actions/validate-config/README.md`](../actions/validate-config/README.md), [`../actions/run-lifecycle/README.md`](../actions/run-lifecycle/README.md), [`../actions/promote-buildchain-ref/README.md`](../actions/promote-buildchain-ref/README.md), [`../actions/report-buildchain-issue/README.md`](../actions/report-buildchain-issue/README.md) | use | stable |
@@ -117,6 +118,8 @@ replace them.
117
118
  [`release-flow.md`](release-flow.md).
118
119
  - **Buildchain self-dogfood / released alpha canary / stable compatibility lane** ->
119
120
  [`release-governance.md`](release-governance.md#buildchain-alpha-self-dogfood).
121
+ - **qualified alpha ledger / scheduled stable selection / hold and revoke** ->
122
+ [`stable-candidate-patrol.md`](stable-candidate-patrol.md).
120
123
  - **v2.1 vs v2.2 / when to open a new minor line** ->
121
124
  [`versioning.md`](versioning.md).
122
125
  - **dry-run / what would happen if this channel PR merges** -> [`cli.md`](cli.md)
@@ -303,6 +303,15 @@ Compatible work should still be batched until a stable release has a concrete
303
303
  consumer need. Changing the interval, canary set, attestors, product path
304
304
  boundary, or soak time is a reviewed policy change.
305
305
 
306
+ Repositories that want a predictable scheduled stable window can use
307
+ [`Stable Candidate Patrol`](stable-candidate-patrol.md). It persists each exact
308
+ alpha independently, qualifies it after repository-declared checks and soak,
309
+ and selects the newest qualified non-revoked candidate. A newer soaking alpha
310
+ does not invalidate an older qualified candidate. The selected tree enters the
311
+ existing strict `publish-gate/release/<line>/<version> -> release/<line>` PR
312
+ path, so scheduled selection changes release intent timing without weakening
313
+ source locks, review, verification, publish transactions, or passports.
314
+
306
315
  If release finalization is resumed after generated version-state bookkeeping was
307
316
  partially applied, Buildchain applies the same recovery rule: the current
308
317
  release head may be the generated commit, or a historical merge commit that
@@ -445,6 +454,7 @@ patrol workflow family:
445
454
  | `.github/workflows/patrol-daily.yml` | daily | lightweight inspection plus ready dev PR maintenance |
446
455
  | `.github/workflows/patrol-weekly.yml` | weekly | release-state, passport, gate, and stale-state health checks as they are added |
447
456
  | `.github/workflows/patrol-monthly.yml` | monthly | governance, permission, branch-protection, and workflow drift checks as they are added |
457
+ | `.github/workflows/stable-candidate-patrol.yml` | repository-selected release window | qualify immutable alpha candidates and open the exact source-lock stable PR |
448
458
 
449
459
  The cadence names describe patrol intensity, not release cadence:
450
460
 
@@ -453,6 +463,12 @@ The cadence names describe patrol intensity, not release cadence:
453
463
  - monthly patrol is for structural drift checks that should not block ordinary
454
464
  development velocity.
455
465
 
466
+ Stable Candidate Patrol is separate from those maintenance cadences because its
467
+ caller-owned cron is a release-intent window. Its candidate ledger and selection
468
+ remain generic; registry-specific side effects still run through the normal
469
+ repository `lifecycle.publish` transaction. See
470
+ [`stable-candidate-patrol.md`](stable-candidate-patrol.md).
471
+
456
472
  Consumers should schedule thin callers and keep their YAML declarative. For
457
473
  example:
458
474
 
@@ -0,0 +1,172 @@
1
+ ---
2
+ status: preview
3
+ period: ongoing
4
+ theme: stable-candidate-patrol
5
+ doc_type: architecture-and-usage
6
+ source_level: local-files
7
+ confidence: high
8
+ sensitivity: public
9
+ evidence_grade: A
10
+ review_state: self-reviewed
11
+ last_reviewed: 2026-07-11
12
+ ai_provenance:
13
+ model_family: GPT-5
14
+ product: Codex
15
+ generated_at: 2026-07-11
16
+ visible_context: Buildchain candidate passport, stable gate, Patrol, publish transaction, exact source-lock PR contracts, tests, and user consensus.
17
+ invisible_context_boundary: No credentials, private logs, or unpublished consumer content were used.
18
+ ---
19
+
20
+ # Stable Candidate Patrol
21
+
22
+ Buildchain can treat every exact alpha as an independent stable candidate. A
23
+ new alpha creates a new candidate; it does not silently revoke an older alpha
24
+ that already completed its checks and soak interval.
25
+
26
+ ## Candidate lifecycle
27
+
28
+ The durable ledger contract is `kungfu-buildchain-stable-candidate-ledger`:
29
+
30
+ ```text
31
+ registered -> soaking -> qualified -> promoted
32
+ \-> revoked
33
+ ```
34
+
35
+ Each entry binds an exact alpha version to one immutable commit SHA. Registering
36
+ the same version at another SHA fails closed. Qualification records the required
37
+ checks, their completion times, the derived soak start, and elapsed time.
38
+
39
+ Scheduled selection chooses the newest `qualified` candidate that is neither
40
+ `revoked` nor already `promoted`. A newer alpha that is still `soaking` does not
41
+ hide an older qualified candidate. Once a stable version is promoted, remaining
42
+ alphas for that exact stable version are closed because that immutable stable
43
+ version has been consumed; their later product changes continue through the
44
+ next patch alpha prepared by the normal release transaction.
45
+
46
+ ## Repository policy
47
+
48
+ Declare the default once in `.buildchain/buildchain.toml`:
49
+
50
+ ```toml
51
+ [release.stable]
52
+ strategy = "latest-qualified-alpha"
53
+ timezone = "Asia/Shanghai"
54
+ publish_at = "03:00"
55
+ minimum_soak_seconds = 3600
56
+ required_checks = [
57
+ "alpha-release",
58
+ "workflow:Build",
59
+ "status:buildchain-canary/consumer",
60
+ ]
61
+ auto_promote = true
62
+ auto_merge = true
63
+ ```
64
+
65
+ Check identifiers use these forms:
66
+
67
+ - `alpha-release`: the exact GitHub prerelease publication fact;
68
+ - `workflow:<name>`: a successful Actions workflow run on the exact candidate SHA;
69
+ - `status:<context>`: a successful commit status on the exact candidate SHA;
70
+ - an unprefixed value: an exact status context or check-run name.
71
+
72
+ Candidate discovery uses immutable exact alpha Git tags. When a repository also
73
+ publishes GitHub prereleases, `alpha-release` binds qualification to that public
74
+ release fact. Repositories that intentionally disable GitHub Releases omit
75
+ `alpha-release` and declare their own exact-SHA workflow/status evidence; tag
76
+ commit time remains the earliest possible soak start.
77
+
78
+ `publish_at` and `timezone` are the auditable policy declaration. GitHub only
79
+ starts scheduled workflows from caller-owned cron, so the thin caller keeps the
80
+ matching UTC trigger:
81
+
82
+ ```yaml
83
+ name: Stable Candidate Patrol
84
+
85
+ on:
86
+ schedule:
87
+ - cron: "0 19 * * *" # 03:00 Asia/Shanghai
88
+ workflow_dispatch:
89
+ inputs:
90
+ release-now:
91
+ description: Exact alpha selected by explicit human authority
92
+ required: false
93
+ default: ""
94
+
95
+ permissions:
96
+ contents: write
97
+ pull-requests: write
98
+ checks: read
99
+ statuses: read
100
+
101
+ jobs:
102
+ stable:
103
+ uses: kungfu-systems/buildchain/.github/workflows/stable-candidate-patrol.yml@v2
104
+ with:
105
+ release-now: ${{ inputs.release-now }}
106
+ dry-run: false
107
+ secrets:
108
+ promotion-token: ${{ secrets.BUILDCHAIN_PROMOTION_TOKEN }}
109
+ ```
110
+
111
+ The promotion token must be repository-owned and capable of creating the
112
+ machine ledger branch, exact source-lock branch, and pull request. Repositories
113
+ that require an approving review can set `auto-approve: true` only when their
114
+ GitHub Actions policy explicitly allows the caller token to approve the PR;
115
+ otherwise a repository-owned App or review bot supplies the approval. The
116
+ generated PR may use auto-merge, but it never bypasses the target branch checks.
117
+
118
+ ## Exact-source stable promotion
119
+
120
+ For a selected `2.12.0-alpha.4`, Patrol creates the immutable source branch:
121
+
122
+ ```text
123
+ publish-gate/release/v2/v2.12/2.12.0-alpha.4
124
+ ```
125
+
126
+ and opens it against `release/v2/v2.12`. This is an existing strict Buildchain
127
+ governance path. The PR freezes the qualified candidate even if `v2.12-alpha`
128
+ or `alpha/v2/v2.12` has already moved to alpha.5. Normal Verify,
129
+ release-candidate resolution, source-tree equivalence, publish transaction,
130
+ passport, registry, tag, and floating-ref checks still run.
131
+
132
+ ## Hold, revoke, and immediate release
133
+
134
+ Persistent repository controls can be supplied as reusable-workflow inputs or
135
+ repository variables:
136
+
137
+ ```text
138
+ BUILDCHAIN_STABLE_HOLD=true
139
+ BUILDCHAIN_STABLE_HOLD_REASON=release freeze
140
+ BUILDCHAIN_STABLE_REVOKED_ALPHA_VERSIONS=2.12.0-alpha.5,2.12.0-alpha.7
141
+ BUILDCHAIN_STABLE_REVOKE_REASON=consumer regression
142
+ ```
143
+
144
+ Revocation is explicit evidence; publishing a newer alpha alone is not
145
+ revocation. A manual `workflow_dispatch` `release-now` chooses one exact,
146
+ non-revoked candidate immediately. It may bypass the scheduled soak decision,
147
+ but cannot change candidate SHA, reuse a consumed stable version, or bypass the
148
+ source-lock PR and publish transaction.
149
+
150
+ For Buildchain's own stable gate, Patrol automatically projects that explicit
151
+ human decision into the exact-candidate `BUILDCHAIN_STABLE_RELEASE_NOW` and
152
+ reason variables. A later Patrol run removes them after it observes the public
153
+ stable release. This is an internal compatibility projection, not a manual user
154
+ step; the durable authority record remains the candidate ledger entry and PR.
155
+
156
+ ## Durable recovery
157
+
158
+ The default ledger ref is derived from the release line, for example:
159
+
160
+ ```text
161
+ buildchain/candidate-ledger/v2/v2.12
162
+ ```
163
+
164
+ It stores `.buildchain/stable-candidate-ledger.json`. Patrol runs are serialized
165
+ per repository and release line. Repeated runs reuse the same exact source-lock
166
+ branch and PR, while later runs observe the public stable release and mark the
167
+ candidate `promoted`.
168
+
169
+ When Patrol is enabled after a stable version already exists, it reconstructs
170
+ that consumed patch from GitHub Release truth and closes historical alpha
171
+ candidates for the same stable version. It never attempts to republish an
172
+ already claimed exact stable version.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "2.12.0",
3
+ "version": "2.12.1-alpha.2",
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",
@@ -30,6 +30,7 @@
30
30
  "./publication-package": "./packages/core/publication-package.js",
31
31
  "./kfd-gate": "./packages/core/kfd-gate.js",
32
32
  "./release-candidate": "./packages/core/release-candidate.js",
33
+ "./stable-candidate-ledger": "./packages/core/stable-candidate-ledger.js",
33
34
  "./release-passport": "./packages/core/release-passport.js",
34
35
  "./release-propagation": "./packages/core/release-propagation.js",
35
36
  "./surface-manifest": "./packages/core/surface-manifest.js",
@@ -19,6 +19,7 @@ const SUPPORTED_PUBLISH_MODES = new Set(["publish-final-version", "promote-exist
19
19
  const SUPPORTED_PUBLISH_AUTH = new Set(["trusted-publishing", "npm-token"]);
20
20
  const SUPPORTED_PUBLISH_KINDS = new Set(["npm-package", "npm-paper-package"]);
21
21
  const SUPPORTED_PACKAGE_SET_ORDER = new Set(["as-provided", "platforms-first-main-last"]);
22
+ const SUPPORTED_STABLE_RELEASE_STRATEGIES = new Set(["manual", "latest-qualified-alpha"]);
22
23
  const SUPPORTED_NATIVE_COMPILER_CACHE = new Set(["auto", "ccache", "sccache", "none"]);
23
24
  const WEB_SURFACE_CHANNELS = ["preview", "staging", "production"];
24
25
  const SUPPORTED_CHANNEL_VISIBILITY = new Set(["ephemeral", "protected", "public", "internal"]);
@@ -180,6 +181,9 @@ export function normalizeBuildchainConfig(config) {
180
181
  if (normalized.publish !== undefined) {
181
182
  normalized.publish = normalizePublishSection(normalized.publish);
182
183
  }
184
+ if (normalized.release !== undefined) {
185
+ normalized.release = normalizeReleaseSection(normalized.release);
186
+ }
183
187
  if (normalized.channels !== undefined) {
184
188
  normalized.channels = normalizeChannelsSection(normalized.channels, normalized.project);
185
189
  }
@@ -215,6 +219,59 @@ export function normalizeBuildchainConfig(config) {
215
219
  return normalized;
216
220
  }
217
221
 
222
+ function normalizeReleaseSection(release) {
223
+ assertPlainObject(release, "release");
224
+ const normalized = { ...release };
225
+ if (release.stable !== undefined) {
226
+ assertPlainObject(release.stable, "release.stable");
227
+ const strategy = release.stable.strategy === undefined
228
+ ? "manual"
229
+ : assertString(release.stable.strategy, "release.stable.strategy");
230
+ if (!SUPPORTED_STABLE_RELEASE_STRATEGIES.has(strategy)) {
231
+ throw new Error("release.stable.strategy must be one of manual or latest-qualified-alpha");
232
+ }
233
+ const minimumSoakSeconds = release.stable.minimum_soak_seconds === undefined
234
+ ? 3600
235
+ : Number(release.stable.minimum_soak_seconds);
236
+ if (!Number.isInteger(minimumSoakSeconds) || minimumSoakSeconds < 0) {
237
+ throw new Error("release.stable.minimum_soak_seconds must be a non-negative integer");
238
+ }
239
+ normalized.stable = {
240
+ strategy,
241
+ timezone: release.stable.timezone === undefined
242
+ ? "UTC"
243
+ : assertString(release.stable.timezone, "release.stable.timezone"),
244
+ publishAt: release.stable.publish_at === undefined
245
+ ? "03:00"
246
+ : assertString(release.stable.publish_at, "release.stable.publish_at"),
247
+ minimumSoakSeconds,
248
+ requiredChecks: normalizeStringArray(release.stable.required_checks, "release.stable.required_checks"),
249
+ ledgerRef: release.stable.ledger_ref === undefined
250
+ ? ""
251
+ : assertString(release.stable.ledger_ref, "release.stable.ledger_ref"),
252
+ autoPromote: optionalBoolean(release.stable.auto_promote, strategy === "latest-qualified-alpha"),
253
+ autoMerge: optionalBoolean(release.stable.auto_merge, false),
254
+ };
255
+ if (!/^([01]\d|2[0-3]):[0-5]\d$/.test(normalized.stable.publishAt)) {
256
+ throw new Error("release.stable.publish_at must be HH:MM in 24-hour time");
257
+ }
258
+ }
259
+ return normalized;
260
+ }
261
+
262
+ export function getStableReleasePolicy(loadedConfig) {
263
+ return loadedConfig?.config?.release?.stable || {
264
+ strategy: "manual",
265
+ timezone: "UTC",
266
+ publishAt: "03:00",
267
+ minimumSoakSeconds: 3600,
268
+ requiredChecks: [],
269
+ ledgerRef: "",
270
+ autoPromote: false,
271
+ autoMerge: false,
272
+ };
273
+ }
274
+
218
275
  function normalizeFactsSection(facts) {
219
276
  assertPlainObject(facts, "facts");
220
277
  return {
@@ -1389,6 +1446,7 @@ export function validateBuildchainConfig(
1389
1446
  })),
1390
1447
  lifecycleStages,
1391
1448
  publish: loadedConfig.config.publish,
1449
+ release: loadedConfig.config.release,
1392
1450
  facts: loadedConfig.config.facts,
1393
1451
  publication: loadedConfig.config.publication,
1394
1452
  };
@@ -3,6 +3,7 @@ export {
3
3
  getLifecycleStage,
4
4
  getNativeDiagnosticsProfile,
5
5
  getPublishContract,
6
+ getStableReleasePolicy,
6
7
  getVersionStrategy,
7
8
  loadBuildchainConfig,
8
9
  loadConfiguredAnchorManifest,
@@ -13,6 +14,20 @@ export {
13
14
  validateBuildchainConfig,
14
15
  } from "./buildchain-config.js";
15
16
 
17
+ export {
18
+ STABLE_CANDIDATE_LEDGER_CONTRACT,
19
+ STABLE_CANDIDATE_STATES,
20
+ createStableCandidateLedger,
21
+ markStableCandidatePromoted,
22
+ normalizeStableCandidateLedger,
23
+ qualifyStableCandidate,
24
+ registerStableCandidate,
25
+ revokeStableCandidate,
26
+ selectStableCandidate,
27
+ setStableCandidateHold,
28
+ stableCandidatePromotionRefs,
29
+ } from "./stable-candidate-ledger.js";
30
+
16
31
  export {
17
32
  assertPackageManager,
18
33
  commandForKungfuUpgrade,