@kungfu-tech/buildchain 2.11.14-alpha.0 → 2.11.14-alpha.1
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 +15 -10
- package/dist/site/kfd-claims.json +2 -2
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +2 -2
- package/dist/site/page-registry.json +9 -4
- package/dist/site/public-surface-audit.json +1 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +6 -6
- package/docs/release-flow.md +10 -1
- package/docs/release-governance.md +42 -0
- package/package.json +1 -1
- package/packages/core/stable-release-gate.js +284 -0
- package/scripts/check-inventory.mjs +2 -0
- package/scripts/stable-release-gate.mjs +295 -0
|
@@ -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-10T11:01:58.300Z",
|
|
5
|
+
"publishedAt": "2026-07-10T11:01:58.300Z",
|
|
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": "5a95cdcb4e1114635046a0765aec1ea1ecc60dcd",
|
|
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": "2.11.14-alpha.
|
|
40
|
+
"version": "2.11.14-alpha.1",
|
|
41
41
|
"versionSource": "package.json#version"
|
|
42
42
|
},
|
|
43
43
|
"entrypoint": "buildchain-site.json",
|
|
@@ -221,7 +221,7 @@
|
|
|
221
221
|
"path": "docs/release-governance.md",
|
|
222
222
|
"plane": "why",
|
|
223
223
|
"exists": true,
|
|
224
|
-
"digest": "sha256:
|
|
224
|
+
"digest": "sha256:f2eae65a8b678203da26e223bda9cffd7939852deea406f7328ce9e617da3c6f"
|
|
225
225
|
},
|
|
226
226
|
{
|
|
227
227
|
"id": "release-flow",
|
|
@@ -229,7 +229,7 @@
|
|
|
229
229
|
"path": "docs/release-flow.md",
|
|
230
230
|
"plane": "verify",
|
|
231
231
|
"exists": true,
|
|
232
|
-
"digest": "sha256:
|
|
232
|
+
"digest": "sha256:5b855ab3863e197b2ab257536b1f3ff85c79bca1fe962c45089b3f4c5ab81189"
|
|
233
233
|
},
|
|
234
234
|
{
|
|
235
235
|
"id": "runtime-train-validation",
|
package/docs/release-flow.md
CHANGED
|
@@ -12,6 +12,8 @@ flowchart TD
|
|
|
12
12
|
Review["Protected branch review"]
|
|
13
13
|
Merge["Merge PR into alpha or release"]
|
|
14
14
|
Promotion["Buildchain Ref Promotion"]
|
|
15
|
+
StableDecision{"Release channel?"}
|
|
16
|
+
StableGate["Stable only: exact-alpha canaries + soak + cooldown"]
|
|
15
17
|
Action["promote-buildchain-ref action"]
|
|
16
18
|
VersionState["Version-state commit"]
|
|
17
19
|
ExactTag["Exact tag"]
|
|
@@ -22,7 +24,10 @@ flowchart TD
|
|
|
22
24
|
Verify --> Review
|
|
23
25
|
Review --> Merge
|
|
24
26
|
Merge --> Promotion
|
|
25
|
-
Promotion -->
|
|
27
|
+
Promotion --> StableDecision
|
|
28
|
+
StableDecision -->|yes| StableGate
|
|
29
|
+
StableDecision -->|no: alpha| Action
|
|
30
|
+
StableGate --> Action
|
|
26
31
|
Action --> VersionState
|
|
27
32
|
Action --> ExactTag
|
|
28
33
|
Action --> FloatingRefs
|
|
@@ -33,6 +38,10 @@ flowchart TD
|
|
|
33
38
|
Buildchain treats the PR merge as release intent and the promotion action as the
|
|
34
39
|
only component allowed to turn that intent into release refs.
|
|
35
40
|
|
|
41
|
+
`StableGate` applies only to Buildchain's release channel. Alpha and train
|
|
42
|
+
iteration bypass it. See [Stable Release Throttle And Canary Gate](release-governance.md#stable-release-throttle-and-canary-gate)
|
|
43
|
+
for the versioned policy and evidence contract.
|
|
44
|
+
|
|
36
45
|
## Ref State
|
|
37
46
|
|
|
38
47
|
| Ref kind | Example | Mutability | Purpose |
|
|
@@ -206,6 +206,41 @@ The production channel and the test channel therefore intentionally diverge
|
|
|
206
206
|
after release: production stays on the release commit, while alpha/dev continue
|
|
207
207
|
at the next prerelease commit.
|
|
208
208
|
|
|
209
|
+
### Stable Release Throttle And Canary Gate
|
|
210
|
+
|
|
211
|
+
Buildchain's own stable channel has an additional pre-publication gate. It is
|
|
212
|
+
evaluated after the PR-stage release candidate has been resolved and before the
|
|
213
|
+
publish-gate ref, package registry, exact stable tag, or floating stable refs
|
|
214
|
+
are mutated. Train refs and alpha promotion do not execute this gate.
|
|
215
|
+
|
|
216
|
+
The policy is versioned in `.buildchain/stable-release-policy.json`. A stable
|
|
217
|
+
candidate is allowed only when all of these facts are true:
|
|
218
|
+
|
|
219
|
+
- the candidate resolves to an immutable exact alpha tag and its GitHub
|
|
220
|
+
prerelease timestamp;
|
|
221
|
+
- at least 24 hours have passed since the preceding stable patch on the same
|
|
222
|
+
minor line;
|
|
223
|
+
- the preceding stable-to-alpha comparison contains a product or public
|
|
224
|
+
contract path, not only version, test, evidence, or retrospective changes;
|
|
225
|
+
- the version-bound impact record has a non-empty summary and at least one
|
|
226
|
+
surface impact;
|
|
227
|
+
- the `Build Surface Fixture` release-candidate run succeeded;
|
|
228
|
+
- `site-libkungfu-dev` completed its no-apply `Buildchain Stable Canary` and an allowed
|
|
229
|
+
maintainer attested that successful run on the exact alpha SHA through the
|
|
230
|
+
`buildchain-canary/site-libkungfu-dev` commit-status context;
|
|
231
|
+
- at least one hour has elapsed after the last required canary completed.
|
|
232
|
+
|
|
233
|
+
The machine report is written to
|
|
234
|
+
`.buildchain/release-passport/stable-release-gate.json`. A passing report is
|
|
235
|
+
uploaded with the stable release passport. A blocked run writes the same report
|
|
236
|
+
before failing, so the missing or stale condition is inspectable without
|
|
237
|
+
opening a publication transaction.
|
|
238
|
+
|
|
239
|
+
The cooldown is a minimum interval, not an instruction to release every day.
|
|
240
|
+
Compatible work should still be batched until a stable release has a concrete
|
|
241
|
+
consumer need. Changing the interval, canary set, attestors, product path
|
|
242
|
+
boundary, or soak time is a reviewed policy change.
|
|
243
|
+
|
|
209
244
|
If release finalization is resumed after generated version-state bookkeeping was
|
|
210
245
|
partially applied, Buildchain applies the same recovery rule: the current
|
|
211
246
|
release head may be the generated commit, or a historical merge commit that
|
|
@@ -213,6 +248,13 @@ contains the recorded release material, existing exact tags and alpha/dev refs
|
|
|
213
248
|
are accepted when they match the transaction, and missing floating `vX.Y` or
|
|
214
249
|
`vX` tags are retried idempotently before completion.
|
|
215
250
|
|
|
251
|
+
Once the durable release transaction is `complete` and the exact/floating
|
|
252
|
+
stable refs have moved, next-alpha preparation is post-release bookkeeping. A
|
|
253
|
+
failure there records `deferred-post-release-bookkeeping` and
|
|
254
|
+
`next-anchor-required` instead of retroactively reporting the stable release as
|
|
255
|
+
failed. Generated next-alpha merges use the current dev tree as their base and
|
|
256
|
+
overlay only declared version-state paths, preserving concurrent dev changes.
|
|
257
|
+
|
|
216
258
|
## Major Gate Semantics
|
|
217
259
|
|
|
218
260
|
A major gate merge is:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kungfu-tech/buildchain",
|
|
3
|
-
"version": "2.11.14-alpha.
|
|
3
|
+
"version": "2.11.14-alpha.1",
|
|
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",
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const STABLE_RELEASE_POLICY_CONTRACT = "kungfu-buildchain-stable-release-policy";
|
|
5
|
+
export const STABLE_RELEASE_GATE_CONTRACT = "kungfu-buildchain-stable-release-gate";
|
|
6
|
+
|
|
7
|
+
function string(value = "") {
|
|
8
|
+
return String(value ?? "").trim();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function positiveInteger(value, label) {
|
|
12
|
+
const number = Number(value);
|
|
13
|
+
if (!Number.isInteger(number) || number < 0) {
|
|
14
|
+
throw new Error(`${label} must be a non-negative integer`);
|
|
15
|
+
}
|
|
16
|
+
return number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function timestamp(value, label) {
|
|
20
|
+
const normalized = string(value);
|
|
21
|
+
const milliseconds = Date.parse(normalized);
|
|
22
|
+
if (!normalized || !Number.isFinite(milliseconds)) {
|
|
23
|
+
throw new Error(`${label} must be an ISO-8601 timestamp`);
|
|
24
|
+
}
|
|
25
|
+
return { iso: new Date(milliseconds).toISOString(), milliseconds };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function loadJsonInput({ cwd = process.cwd(), input = "", label }) {
|
|
29
|
+
const normalized = string(input);
|
|
30
|
+
if (!normalized) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (normalized.startsWith("{")) {
|
|
34
|
+
return JSON.parse(normalized);
|
|
35
|
+
}
|
|
36
|
+
const resolved = path.isAbsolute(normalized) ? normalized : path.resolve(cwd, normalized);
|
|
37
|
+
if (!fs.existsSync(resolved)) {
|
|
38
|
+
throw new Error(`${label} path does not exist: ${normalized}`);
|
|
39
|
+
}
|
|
40
|
+
return JSON.parse(fs.readFileSync(resolved, "utf8"));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function loadStableReleasePolicy({ cwd = process.cwd(), input = "" } = {}) {
|
|
44
|
+
const policy = loadJsonInput({ cwd, input, label: "stable release policy" });
|
|
45
|
+
if (!policy) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
if (policy.contract !== STABLE_RELEASE_POLICY_CONTRACT) {
|
|
49
|
+
throw new Error(`stable release policy contract must be ${STABLE_RELEASE_POLICY_CONTRACT}`);
|
|
50
|
+
}
|
|
51
|
+
if (Number(policy.schemaVersion) !== 1) {
|
|
52
|
+
throw new Error("stable release policy schemaVersion must be 1");
|
|
53
|
+
}
|
|
54
|
+
const minimumStableIntervalSeconds = positiveInteger(
|
|
55
|
+
policy.minimumStableIntervalSeconds,
|
|
56
|
+
"minimumStableIntervalSeconds",
|
|
57
|
+
);
|
|
58
|
+
const minimumCanarySoakSeconds = positiveInteger(
|
|
59
|
+
policy.minimumCanarySoakSeconds,
|
|
60
|
+
"minimumCanarySoakSeconds",
|
|
61
|
+
);
|
|
62
|
+
const productPathPrefixes = [...new Set(
|
|
63
|
+
(Array.isArray(policy.productPathPrefixes) ? policy.productPathPrefixes : [])
|
|
64
|
+
.map(string)
|
|
65
|
+
.filter(Boolean),
|
|
66
|
+
)];
|
|
67
|
+
if (productPathPrefixes.length === 0) {
|
|
68
|
+
throw new Error("stable release policy requires productPathPrefixes[]");
|
|
69
|
+
}
|
|
70
|
+
const requiredCanaries = (Array.isArray(policy.requiredCanaries) ? policy.requiredCanaries : [])
|
|
71
|
+
.map((canary, index) => {
|
|
72
|
+
const id = string(canary?.id);
|
|
73
|
+
const source = string(canary?.source);
|
|
74
|
+
if (!id) {
|
|
75
|
+
throw new Error(`requiredCanaries[${index}].id is required`);
|
|
76
|
+
}
|
|
77
|
+
if (!new Set(["release-candidate", "commit-status"]).has(source)) {
|
|
78
|
+
throw new Error(`requiredCanaries[${index}].source must be release-candidate or commit-status`);
|
|
79
|
+
}
|
|
80
|
+
if (source === "commit-status" && !string(canary.context)) {
|
|
81
|
+
throw new Error(`requiredCanaries[${index}].context is required for commit-status canaries`);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
id,
|
|
85
|
+
source,
|
|
86
|
+
repository: string(canary.repository),
|
|
87
|
+
workflow: string(canary.workflow),
|
|
88
|
+
context: string(canary.context),
|
|
89
|
+
allowedAttestors: [...new Set(
|
|
90
|
+
(Array.isArray(canary.allowedAttestors) ? canary.allowedAttestors : [])
|
|
91
|
+
.map(string)
|
|
92
|
+
.filter(Boolean),
|
|
93
|
+
)],
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
if (requiredCanaries.length === 0) {
|
|
97
|
+
throw new Error("stable release policy requires requiredCanaries[]");
|
|
98
|
+
}
|
|
99
|
+
if (new Set(requiredCanaries.map((canary) => canary.id)).size !== requiredCanaries.length) {
|
|
100
|
+
throw new Error("stable release policy canary ids must be unique");
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
schemaVersion: 1,
|
|
104
|
+
contract: STABLE_RELEASE_POLICY_CONTRACT,
|
|
105
|
+
enabled: policy.enabled !== false,
|
|
106
|
+
minimumStableIntervalSeconds,
|
|
107
|
+
minimumCanarySoakSeconds,
|
|
108
|
+
productPathPrefixes,
|
|
109
|
+
requiredCanaries,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function check(ok, id, message, details = {}) {
|
|
114
|
+
return { id, status: ok ? "pass" : "fail", message, details };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function matchesProductPath(file, prefixes) {
|
|
118
|
+
return prefixes.some((prefix) => file === prefix || file.startsWith(prefix));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function evaluateStableReleaseGate({
|
|
122
|
+
policy,
|
|
123
|
+
channel = "",
|
|
124
|
+
candidate = {},
|
|
125
|
+
previousStable = undefined,
|
|
126
|
+
changedPaths = [],
|
|
127
|
+
impact = {},
|
|
128
|
+
canaries = [],
|
|
129
|
+
now = new Date().toISOString(),
|
|
130
|
+
} = {}) {
|
|
131
|
+
const normalizedChannel = string(channel);
|
|
132
|
+
if (!policy || policy.enabled === false || normalizedChannel !== "release") {
|
|
133
|
+
return {
|
|
134
|
+
schemaVersion: 1,
|
|
135
|
+
contract: STABLE_RELEASE_GATE_CONTRACT,
|
|
136
|
+
applies: false,
|
|
137
|
+
ok: true,
|
|
138
|
+
channel: normalizedChannel,
|
|
139
|
+
checks: [],
|
|
140
|
+
summary: { reason: !policy ? "policy-not-configured" : policy.enabled === false ? "policy-disabled" : "non-stable-channel" },
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const nowTime = timestamp(now, "now");
|
|
145
|
+
const candidatePublished = timestamp(candidate.publishedAt, "candidate.publishedAt");
|
|
146
|
+
const candidateSha = string(candidate.sha);
|
|
147
|
+
const candidateTag = string(candidate.tag);
|
|
148
|
+
const checks = [];
|
|
149
|
+
checks.push(check(/^[0-9a-f]{40}$/i.test(candidateSha), "candidate.sha", "candidate alpha SHA is exact", { sha: candidateSha }));
|
|
150
|
+
checks.push(check(/-alpha\.\d+$/.test(candidateTag), "candidate.tag", "candidate is an exact alpha tag", { tag: candidateTag }));
|
|
151
|
+
|
|
152
|
+
if (previousStable) {
|
|
153
|
+
const previousPublished = timestamp(previousStable.publishedAt, "previousStable.publishedAt");
|
|
154
|
+
const elapsedSeconds = Math.floor((nowTime.milliseconds - previousPublished.milliseconds) / 1000);
|
|
155
|
+
checks.push(check(
|
|
156
|
+
elapsedSeconds >= policy.minimumStableIntervalSeconds,
|
|
157
|
+
"stable.minimum_interval",
|
|
158
|
+
"minimum interval since the previous stable release is satisfied",
|
|
159
|
+
{
|
|
160
|
+
previousTag: string(previousStable.tag),
|
|
161
|
+
previousPublishedAt: previousPublished.iso,
|
|
162
|
+
elapsedSeconds,
|
|
163
|
+
requiredSeconds: policy.minimumStableIntervalSeconds,
|
|
164
|
+
},
|
|
165
|
+
));
|
|
166
|
+
} else {
|
|
167
|
+
checks.push(check(true, "stable.minimum_interval", "no previous stable release exists", { firstStable: true }));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const normalizedChangedPaths = [...new Set(changedPaths.map(string).filter(Boolean))].sort();
|
|
171
|
+
const productChangedPaths = normalizedChangedPaths.filter((file) =>
|
|
172
|
+
matchesProductPath(file, policy.productPathPrefixes));
|
|
173
|
+
checks.push(check(
|
|
174
|
+
productChangedPaths.length > 0,
|
|
175
|
+
"stable.product_diff",
|
|
176
|
+
"candidate contains a product or contract difference from the current stable release",
|
|
177
|
+
{ changedPaths: productChangedPaths, comparedPathCount: normalizedChangedPaths.length },
|
|
178
|
+
));
|
|
179
|
+
|
|
180
|
+
const surfaceImpacts = Array.isArray(impact?.surfaceImpacts) ? impact.surfaceImpacts : [];
|
|
181
|
+
checks.push(check(
|
|
182
|
+
surfaceImpacts.length > 0 && string(impact?.summary) !== "",
|
|
183
|
+
"stable.impact",
|
|
184
|
+
"version-bound impact declares a summary and at least one surface",
|
|
185
|
+
{ summary: string(impact?.summary), surfaceIds: surfaceImpacts.map((entry) => string(entry?.id)).filter(Boolean) },
|
|
186
|
+
));
|
|
187
|
+
|
|
188
|
+
const canaryById = new Map(canaries.map((canary) => [string(canary?.id), canary]));
|
|
189
|
+
const canaryCompletionTimes = [];
|
|
190
|
+
for (const required of policy.requiredCanaries) {
|
|
191
|
+
const evidence = canaryById.get(required.id);
|
|
192
|
+
const completedAt = evidence?.completedAt
|
|
193
|
+
? timestamp(evidence.completedAt, `canary ${required.id} completedAt`)
|
|
194
|
+
: undefined;
|
|
195
|
+
const attestorAllowed = required.allowedAttestors.length === 0 ||
|
|
196
|
+
required.allowedAttestors.includes(string(evidence?.attestor));
|
|
197
|
+
const valid = Boolean(
|
|
198
|
+
evidence &&
|
|
199
|
+
string(evidence.status) === "success" &&
|
|
200
|
+
string(evidence.candidateSha) === candidateSha &&
|
|
201
|
+
completedAt &&
|
|
202
|
+
completedAt.milliseconds >= candidatePublished.milliseconds &&
|
|
203
|
+
attestorAllowed,
|
|
204
|
+
);
|
|
205
|
+
if (completedAt) {
|
|
206
|
+
canaryCompletionTimes.push(completedAt.milliseconds);
|
|
207
|
+
}
|
|
208
|
+
checks.push(check(
|
|
209
|
+
valid,
|
|
210
|
+
`stable.canary.${required.id}`,
|
|
211
|
+
`required canary ${required.id} passed for the exact alpha candidate`,
|
|
212
|
+
{
|
|
213
|
+
source: required.source,
|
|
214
|
+
context: required.context,
|
|
215
|
+
repository: string(evidence?.repository || required.repository),
|
|
216
|
+
workflow: string(evidence?.workflow || required.workflow),
|
|
217
|
+
runtimeRef: string(evidence?.runtimeRef),
|
|
218
|
+
candidateSha: string(evidence?.candidateSha),
|
|
219
|
+
completedAt: completedAt?.iso || "",
|
|
220
|
+
evidenceUrl: string(evidence?.evidenceUrl),
|
|
221
|
+
attestor: string(evidence?.attestor),
|
|
222
|
+
attestorAllowed,
|
|
223
|
+
},
|
|
224
|
+
));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const soakStartMilliseconds = Math.max(candidatePublished.milliseconds, ...canaryCompletionTimes);
|
|
228
|
+
const soakElapsedSeconds = Math.floor((nowTime.milliseconds - soakStartMilliseconds) / 1000);
|
|
229
|
+
checks.push(check(
|
|
230
|
+
canaryCompletionTimes.length === policy.requiredCanaries.length &&
|
|
231
|
+
soakElapsedSeconds >= policy.minimumCanarySoakSeconds,
|
|
232
|
+
"stable.canary_soak",
|
|
233
|
+
"minimum soak interval after the final required canary is satisfied",
|
|
234
|
+
{
|
|
235
|
+
soakStartedAt: new Date(soakStartMilliseconds).toISOString(),
|
|
236
|
+
elapsedSeconds: soakElapsedSeconds,
|
|
237
|
+
requiredSeconds: policy.minimumCanarySoakSeconds,
|
|
238
|
+
},
|
|
239
|
+
));
|
|
240
|
+
|
|
241
|
+
const ok = checks.every((entry) => entry.status === "pass");
|
|
242
|
+
return {
|
|
243
|
+
schemaVersion: 1,
|
|
244
|
+
contract: STABLE_RELEASE_GATE_CONTRACT,
|
|
245
|
+
applies: true,
|
|
246
|
+
ok,
|
|
247
|
+
channel: normalizedChannel,
|
|
248
|
+
evaluatedAt: nowTime.iso,
|
|
249
|
+
candidate: {
|
|
250
|
+
tag: candidateTag,
|
|
251
|
+
sha: candidateSha,
|
|
252
|
+
publishedAt: candidatePublished.iso,
|
|
253
|
+
},
|
|
254
|
+
previousStable: previousStable
|
|
255
|
+
? {
|
|
256
|
+
tag: string(previousStable.tag),
|
|
257
|
+
sha: string(previousStable.sha),
|
|
258
|
+
publishedAt: timestamp(previousStable.publishedAt, "previousStable.publishedAt").iso,
|
|
259
|
+
}
|
|
260
|
+
: undefined,
|
|
261
|
+
policy: {
|
|
262
|
+
minimumStableIntervalSeconds: policy.minimumStableIntervalSeconds,
|
|
263
|
+
minimumCanarySoakSeconds: policy.minimumCanarySoakSeconds,
|
|
264
|
+
requiredCanaries: policy.requiredCanaries.map((canary) => canary.id),
|
|
265
|
+
},
|
|
266
|
+
checks,
|
|
267
|
+
summary: {
|
|
268
|
+
decision: ok ? "allow" : "block",
|
|
269
|
+
failedChecks: checks.filter((entry) => entry.status === "fail").map((entry) => entry.id),
|
|
270
|
+
productChangedPaths,
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function assertStableReleaseGate(input = {}) {
|
|
276
|
+
const report = evaluateStableReleaseGate(input);
|
|
277
|
+
if (!report.ok) {
|
|
278
|
+
throw Object.assign(
|
|
279
|
+
new Error(`stable release gate blocked promotion: ${report.summary.failedChecks.join(", ")}`),
|
|
280
|
+
{ report },
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
return report;
|
|
284
|
+
}
|
|
@@ -725,6 +725,8 @@ for (const requiredSnippet of [
|
|
|
725
725
|
"publish-source-ref: ${{ steps.publish-gate.outputs.ref }}",
|
|
726
726
|
"publish-source-sha: ${{ steps.publish-gate.outputs.sha }}",
|
|
727
727
|
"publish-source-locked: ${{ steps.publish-gate.outputs.locked }}",
|
|
728
|
+
"Enforce Buildchain stable release canary gate",
|
|
729
|
+
"BUILDCHAIN_STABLE_RELEASE_POLICY: .buildchain/stable-release-policy.json",
|
|
728
730
|
]) {
|
|
729
731
|
if (!releaseCandidatePromoteWorkflow.includes(requiredSnippet)) {
|
|
730
732
|
throw new Error(`release candidate promote workflow missing KFD gate pass-through: ${requiredSnippet}`);
|