@kungfu-tech/buildchain 2.5.5 → 2.5.6
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/bin/buildchain.mjs +2 -0
- package/docs/cli.md +12 -0
- package/docs/release-passport.md +13 -0
- package/docs/versioning.md +33 -12
- package/package.json +1 -1
- package/packages/core/release-passport.js +173 -6
- package/scripts/check-inventory.mjs +3 -1
- package/scripts/release-candidate-resolver.mjs +34 -14
package/bin/buildchain.mjs
CHANGED
|
@@ -67,6 +67,7 @@ function usage() {
|
|
|
67
67
|
[--trusted-publishing-json <json-or-path>]
|
|
68
68
|
[--transaction-json <json-or-path>]
|
|
69
69
|
[--anchor-manifest-json <json-or-path>]
|
|
70
|
+
[--impact-json <json-or-path>]
|
|
70
71
|
[--build-summary-json <json-or-path>]
|
|
71
72
|
[--platform-manifest-json <json-or-path>]...
|
|
72
73
|
[--dist-tag-evidence-json <json-or-path>]
|
|
@@ -716,6 +717,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
716
717
|
trustedPublishingJson: readFlag(collectArgs, "trusted-publishing-json", ""),
|
|
717
718
|
transactionJson: readFlag(collectArgs, "transaction-json", ""),
|
|
718
719
|
anchorManifestJson: readFlag(collectArgs, "anchor-manifest-json", ""),
|
|
720
|
+
impactJson: readFlag(collectArgs, "impact-json", ""),
|
|
719
721
|
buildSummaryJson: readFlag(collectArgs, "build-summary-json", ""),
|
|
720
722
|
platformManifestJsons: readRepeatedFlag(collectArgs, "platform-manifest-json"),
|
|
721
723
|
distTagEvidenceJson: readFlag(collectArgs, "dist-tag-evidence-json", ""),
|
package/docs/cli.md
CHANGED
|
@@ -252,6 +252,7 @@ buildchain collect github-release \
|
|
|
252
252
|
--publish-evidence-json .buildchain/release-evidence/v2.3.2/evidence.json \
|
|
253
253
|
--transaction-json .buildchain/release-state/v2.3.2/state.json \
|
|
254
254
|
--package-set-json package-set.json \
|
|
255
|
+
--impact-json impact.json \
|
|
255
256
|
--trusted-publishing-json trusted-publishing.json \
|
|
256
257
|
--anchor-manifest-json libnode.release.json \
|
|
257
258
|
--build-summary-json .buildchain/artifacts/build-summary.json \
|
|
@@ -275,6 +276,17 @@ platform packages with version, dist-tag, and digest evidence. Verification
|
|
|
275
276
|
fails closed if supplied sections are internally incomplete or point to
|
|
276
277
|
artifacts without matching evidence.
|
|
277
278
|
|
|
279
|
+
`--impact-json` supplies the surface-aware impact ledger. Production release
|
|
280
|
+
passports (`release/*`) and major publish-gate passports require
|
|
281
|
+
`surfaceImpacts[]`; alpha, local, and legacy passport contexts keep it
|
|
282
|
+
optional. When `surfaceImpacts[]` is required or supplied, the verifier requires
|
|
283
|
+
each entry to include an id, impact, and rationale, and requires
|
|
284
|
+
`versionImpact.final` to match the highest declared surface impact. The
|
|
285
|
+
collector copies `versionImpact` plus `surfaceImpacts` into
|
|
286
|
+
`buildchain.release.json`. This lets
|
|
287
|
+
`buildchain explain release --for agent --json` state why a release is patch,
|
|
288
|
+
minor, or major instead of relying on file-path memory.
|
|
289
|
+
|
|
278
290
|
Buildchain dogfoods its observability toolkit in this lane. The standalone
|
|
279
291
|
builder writes API-generated events, while the workflow uses `buildchain mark`,
|
|
280
292
|
`buildchain span`, `buildchain verify observability-log`, and `buildchain log
|
package/docs/release-passport.md
CHANGED
|
@@ -36,6 +36,10 @@ Additive passport sections:
|
|
|
36
36
|
|
|
37
37
|
- `release`: exact tag, line, channel, source SHA, target ref, release SHA,
|
|
38
38
|
release material SHA, publish tooling SHA, and durable release-state ref.
|
|
39
|
+
- `versionImpact`: final patch/minor/major classification, source, and
|
|
40
|
+
rationale.
|
|
41
|
+
- `surfaceImpacts`: per registered surface classification. The final impact is
|
|
42
|
+
the highest entry in this list.
|
|
39
43
|
- `packageSet`: main package, platform packages, package-set order, registry,
|
|
40
44
|
versions, dist-tags, and package digests.
|
|
41
45
|
- `anchorManifest`: anchored/manual version manifest path, digest, and fields.
|
|
@@ -102,6 +106,15 @@ dist-tag, registry, role, platform, and digest, so agents do not need to stitch
|
|
|
102
106
|
npm facts back together from the lower-level evidence files.
|
|
103
107
|
`buildSummary`, `platformArtifactManifests`, and `distTagPromotion` preserve the
|
|
104
108
|
build and npm dist-tag evidence chain in the same passport.
|
|
109
|
+
`impact.json` can be supplied with `--impact-json`. Production release
|
|
110
|
+
passports (`release/*`) and major publish-gate passports require
|
|
111
|
+
`surfaceImpacts[]`; alpha, local, and legacy passport contexts keep the field
|
|
112
|
+
optional. When `surfaceImpacts[]` is required or supplied, verification fails
|
|
113
|
+
closed unless each entry has an id, impact, and rationale, and
|
|
114
|
+
`versionImpact.final` matches the highest surface impact. For example, KFD-2
|
|
115
|
+
content can remain patch while an additive `registry.kind` field on the
|
|
116
|
+
machine-consumed KFD registry schema records a minor `kfd-registry-schema`
|
|
117
|
+
surface impact.
|
|
105
118
|
|
|
106
119
|
Verify a release passport:
|
|
107
120
|
|
package/docs/versioning.md
CHANGED
|
@@ -20,7 +20,8 @@ latest stable production patch for that minor line.
|
|
|
20
20
|
|
|
21
21
|
## Welded Surfaces
|
|
22
22
|
|
|
23
|
-
These surfaces
|
|
23
|
+
These surfaces are classified independently; the final release impact is the
|
|
24
|
+
highest impact across the affected registered surfaces:
|
|
24
25
|
|
|
25
26
|
- reusable workflow inputs, outputs, and artifact contracts;
|
|
26
27
|
- public CLI command families and their machine-readable JSON shapes;
|
|
@@ -31,20 +32,40 @@ These surfaces require at least a minor bump when first introduced:
|
|
|
31
32
|
ledger, and agent index files;
|
|
32
33
|
- binary distribution shapes that users can install or automate against.
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
For each surface:
|
|
36
|
+
|
|
37
|
+
- content, documentation, or implementation-only work that does not touch a
|
|
38
|
+
registered surface is patch;
|
|
39
|
+
- additive fields, new commands, new exports, new evidence sections, or new
|
|
40
|
+
registered surfaces are minor;
|
|
41
|
+
- removals, incompatible renames, changed meanings, newly required fields,
|
|
42
|
+
weakened trust gates, or changed ref flow are major.
|
|
43
|
+
|
|
44
|
+
The release passport records this as `surfaceImpacts[]` plus
|
|
45
|
+
`versionImpact.final`. The final impact must equal the highest surface impact,
|
|
46
|
+
so an agent cannot silently label a release patch when one machine surface needs
|
|
47
|
+
minor review.
|
|
48
|
+
|
|
49
|
+
`surfaceImpacts[]` is mandatory for production release passports (`release/*`)
|
|
50
|
+
and major publish-gate passports. Alpha, local, and legacy passport contexts
|
|
51
|
+
keep the field optional so temporary validation can proceed without pretending
|
|
52
|
+
to be a production release decision.
|
|
53
|
+
|
|
54
|
+
Example: a KFD document such as KFD-2 is content and remains patch, but adding a
|
|
55
|
+
`kind` field to the machine-consumed KFD `registry.json` is an additive change
|
|
56
|
+
to the `kfd-registry-schema` surface and therefore requires minor-impact
|
|
57
|
+
review. This avoids both false shortcuts: "new KFD means minor" and "all KFD
|
|
58
|
+
repository changes are patch".
|
|
38
59
|
|
|
39
60
|
## Decision Log
|
|
40
61
|
|
|
41
|
-
| Date |
|
|
42
|
-
| --- | --- | --- | --- |
|
|
43
|
-
| 2026-07-
|
|
44
|
-
| 2026-07-
|
|
45
|
-
| 2026-07-02 |
|
|
46
|
-
| 2026-07-
|
|
47
|
-
| 2026-07-
|
|
62
|
+
| Date | Action | Line | Faces | Class | Rationale | PR |
|
|
63
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
64
|
+
| 2026-07-04 | open-minor | `v2.5` | scheduled-integration-governance | additive | Scheduled integration governance adds scheduled feature-branch discovery, conflict-free integration, reporting, and agent-visible governance automation for dev-line maintenance. | |
|
|
65
|
+
| 2026-07-03 | open-minor | `v2.4` | infra-contract-lifecycle | additive | Infra contract lifecycle adds the provider-neutral `infra-contract` CLI command family, project type, adapter capability contract, lifecycle evidence bundle, propagation evidence, CI evidence mode, and consumer-facing contract artifacts. | |
|
|
66
|
+
| 2026-07-02 | open-minor | `v2.3` | web-surface-host-mapping | additive | Web surface host mapping adds first-class multi-host surface bindings, reusable workflow URL outputs, per-surface deployment overrides, and an agent-readable fixture contract. | |
|
|
67
|
+
| 2026-07-02 | open-minor | `v2.2` | release-passport, binary-distribution | additive | Release passport and binary distribution add agent-readable release passport files, artifact evidence, impact ledger, agent index, GitHub Release collection and verification commands, and standalone binary assets. | |
|
|
68
|
+
| 2026-07-02 | open-minor | `v2.1` | logging-sdk, cli-observability, package-subpaths | additive | Buildchain toolkit observability adds the public logging SDK, CLI observability commands, and package subpaths that consumers can import. | |
|
|
48
69
|
|
|
49
70
|
## Runner Policy
|
|
50
71
|
|
package/package.json
CHANGED
|
@@ -36,6 +36,66 @@ function optionalString(value) {
|
|
|
36
36
|
return value === undefined || value === null ? "" : String(value);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
const VERSION_IMPACT_ORDER = new Map([
|
|
40
|
+
["unknown", 0],
|
|
41
|
+
["patch", 1],
|
|
42
|
+
["minor", 2],
|
|
43
|
+
["major", 3],
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
function normalizeImpactLevel(value, fallback = "unknown") {
|
|
47
|
+
const normalized = optionalString(value || fallback).toLowerCase();
|
|
48
|
+
return VERSION_IMPACT_ORDER.has(normalized) ? normalized : optionalString(value || fallback);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function highestImpactLevel(levels = []) {
|
|
52
|
+
let highest = "unknown";
|
|
53
|
+
for (const level of levels) {
|
|
54
|
+
const normalized = normalizeImpactLevel(level);
|
|
55
|
+
if ((VERSION_IMPACT_ORDER.get(normalized) ?? -1) > (VERSION_IMPACT_ORDER.get(highest) ?? -1)) {
|
|
56
|
+
highest = normalized;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return highest;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function surfaceImpactRequirement({ passport = {}, impact = {} } = {}) {
|
|
63
|
+
const release = passport?.release && typeof passport.release === "object" ? passport.release : {};
|
|
64
|
+
const impactRelease = impact?.release && typeof impact.release === "object" ? impact.release : {};
|
|
65
|
+
const channel = optionalString(release.channel || impactRelease.channel).toLowerCase();
|
|
66
|
+
const targetRef = optionalString(
|
|
67
|
+
release.targetRef ||
|
|
68
|
+
release.target_ref ||
|
|
69
|
+
impactRelease.targetRef ||
|
|
70
|
+
impactRelease.target_ref,
|
|
71
|
+
).toLowerCase();
|
|
72
|
+
if (channel === "release" || targetRef.startsWith("release/")) {
|
|
73
|
+
return {
|
|
74
|
+
required: true,
|
|
75
|
+
type: "production-release",
|
|
76
|
+
reason: "production release passports require surfaceImpacts[] so agents can audit the final version impact",
|
|
77
|
+
channel,
|
|
78
|
+
targetRef,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (channel === "major" || targetRef === "publish-gate/major" || targetRef === "major-gate") {
|
|
82
|
+
return {
|
|
83
|
+
required: true,
|
|
84
|
+
type: "major-gate",
|
|
85
|
+
reason: "major publish gates require surfaceImpacts[] so breaking-surface rationale is explicit",
|
|
86
|
+
channel,
|
|
87
|
+
targetRef,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
required: false,
|
|
92
|
+
type: channel || (targetRef ? "non-production-release" : "legacy"),
|
|
93
|
+
reason: "surfaceImpacts[] is optional for alpha, local, and legacy passport contexts",
|
|
94
|
+
channel,
|
|
95
|
+
targetRef,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
39
99
|
function stableJson(value) {
|
|
40
100
|
if (Array.isArray(value)) {
|
|
41
101
|
return `[${value.map(stableJson).join(",")}]`;
|
|
@@ -123,14 +183,21 @@ function defaultProductMechanism({ repository = "", productName = "Buildchain" }
|
|
|
123
183
|
}
|
|
124
184
|
|
|
125
185
|
function defaultImpact({ tag = "", line = "", decision = "unknown" } = {}) {
|
|
186
|
+
const finalImpact = normalizeImpactLevel(decision);
|
|
126
187
|
return {
|
|
127
188
|
schemaVersion: 1,
|
|
128
189
|
contract: IMPACT_LEDGER_CONTRACT,
|
|
129
190
|
release: { tag, line },
|
|
130
|
-
|
|
131
|
-
|
|
191
|
+
versionImpact: {
|
|
192
|
+
final: finalImpact,
|
|
193
|
+
source: "default",
|
|
194
|
+
rationale: "No surface impact classification was supplied.",
|
|
195
|
+
},
|
|
196
|
+
surfaceImpacts: [],
|
|
197
|
+
classification: finalImpact,
|
|
198
|
+
breaking: finalImpact === "major",
|
|
132
199
|
security: false,
|
|
133
|
-
migrationRequired:
|
|
200
|
+
migrationRequired: finalImpact === "major",
|
|
134
201
|
summary: "No release impact summary was supplied.",
|
|
135
202
|
recovery: {
|
|
136
203
|
rollback: "Use the previous exact release tag or previous floating channel ref.",
|
|
@@ -339,6 +406,55 @@ function normalizePublishEvidence(value = undefined) {
|
|
|
339
406
|
};
|
|
340
407
|
}
|
|
341
408
|
|
|
409
|
+
function normalizeSurfaceImpact(entry = {}, index = 0) {
|
|
410
|
+
const id = nonEmptyString(entry.id || entry.surface || entry.surfaceId, `surfaceImpacts[${index}].id`);
|
|
411
|
+
const impact = normalizeImpactLevel(entry.impact || entry.classification || entry.versionImpact);
|
|
412
|
+
return {
|
|
413
|
+
id,
|
|
414
|
+
impact,
|
|
415
|
+
class: optionalString(entry.class || entry.changeClass || entry.change_class),
|
|
416
|
+
rationale: optionalString(entry.rationale || entry.reason),
|
|
417
|
+
source: optionalString(entry.source),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function normalizeImpactLedger(value = undefined, { tag = "", line = "", decision = "unknown" } = {}) {
|
|
422
|
+
if (!value) {
|
|
423
|
+
return defaultImpact({ tag, line, decision });
|
|
424
|
+
}
|
|
425
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
426
|
+
throw new Error("impact must be a JSON object");
|
|
427
|
+
}
|
|
428
|
+
const surfaceImpacts = Array.isArray(value.surfaceImpacts)
|
|
429
|
+
? value.surfaceImpacts.map((entry, index) => normalizeSurfaceImpact(entry, index))
|
|
430
|
+
: [];
|
|
431
|
+
const finalImpact = normalizeImpactLevel(
|
|
432
|
+
value.versionImpact?.final ||
|
|
433
|
+
value.versionImpact ||
|
|
434
|
+
value.classification ||
|
|
435
|
+
decision,
|
|
436
|
+
);
|
|
437
|
+
return {
|
|
438
|
+
...value,
|
|
439
|
+
schemaVersion: Number(value.schemaVersion || 1),
|
|
440
|
+
contract: value.contract || IMPACT_LEDGER_CONTRACT,
|
|
441
|
+
release: {
|
|
442
|
+
tag: optionalString(value.release?.tag || tag),
|
|
443
|
+
line: optionalString(value.release?.line || line),
|
|
444
|
+
...(value.release && typeof value.release === "object" && !Array.isArray(value.release) ? value.release : {}),
|
|
445
|
+
},
|
|
446
|
+
versionImpact: {
|
|
447
|
+
final: finalImpact,
|
|
448
|
+
source: optionalString(value.versionImpact?.source || value.impactSource || "declared"),
|
|
449
|
+
rationale: optionalString(value.versionImpact?.rationale || value.rationale || value.summary),
|
|
450
|
+
},
|
|
451
|
+
surfaceImpacts,
|
|
452
|
+
classification: normalizeImpactLevel(value.classification || finalImpact),
|
|
453
|
+
breaking: value.breaking === undefined ? finalImpact === "major" : Boolean(value.breaking),
|
|
454
|
+
migrationRequired: value.migrationRequired === undefined ? finalImpact === "major" : Boolean(value.migrationRequired),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
342
458
|
function packageSetEntries(packageSet = undefined) {
|
|
343
459
|
if (!packageSet) {
|
|
344
460
|
return [];
|
|
@@ -503,6 +619,7 @@ export function createReleasePassport({
|
|
|
503
619
|
distTagPromotionEvidence = undefined,
|
|
504
620
|
release = {},
|
|
505
621
|
publish = {},
|
|
622
|
+
impact = undefined,
|
|
506
623
|
workflow = {},
|
|
507
624
|
} = {}) {
|
|
508
625
|
const normalizedTag = nonEmptyString(tag, "tag");
|
|
@@ -518,6 +635,7 @@ export function createReleasePassport({
|
|
|
518
635
|
const normalizedDistTagPromotionEvidence = distTagPromotionEvidence
|
|
519
636
|
? normalizeEvidenceDocument(distTagPromotionEvidence, "distTagPromotionEvidence")
|
|
520
637
|
: undefined;
|
|
638
|
+
const normalizedImpact = normalizeImpactLedger(impact, { tag: normalizedTag, line });
|
|
521
639
|
const publishArtifacts = normalizedPublishEvidence?.artifacts || [];
|
|
522
640
|
const normalizedPublishSummary = normalizePublishSummary({
|
|
523
641
|
packageSet: normalizedPackageSet,
|
|
@@ -618,6 +736,8 @@ export function createReleasePassport({
|
|
|
618
736
|
...(normalizedBuildSummary ? { buildSummary: normalizedBuildSummary } : {}),
|
|
619
737
|
...(normalizedPlatformArtifactManifests.length > 0 ? { platformArtifactManifests: normalizedPlatformArtifactManifests } : {}),
|
|
620
738
|
...(normalizedDistTagPromotionEvidence ? { distTagPromotion: normalizedDistTagPromotionEvidence } : {}),
|
|
739
|
+
versionImpact: normalizedImpact.versionImpact,
|
|
740
|
+
surfaceImpacts: normalizedImpact.surfaceImpacts,
|
|
621
741
|
artifacts: [
|
|
622
742
|
...artifactEvidence.artifacts.map((asset) => ({
|
|
623
743
|
group: "release",
|
|
@@ -680,6 +800,7 @@ export function collectGitHubReleasePassport({
|
|
|
680
800
|
trustedPublishingJson = "",
|
|
681
801
|
transactionJson = "",
|
|
682
802
|
anchorManifestJson = "",
|
|
803
|
+
impactJson = "",
|
|
683
804
|
buildSummaryJson = "",
|
|
684
805
|
platformManifestJsons = [],
|
|
685
806
|
distTagEvidenceJson = "",
|
|
@@ -695,6 +816,7 @@ export function collectGitHubReleasePassport({
|
|
|
695
816
|
const trustedPublishing = parseJsonInput(trustedPublishingJson, undefined);
|
|
696
817
|
const transactionMeta = parseJsonInputWithMeta(transactionJson, undefined);
|
|
697
818
|
const anchorManifest = normalizeAnchorManifest(parseJsonInputWithMeta(anchorManifestJson, undefined));
|
|
819
|
+
const impactMeta = parseJsonInputWithMeta(impactJson, undefined);
|
|
698
820
|
const buildSummaryMeta = parseJsonInputWithMeta(buildSummaryJson, undefined);
|
|
699
821
|
const platformManifestMetas = (platformManifestJsons || [])
|
|
700
822
|
.filter(Boolean)
|
|
@@ -710,7 +832,7 @@ export function collectGitHubReleasePassport({
|
|
|
710
832
|
const resolvedOutputDir = path.resolve(cwd, outputDir);
|
|
711
833
|
const productMechanism = defaultProductMechanism({ repository, productName });
|
|
712
834
|
const artifactEvidence = createArtifactEvidence({ assets, repository, tag: resolvedTag, sourceSha, workflow });
|
|
713
|
-
const impact =
|
|
835
|
+
const impact = normalizeImpactLedger(impactMeta.value, { tag: resolvedTag, line, decision: "unknown" });
|
|
714
836
|
const agentIndex = defaultAgentIndex({ tag: resolvedTag });
|
|
715
837
|
const passport = createReleasePassport({
|
|
716
838
|
cwd,
|
|
@@ -747,6 +869,7 @@ export function collectGitHubReleasePassport({
|
|
|
747
869
|
: undefined,
|
|
748
870
|
release: releaseExtra,
|
|
749
871
|
publish,
|
|
872
|
+
impact,
|
|
750
873
|
publishEvidencePath: publishEvidenceMeta.path ? path.relative(resolvedOutputDir, publishEvidenceMeta.path).split(path.sep).join("/") : "",
|
|
751
874
|
transactionStatePath: transactionMeta.path ? path.relative(resolvedOutputDir, transactionMeta.path).split(path.sep).join("/") : "",
|
|
752
875
|
workflow,
|
|
@@ -1042,6 +1165,42 @@ export function createReleaseCheckReport({
|
|
|
1042
1165
|
if (!passport?.runnerPolicy?.productionDefault) {
|
|
1043
1166
|
issues.push(issue("warning", "runnerPolicy.productionDefault", "runner policy should record the production default"));
|
|
1044
1167
|
}
|
|
1168
|
+
const surfaceImpacts = Array.isArray(impact?.surfaceImpacts) ? impact.surfaceImpacts : [];
|
|
1169
|
+
const passportSurfaceImpacts = Array.isArray(passport?.surfaceImpacts) ? passport.surfaceImpacts : [];
|
|
1170
|
+
const declaredFinalImpact = normalizeImpactLevel(impact?.versionImpact?.final || impact?.classification);
|
|
1171
|
+
const computedFinalImpact = highestImpactLevel(surfaceImpacts.map((entry) => entry.impact));
|
|
1172
|
+
const requiredSurfaceImpacts = surfaceImpactRequirement({ passport, impact });
|
|
1173
|
+
if (requiredSurfaceImpacts.required && surfaceImpacts.length === 0) {
|
|
1174
|
+
issues.push(issue("error", "impact.surfaceImpacts.required", "surfaceImpacts[] is required for this release passport type", requiredSurfaceImpacts));
|
|
1175
|
+
}
|
|
1176
|
+
if (surfaceImpacts.length > 0) {
|
|
1177
|
+
for (const [index, entry] of surfaceImpacts.entries()) {
|
|
1178
|
+
if (!entry.id) {
|
|
1179
|
+
issues.push(issue("error", `impact.surfaceImpacts[${index}].id`, "surface impact id is required"));
|
|
1180
|
+
}
|
|
1181
|
+
if (!VERSION_IMPACT_ORDER.has(entry.impact)) {
|
|
1182
|
+
issues.push(issue("error", `impact.surfaceImpacts[${index}].impact`, "surface impact must be patch, minor, or major"));
|
|
1183
|
+
}
|
|
1184
|
+
if (!entry.rationale) {
|
|
1185
|
+
issues.push(issue("error", `impact.surfaceImpacts[${index}].rationale`, "surface impact rationale is required"));
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
if (!impact?.versionImpact?.rationale) {
|
|
1189
|
+
issues.push(issue("error", "impact.versionImpact.rationale", "version impact rationale is required when surface impacts are supplied"));
|
|
1190
|
+
}
|
|
1191
|
+
if (declaredFinalImpact !== computedFinalImpact) {
|
|
1192
|
+
issues.push(issue("error", "impact.versionImpact.final", "versionImpact.final must equal the highest surface impact", {
|
|
1193
|
+
declared: declaredFinalImpact,
|
|
1194
|
+
computed: computedFinalImpact,
|
|
1195
|
+
}));
|
|
1196
|
+
}
|
|
1197
|
+
if (stableJson(passportSurfaceImpacts) !== stableJson(surfaceImpacts)) {
|
|
1198
|
+
issues.push(issue("error", "passport.surfaceImpacts", "passport.surfaceImpacts must mirror impact.surfaceImpacts"));
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
if (passport?.versionImpact?.final && impact?.versionImpact?.final && passport.versionImpact.final !== impact.versionImpact.final) {
|
|
1202
|
+
issues.push(issue("error", "passport.versionImpact.final", "passport.versionImpact.final must match impact.versionImpact.final"));
|
|
1203
|
+
}
|
|
1045
1204
|
const ok = issues.every((entry) => entry.level !== "error");
|
|
1046
1205
|
return {
|
|
1047
1206
|
schemaVersion: 1,
|
|
@@ -1049,6 +1208,7 @@ export function createReleaseCheckReport({
|
|
|
1049
1208
|
checkedAt,
|
|
1050
1209
|
ok,
|
|
1051
1210
|
trust: ok ? "pass" : "fail",
|
|
1211
|
+
surfaceImpactRequirement: requiredSurfaceImpacts,
|
|
1052
1212
|
completeness: {
|
|
1053
1213
|
artifactCount: artifacts.length,
|
|
1054
1214
|
evidenceArtifactCount: evidenceArtifacts.length,
|
|
@@ -1064,6 +1224,9 @@ export function createReleaseCheckReport({
|
|
|
1064
1224
|
impactPresent: Boolean(impact),
|
|
1065
1225
|
agentIndexPresent: Boolean(agentIndex),
|
|
1066
1226
|
productMechanismPresent: Boolean(productMechanism),
|
|
1227
|
+
surfaceImpactsRequired: requiredSurfaceImpacts.required,
|
|
1228
|
+
surfaceImpactCount: surfaceImpacts.length,
|
|
1229
|
+
versionImpact: impact?.versionImpact?.final || "",
|
|
1067
1230
|
},
|
|
1068
1231
|
issues,
|
|
1069
1232
|
};
|
|
@@ -1156,8 +1319,12 @@ export async function explainReleasePassport({ passportLocation, forAudience = "
|
|
|
1156
1319
|
artifactCount: report.completeness.artifactCount,
|
|
1157
1320
|
runnerPolicy: passport.runnerPolicy,
|
|
1158
1321
|
impact: {
|
|
1159
|
-
|
|
1160
|
-
|
|
1322
|
+
versionImpact: passport.versionImpact || {},
|
|
1323
|
+
surfaceImpacts: Array.isArray(passport.surfaceImpacts) ? passport.surfaceImpacts : [],
|
|
1324
|
+
surfaceImpactRequirement: report.surfaceImpactRequirement,
|
|
1325
|
+
breaking: Boolean(passport.versionImpact?.final === "major"),
|
|
1326
|
+
migrationRequired: Boolean(passport.versionImpact?.final === "major"),
|
|
1327
|
+
summary: passport.versionImpact?.rationale || "",
|
|
1161
1328
|
},
|
|
1162
1329
|
recovery: passport.recovery,
|
|
1163
1330
|
nextAction,
|
|
@@ -130,7 +130,9 @@ if (!coreIndexSource.includes("reportBuildchainIssue")) {
|
|
|
130
130
|
throw new Error("packages/core/index.js must export reportBuildchainIssue");
|
|
131
131
|
}
|
|
132
132
|
for (const requiredSnippet of [
|
|
133
|
-
"
|
|
133
|
+
"surfaceImpacts[]",
|
|
134
|
+
"versionImpact.final",
|
|
135
|
+
"kfd-registry-schema",
|
|
134
136
|
"`v2.2`",
|
|
135
137
|
"GitHub-hosted runners for production",
|
|
136
138
|
"Self-hosted runners remain compatibility fixtures",
|
|
@@ -91,7 +91,7 @@ export function selectMergedChannelPullRequest({ pullRequests = [], targetRef, r
|
|
|
91
91
|
return candidates[0];
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
export function
|
|
94
|
+
export function selectReleaseCandidateRuns({ runs = [], pullRequest, workflowName = "" }) {
|
|
95
95
|
const prNumber = Number(pullRequest?.number || 0);
|
|
96
96
|
const prHeadSha = String(pullRequest?.head?.sha || pullRequest?.headRefOid || "").trim();
|
|
97
97
|
const prHeadBranch = normalizeBranch(pullRequest?.head?.ref || pullRequest?.headRefName || "");
|
|
@@ -111,7 +111,11 @@ export function selectReleaseCandidateRun({ runs = [], pullRequest, workflowName
|
|
|
111
111
|
return matchesPr && matchesWorkflow && run.event === "pull_request" && run.status === "completed" && run.conclusion === "success";
|
|
112
112
|
});
|
|
113
113
|
candidates.sort((left, right) => Date.parse(right.updated_at || right.created_at || "") - Date.parse(left.updated_at || left.created_at || ""));
|
|
114
|
-
return candidates
|
|
114
|
+
return candidates;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function selectReleaseCandidateRun({ runs = [], pullRequest, workflowName = "" }) {
|
|
118
|
+
return selectReleaseCandidateRuns({ runs, pullRequest, workflowName })[0];
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
function outputPath(filePath) {
|
|
@@ -375,24 +379,40 @@ export async function resolveReleaseCandidateArtifacts({
|
|
|
375
379
|
fetchImpl,
|
|
376
380
|
path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/workflows/${encodeURIComponent(workflowFile)}/runs?event=pull_request&status=success&per_page=100`,
|
|
377
381
|
});
|
|
378
|
-
const
|
|
382
|
+
const candidateRuns = selectReleaseCandidateRuns({
|
|
379
383
|
runs: Array.isArray(runs.workflow_runs) ? runs.workflow_runs : [],
|
|
380
384
|
pullRequest,
|
|
381
385
|
workflowName,
|
|
382
386
|
});
|
|
383
|
-
if (!
|
|
387
|
+
if (!candidateRuns.length) {
|
|
384
388
|
throw new Error(`no successful ${workflowName} pull_request run found for channel PR #${pullRequest.number}`);
|
|
385
389
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
390
|
+
let run;
|
|
391
|
+
let artifactResponse;
|
|
392
|
+
let selected;
|
|
393
|
+
const selectionErrors = [];
|
|
394
|
+
for (const candidateRun of candidateRuns) {
|
|
395
|
+
const candidateArtifactResponse = await githubJson({
|
|
396
|
+
apiUrl,
|
|
397
|
+
token,
|
|
398
|
+
fetchImpl,
|
|
399
|
+
path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/runs/${candidateRun.id}/artifacts?per_page=100`,
|
|
400
|
+
});
|
|
401
|
+
try {
|
|
402
|
+
selected = selectReleaseCandidateArtifacts({
|
|
403
|
+
artifacts: Array.isArray(candidateArtifactResponse.artifacts) ? candidateArtifactResponse.artifacts : [],
|
|
404
|
+
artifactName,
|
|
405
|
+
});
|
|
406
|
+
run = candidateRun;
|
|
407
|
+
artifactResponse = candidateArtifactResponse;
|
|
408
|
+
break;
|
|
409
|
+
} catch (error) {
|
|
410
|
+
selectionErrors.push(`run ${candidateRun.id}: ${error.message}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (!run || !artifactResponse || !selected) {
|
|
414
|
+
throw new Error(`no successful ${workflowName} pull_request run for channel PR #${pullRequest.number} contained release-candidate artifacts: ${selectionErrors.join("; ")}`);
|
|
415
|
+
}
|
|
396
416
|
const payloadArtifacts = selectPayloadArtifacts({
|
|
397
417
|
artifacts: Array.isArray(artifactResponse.artifacts) ? artifactResponse.artifacts : [],
|
|
398
418
|
artifactName: selected.prefix,
|