@kungfu-tech/buildchain 4.0.1-alpha.9 → 4.0.2-alpha.0
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/README.md +9 -6
- package/actions/v4-release-candidate-promote/README.md +3 -0
- package/architecture/agent-change-map.md +1017 -0
- package/architecture/ci-lane-change-budget.json +70 -1
- package/architecture/decisions/0004-v4-declarative-release-tail.md +47 -0
- package/architecture/internal-capabilities.json +159 -11
- package/architecture/maintainability-debt.json +3229 -0
- package/architecture/maintainability-policy.json +480 -129
- package/architecture/v3-core-mechanism-inventory.json +6 -3
- package/architecture/v3-v4-capability-closure.md +59 -0
- package/architecture/v3-v4-live-capability-inventory.json +102404 -0
- package/architecture/v4-cross-platform-adopter-qualification.json +52 -0
- package/architecture/v4-delivery-warrant-shadow-fixtures.json +3 -3
- package/architecture/v4-runtime-semantic-closure.json +710 -0
- package/bin/internal/trust-release-release-handlers.mjs +5 -0
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
- package/contracts/v4-publication-qualification-v1.schema.json +51 -0
- package/dist/site/agent-index.json +1 -0
- package/dist/site/artifact-schemas.json +2 -0
- package/dist/site/buildchain-contract.json +123 -32
- package/dist/site/buildchain-site.json +96 -28
- package/dist/site/capability-registry.json +6 -6
- package/dist/site/cli-registry.json +5 -2
- package/dist/site/controller-registry.json +41 -5
- package/dist/site/kfd-claims.json +172 -23
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +6 -6
- package/dist/site/node-api-registry.json +1474 -215
- package/dist/site/page-registry.json +85 -17
- package/dist/site/public-surface-audit.json +91 -21
- package/dist/site/publication-authority-registry.json +28 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +5 -0
- package/dist/site/schemas/v4-publication-qualification-v1.schema.json +51 -0
- package/dist/site/site-manifest.json +10 -10
- package/dist/site/workflow-registry.json +94 -18
- package/docs/MAP.md +1 -1
- package/docs/cli-reference.md +4 -4
- package/docs/dev-delivery-warrant.md +33 -9
- package/docs/node-api-reference.md +291 -206
- package/docs/publish-transaction.md +11 -4
- package/docs/release-candidate.md +7 -0
- package/docs/v4-adopter-delivery.md +23 -4
- package/docs/v4-declarative-release-tail-migration.md +43 -0
- package/package.json +8 -3
- package/packages/core/adopter-delivery-passport.js +36 -0
- package/packages/core/buildchain-contract.js +31 -1
- package/packages/core/buildchain-publication-authority.js +1 -0
- package/packages/core/dev-delivery-warrant-legacy-recovery.js +246 -0
- package/packages/core/dev-delivery-warrant-settlement.js +239 -3
- package/packages/core/dev-delivery-warrant-state.js +15 -2
- package/packages/core/dev-delivery-warrant.js +21 -4
- package/packages/core/github-governance-authority.js +4 -4
- package/packages/core/index.js +1 -0
- package/packages/core/paper-scaffold-content.js +401 -0
- package/packages/core/paper.js +12 -378
- package/packages/core/publication-control-plane-audit.js +135 -111
- package/packages/core/publication-rehearsal-projection.js +104 -0
- package/packages/core/publication-rehearsal-runtime.js +190 -0
- package/packages/core/release-candidate-recovery.js +6 -5
- package/packages/core/release-passport.js +82 -184
- package/packages/core/v4-cross-platform-adopter-qualification.js +343 -0
- package/packages/core/v4-floating-consumer-policy.js +3 -2
- package/packages/core/v4-floating-consumer-release-passport.js +134 -15
- package/packages/core/v4-publication-qualification.js +222 -0
- package/scripts/artifact-signing-controller-core.mjs +1 -1
- package/scripts/audit-publication-control-plane.mjs +17 -17
- package/scripts/buildchain-cli-help.mjs +3 -0
- package/scripts/check-internal-architecture.mjs +23 -3
- package/scripts/check-inventory.mjs +23 -5
- package/scripts/check-maintainability.mjs +145 -4
- package/scripts/check-project-cut-merge-queue-admission.mjs +87 -0
- package/scripts/check-v3-v4-capability-inventory.mjs +574 -0
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +10 -3
- package/scripts/check-v4-runtime-semantic-closure.mjs +266 -0
- package/scripts/dev-delivery-warrant-options.mjs +42 -15
- package/scripts/dev-delivery-warrant.mjs +59 -6
- package/scripts/dev-pr-auto-merge.mjs +25 -11
- package/scripts/dev-pr-delivery-warrant.mjs +44 -0
- package/scripts/dev-pr-prequeue-guard.mjs +0 -18
- package/scripts/dispatch-artifact-signing-authority.mjs +1 -1
- package/scripts/generate-agent-change-map.mjs +118 -0
- package/scripts/generate-release-candidate-passport.mjs +26 -1
- package/scripts/generate-site-bundle.mjs +6 -0
- package/scripts/maintainability-domain-metrics.mjs +237 -0
- package/scripts/maintainability-governance.mjs +473 -0
- package/scripts/maintainability-metrics.mjs +28 -6
- package/scripts/project-cut-merge-queue-admission.mjs +394 -0
- package/scripts/promotion-routing-evidence.mjs +35 -6
- package/scripts/release-candidate-resolver.mjs +44 -0
- package/scripts/release-line-policy.mjs +25 -15
- package/scripts/release-tail.mjs +19 -2
- package/scripts/resume-from-candidate-run.mjs +106 -103
- package/scripts/run-lifecycle-core.mjs +341 -376
- package/scripts/site-capability-metadata.mjs +4 -0
- package/scripts/stable-candidate-qualification.mjs +9 -10
- package/scripts/v3-v4-capability-catalog.mjs +439 -0
- package/scripts/v4-cross-platform-adopter-qualification.mjs +228 -0
- package/scripts/v4-declarative-promotion-admission.mjs +42 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
export const V4_RUNTIME_SEMANTIC_CLOSURE_CONTRACT =
|
|
8
|
+
"kungfu-buildchain-v4-runtime-semantic-closure";
|
|
9
|
+
export const V4_RUNTIME_SEMANTIC_CLOSURE_PATH =
|
|
10
|
+
"architecture/v4-runtime-semantic-closure.json";
|
|
11
|
+
export const REQUIRED_RUNTIME_EVIDENCE = Object.freeze([
|
|
12
|
+
"positive",
|
|
13
|
+
"negative",
|
|
14
|
+
"failure",
|
|
15
|
+
"recovery",
|
|
16
|
+
"idempotence",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
function loadJson(root, relativePath) {
|
|
20
|
+
return JSON.parse(fs.readFileSync(path.join(root, relativePath), "utf8"));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function sorted(values) {
|
|
24
|
+
return [...values].sort((left, right) => left.localeCompare(right));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sameMembers(left, right) {
|
|
28
|
+
return JSON.stringify(sorted(left)) === JSON.stringify(sorted(right));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function publicSurfaceIds(root) {
|
|
32
|
+
const packageJson = loadJson(root, "package.json");
|
|
33
|
+
const cli = loadJson(root, "dist/site/cli-registry.json");
|
|
34
|
+
const workflows = loadJson(root, "dist/site/workflow-registry.json");
|
|
35
|
+
return new Set([
|
|
36
|
+
...Object.keys(packageJson.exports || {}).map((id) => `export:${id}`),
|
|
37
|
+
...(cli.commands || []).map((entry) => `cli:${entry.id}`),
|
|
38
|
+
...(workflows.workflows || []).map((entry) => `workflow:${entry.id}`),
|
|
39
|
+
...(workflows.actions || []).map((entry) => `action:${entry.id}`),
|
|
40
|
+
]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function validateEvidence(root, mechanism, capability, issues) {
|
|
44
|
+
const sourceTests = new Set(mechanism.testPaths || []);
|
|
45
|
+
for (const dimension of REQUIRED_RUNTIME_EVIDENCE) {
|
|
46
|
+
const entries = capability.evidence?.[dimension];
|
|
47
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
48
|
+
issues.push(`${capability.id}: ${dimension} evidence is empty`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (!entry?.path || !entry?.name) {
|
|
53
|
+
issues.push(`${capability.id}: ${dimension} evidence is incomplete`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (!sourceTests.has(entry.path))
|
|
57
|
+
issues.push(
|
|
58
|
+
`${capability.id}: ${dimension} evidence is outside the reverse-scanned test set: ${entry.path}`,
|
|
59
|
+
);
|
|
60
|
+
const absolute = path.join(root, entry.path);
|
|
61
|
+
if (!fs.existsSync(absolute)) {
|
|
62
|
+
issues.push(
|
|
63
|
+
`${capability.id}: evidence path is missing: ${entry.path}`,
|
|
64
|
+
);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const source = fs.readFileSync(absolute, "utf8");
|
|
68
|
+
if (!source.includes(`test(${JSON.stringify(entry.name)}`))
|
|
69
|
+
issues.push(
|
|
70
|
+
`${capability.id}: evidence test is missing from ${entry.path}: ${entry.name}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function validateResidualClosure(mechanism, capability, issues) {
|
|
77
|
+
const sourceResiduals = mechanism.unresolved || [];
|
|
78
|
+
const closed = capability.resolvedResiduals || [];
|
|
79
|
+
if (
|
|
80
|
+
!sameMembers(
|
|
81
|
+
sourceResiduals,
|
|
82
|
+
closed.map((entry) => entry?.source || ""),
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
issues.push(
|
|
86
|
+
`${capability.id}: resolved residual identities do not match the reverse scan`,
|
|
87
|
+
);
|
|
88
|
+
for (const entry of closed) {
|
|
89
|
+
if (!String(entry?.resolution || "").trim())
|
|
90
|
+
issues.push(`${capability.id}: residual resolution is empty`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function validateImplementation(root, mechanism, capability, issues) {
|
|
95
|
+
if (
|
|
96
|
+
!Array.isArray(capability.implementation) ||
|
|
97
|
+
capability.implementation.length === 0
|
|
98
|
+
) {
|
|
99
|
+
issues.push(`${capability.id}: implementation route is empty`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const reverseScanned = new Set(mechanism.sourcePaths || []);
|
|
103
|
+
let boundToReverseScan = false;
|
|
104
|
+
for (const relativePath of capability.implementation) {
|
|
105
|
+
if (!fs.existsSync(path.join(root, relativePath)))
|
|
106
|
+
issues.push(
|
|
107
|
+
`${capability.id}: implementation path is missing: ${relativePath}`,
|
|
108
|
+
);
|
|
109
|
+
if (reverseScanned.has(relativePath)) boundToReverseScan = true;
|
|
110
|
+
}
|
|
111
|
+
if (!boundToReverseScan)
|
|
112
|
+
issues.push(
|
|
113
|
+
`${capability.id}: implementation is not bound to a reverse-scanned v3 mechanism route`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function validateAuthority(capability, stateMachine, issues) {
|
|
118
|
+
for (const field of [
|
|
119
|
+
"exactHeadFence",
|
|
120
|
+
"immutableRootFence",
|
|
121
|
+
"providerReadbackBeforeRetry",
|
|
122
|
+
"idempotentReplay",
|
|
123
|
+
]) {
|
|
124
|
+
if (capability.invariants?.[field] !== true)
|
|
125
|
+
issues.push(`${capability.id}: ${field} must remain true`);
|
|
126
|
+
}
|
|
127
|
+
if (capability.routeKind !== "v4-native")
|
|
128
|
+
issues.push(`${capability.id}: routeKind must be v4-native`);
|
|
129
|
+
if (capability.legacyFallbackAllowed !== false)
|
|
130
|
+
issues.push(`${capability.id}: legacy fallback must remain disabled`);
|
|
131
|
+
if (capability.providerDecisionAuthorityAllowed !== false)
|
|
132
|
+
issues.push(
|
|
133
|
+
`${capability.id}: provider decision authority must remain disabled`,
|
|
134
|
+
);
|
|
135
|
+
if (stateMachine) {
|
|
136
|
+
if (capability.stateAuthority !== "single-writer-state-machine")
|
|
137
|
+
issues.push(
|
|
138
|
+
`${capability.id}: state-machine authority classification drifted`,
|
|
139
|
+
);
|
|
140
|
+
if (
|
|
141
|
+
stateMachine.writer?.authoritative !== true ||
|
|
142
|
+
stateMachine.writer?.secondWriterBudget !== 0
|
|
143
|
+
)
|
|
144
|
+
issues.push(`${capability.id}: v4 state machine is not single-writer`);
|
|
145
|
+
if (
|
|
146
|
+
!Array.isArray(stateMachine.recovery) ||
|
|
147
|
+
stateMachine.recovery.length === 0
|
|
148
|
+
)
|
|
149
|
+
issues.push(`${capability.id}: v4 state machine recovery is empty`);
|
|
150
|
+
} else if (capability.stateAuthority !== "bounded-protocol") {
|
|
151
|
+
issues.push(
|
|
152
|
+
`${capability.id}: bounded protocol authority classification drifted`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function validateRuntimeSemanticClosure({
|
|
158
|
+
root = process.cwd(),
|
|
159
|
+
manifest = loadJson(root, V4_RUNTIME_SEMANTIC_CLOSURE_PATH),
|
|
160
|
+
v3Inventory = loadJson(root, "architecture/v3-core-mechanism-inventory.json"),
|
|
161
|
+
v4Manifest = loadJson(
|
|
162
|
+
root,
|
|
163
|
+
"architecture/v4-capability-state-machine-manifest.json",
|
|
164
|
+
),
|
|
165
|
+
} = {}) {
|
|
166
|
+
const issues = [];
|
|
167
|
+
if (
|
|
168
|
+
manifest?.schemaVersion !== 1 ||
|
|
169
|
+
manifest?.contract !== V4_RUNTIME_SEMANTIC_CLOSURE_CONTRACT
|
|
170
|
+
)
|
|
171
|
+
issues.push(
|
|
172
|
+
"runtime semantic closure contract or schemaVersion is invalid",
|
|
173
|
+
);
|
|
174
|
+
if (!sameMembers(manifest?.requiredEvidence || [], REQUIRED_RUNTIME_EVIDENCE))
|
|
175
|
+
issues.push("runtime semantic closure evidence dimensions drifted");
|
|
176
|
+
|
|
177
|
+
const mechanisms = new Map(
|
|
178
|
+
(v3Inventory.mechanisms || []).map((entry) => [entry.id, entry]),
|
|
179
|
+
);
|
|
180
|
+
const v4Capabilities = new Map(
|
|
181
|
+
(v4Manifest.capabilities || []).map((entry) => [entry.id, entry]),
|
|
182
|
+
);
|
|
183
|
+
const stateMachines = new Map(
|
|
184
|
+
(v4Manifest.stateMachines || []).map((entry) => [entry.id, entry]),
|
|
185
|
+
);
|
|
186
|
+
const capabilities = Array.isArray(manifest?.capabilities)
|
|
187
|
+
? manifest.capabilities
|
|
188
|
+
: [];
|
|
189
|
+
if (
|
|
190
|
+
!sameMembers(
|
|
191
|
+
capabilities.map((entry) => entry.id),
|
|
192
|
+
mechanisms.keys(),
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
issues.push(
|
|
196
|
+
"runtime semantic closure does not cover every v3 mechanism exactly once",
|
|
197
|
+
);
|
|
198
|
+
if (
|
|
199
|
+
!sameMembers(
|
|
200
|
+
capabilities.map((entry) => entry.id),
|
|
201
|
+
v4Capabilities.keys(),
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
issues.push(
|
|
205
|
+
"runtime semantic closure does not match the v4 capability manifest",
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const surfaces = publicSurfaceIds(root);
|
|
209
|
+
const ids = new Set();
|
|
210
|
+
for (const capability of capabilities) {
|
|
211
|
+
if (!capability?.id || ids.has(capability.id)) {
|
|
212
|
+
issues.push(
|
|
213
|
+
`runtime semantic capability id is missing or duplicated: ${capability?.id || "<empty>"}`,
|
|
214
|
+
);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
ids.add(capability.id);
|
|
218
|
+
const mechanism = mechanisms.get(capability.id);
|
|
219
|
+
const v4Capability = v4Capabilities.get(capability.id);
|
|
220
|
+
if (!mechanism || !v4Capability) continue;
|
|
221
|
+
if (capability.sourceMechanismId !== capability.id)
|
|
222
|
+
issues.push(`${capability.id}: source mechanism identity drifted`);
|
|
223
|
+
validateImplementation(root, mechanism, capability, issues);
|
|
224
|
+
validateResidualClosure(mechanism, capability, issues);
|
|
225
|
+
validateEvidence(root, mechanism, capability, issues);
|
|
226
|
+
validateAuthority(capability, stateMachines.get(capability.id), issues);
|
|
227
|
+
for (const surface of v4Capability.surfaces || []) {
|
|
228
|
+
if (!surfaces.has(surface))
|
|
229
|
+
issues.push(
|
|
230
|
+
`${capability.id}: executable v4 surface is absent: ${surface}`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (issues.length)
|
|
236
|
+
throw new Error(
|
|
237
|
+
`v4 runtime semantic closure failed:\n- ${issues.join("\n- ")}`,
|
|
238
|
+
);
|
|
239
|
+
return {
|
|
240
|
+
capabilities: capabilities.length,
|
|
241
|
+
evidenceDimensions: capabilities.length * REQUIRED_RUNTIME_EVIDENCE.length,
|
|
242
|
+
residualsClosed: capabilities.reduce(
|
|
243
|
+
(sum, entry) => sum + entry.resolvedResiduals.length,
|
|
244
|
+
0,
|
|
245
|
+
),
|
|
246
|
+
legacyFallbacks: 0,
|
|
247
|
+
providerDecisionAuthorities: 0,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (
|
|
252
|
+
process.argv[1] &&
|
|
253
|
+
import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href
|
|
254
|
+
) {
|
|
255
|
+
try {
|
|
256
|
+
const report = validateRuntimeSemanticClosure();
|
|
257
|
+
console.log(
|
|
258
|
+
`v4 runtime semantic closure passed: ${report.capabilities} capabilities, ` +
|
|
259
|
+
`${report.evidenceDimensions} evidence dimensions, ${report.residualsClosed} reverse-scan residuals closed, ` +
|
|
260
|
+
"0 legacy fallbacks, 0 provider decision authorities",
|
|
261
|
+
);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
264
|
+
process.exitCode = 1;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -7,11 +7,34 @@ export function hasFlag(args, name) {
|
|
|
7
7
|
return args.includes(`--${name}`);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
function terminalEvidenceOptions(rest, environment) {
|
|
11
|
+
return {
|
|
12
|
+
outcome: flag(rest, "outcome", environment.BUILDCHAIN_DEV_DELIVERY_OUTCOME),
|
|
13
|
+
eventAction: flag(
|
|
14
|
+
rest,
|
|
15
|
+
"event-action",
|
|
16
|
+
environment.BUILDCHAIN_DEV_DELIVERY_EVENT_ACTION,
|
|
17
|
+
),
|
|
18
|
+
evidenceRoot: flag(
|
|
19
|
+
rest,
|
|
20
|
+
"evidence-root",
|
|
21
|
+
environment.BUILDCHAIN_DEV_DELIVERY_EVIDENCE_ROOT,
|
|
22
|
+
),
|
|
23
|
+
expectedPriorEvidenceRoot: flag(
|
|
24
|
+
rest,
|
|
25
|
+
"expected-prior-evidence-root",
|
|
26
|
+
environment.BUILDCHAIN_DEV_DELIVERY_EXPECTED_PRIOR_EVIDENCE_ROOT,
|
|
27
|
+
),
|
|
28
|
+
integrationProofPath: flag(
|
|
29
|
+
rest,
|
|
30
|
+
"integration-proof",
|
|
31
|
+
environment.BUILDCHAIN_DEV_DELIVERY_INTEGRATION_PROOF,
|
|
32
|
+
),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function authorityStateOptions(rest, environment) {
|
|
12
37
|
return {
|
|
13
|
-
command,
|
|
14
|
-
repository: flag(rest, "repository", environment.GITHUB_REPOSITORY),
|
|
15
38
|
branch: flag(
|
|
16
39
|
rest,
|
|
17
40
|
"branch",
|
|
@@ -27,6 +50,20 @@ export function devDeliveryCliOptions(args = [], environment = process.env) {
|
|
|
27
50
|
"expected-old",
|
|
28
51
|
environment.BUILDCHAIN_DEV_DELIVERY_EXPECTED_OLD,
|
|
29
52
|
),
|
|
53
|
+
legacyTerminalRecoveryPath: flag(
|
|
54
|
+
rest,
|
|
55
|
+
"legacy-terminal-recovery",
|
|
56
|
+
environment.BUILDCHAIN_DEV_DELIVERY_LEGACY_TERMINAL_RECOVERY,
|
|
57
|
+
),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function devDeliveryCliOptions(args = [], environment = process.env) {
|
|
62
|
+
const [command = "", ...rest] = args;
|
|
63
|
+
return {
|
|
64
|
+
command,
|
|
65
|
+
repository: flag(rest, "repository", environment.GITHUB_REPOSITORY),
|
|
66
|
+
...authorityStateOptions(rest, environment),
|
|
30
67
|
pullRequestNumber: flag(
|
|
31
68
|
rest,
|
|
32
69
|
"pull-request",
|
|
@@ -196,17 +233,7 @@ export function devDeliveryCliOptions(args = [], environment = process.env) {
|
|
|
196
233
|
"lease-seconds",
|
|
197
234
|
environment.BUILDCHAIN_DEV_DELIVERY_LEASE_SECONDS,
|
|
198
235
|
),
|
|
199
|
-
|
|
200
|
-
eventAction: flag(
|
|
201
|
-
rest,
|
|
202
|
-
"event-action",
|
|
203
|
-
environment.BUILDCHAIN_DEV_DELIVERY_EVENT_ACTION,
|
|
204
|
-
),
|
|
205
|
-
evidenceRoot: flag(
|
|
206
|
-
rest,
|
|
207
|
-
"evidence-root",
|
|
208
|
-
environment.BUILDCHAIN_DEV_DELIVERY_EVIDENCE_ROOT,
|
|
209
|
-
),
|
|
236
|
+
...terminalEvidenceOptions(rest, environment),
|
|
210
237
|
transferRoot: flag(rest, "transfer-root"),
|
|
211
238
|
finalizerBoundaryRoot: flag(rest, "finalizer-boundary-root"),
|
|
212
239
|
nativeJobId: flag(rest, "native-job-id"),
|
|
@@ -10,10 +10,12 @@ import {
|
|
|
10
10
|
observeDevDeliveryQueue,
|
|
11
11
|
qualifyDevDeliveryWarrant,
|
|
12
12
|
recoverExpiredDevDeliveryWarrant,
|
|
13
|
+
reconcileDevDeliveryTerminalEvidence,
|
|
13
14
|
selectDevDeliveryWarrant,
|
|
14
15
|
settleDevDeliveryTerminalEvent,
|
|
15
16
|
submitDevDeliveryCandidate,
|
|
16
17
|
} from "../packages/core/dev-delivery-warrant.js";
|
|
18
|
+
import { recoverLegacyTerminalDevDeliveryQueue } from "../packages/core/dev-delivery-warrant-legacy-recovery.js";
|
|
17
19
|
import { runV4DeliveryWarrantReadCandidate } from "../packages/core/v4-delivery-warrant-read-candidate.js";
|
|
18
20
|
|
|
19
21
|
import { GitHubDevDeliveryStore } from "./dev-delivery-warrant-store.mjs";
|
|
@@ -52,6 +54,45 @@ function exactSha(value, label) {
|
|
|
52
54
|
return normalized;
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
function terminalSourceHead(options) {
|
|
58
|
+
return exactSha(
|
|
59
|
+
options.expectedSourceHead || options.sourceHead,
|
|
60
|
+
"sourceHead",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function reconcileTerminalEvidenceCommand(queue, options) {
|
|
65
|
+
return reconcileDevDeliveryTerminalEvidence(
|
|
66
|
+
queue,
|
|
67
|
+
{
|
|
68
|
+
candidateId: exactRoot(options.candidateId, "candidateId"),
|
|
69
|
+
expectedPriorEvidenceRoot: exactRoot(
|
|
70
|
+
options.expectedPriorEvidenceRoot,
|
|
71
|
+
"expectedPriorEvidenceRoot",
|
|
72
|
+
),
|
|
73
|
+
integrationProof:
|
|
74
|
+
options.integrationProof ||
|
|
75
|
+
jsonFile(options.integrationProofPath, "integration proof"),
|
|
76
|
+
reason: options.reason,
|
|
77
|
+
},
|
|
78
|
+
{ now: options.now },
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function requireTerminalEvidenceCas(options) {
|
|
83
|
+
if (
|
|
84
|
+
["reconcile-terminal-evidence", "recover-legacy-terminal"].includes(
|
|
85
|
+
options.command,
|
|
86
|
+
) &&
|
|
87
|
+
options.execute &&
|
|
88
|
+
!options.expectedOldStateRoot
|
|
89
|
+
) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
"terminal evidence reconciliation execute requires expected-old CAS",
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
55
96
|
function normalizeRepository(value) {
|
|
56
97
|
const normalized = text(value);
|
|
57
98
|
const match = normalized.match(/^([^/\s]+)\/([^/\s]+)$/);
|
|
@@ -229,6 +270,13 @@ function transitionFor(command, queue, options) {
|
|
|
229
270
|
}
|
|
230
271
|
if (command === "recover")
|
|
231
272
|
return recoverExpiredDevDeliveryWarrant(queue, { now: options.now });
|
|
273
|
+
if (command === "recover-legacy-terminal") {
|
|
274
|
+
return recoverLegacyTerminalDevDeliveryQueue(
|
|
275
|
+
queue,
|
|
276
|
+
jsonFile(options.legacyTerminalRecoveryPath, "legacy terminal recovery"),
|
|
277
|
+
{ now: options.now },
|
|
278
|
+
);
|
|
279
|
+
}
|
|
232
280
|
if (command === "close") {
|
|
233
281
|
return closeDevDeliveryWarrant(queue, warrantIdentity(queue, options), {
|
|
234
282
|
outcome: options.outcome,
|
|
@@ -245,10 +293,7 @@ function transitionFor(command, queue, options) {
|
|
|
245
293
|
options.pullRequestNumber,
|
|
246
294
|
"pullRequestNumber",
|
|
247
295
|
),
|
|
248
|
-
sourceHead:
|
|
249
|
-
options.expectedSourceHead || options.sourceHead,
|
|
250
|
-
"sourceHead",
|
|
251
|
-
),
|
|
296
|
+
sourceHead: terminalSourceHead(options),
|
|
252
297
|
fencingToken: options.fencingToken,
|
|
253
298
|
leaseGeneration: options.leaseGeneration,
|
|
254
299
|
outcome: options.outcome,
|
|
@@ -263,6 +308,9 @@ function transitionFor(command, queue, options) {
|
|
|
263
308
|
{ now: options.now },
|
|
264
309
|
);
|
|
265
310
|
}
|
|
311
|
+
if (command === "reconcile-terminal-evidence") {
|
|
312
|
+
return reconcileTerminalEvidenceCommand(queue, options);
|
|
313
|
+
}
|
|
266
314
|
if (command === "cancel-queued") {
|
|
267
315
|
return cancelQueuedDevDeliveryCandidate(
|
|
268
316
|
queue,
|
|
@@ -356,6 +404,7 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
|
|
|
356
404
|
now: new Date(optionsInput.now || Date.now()).toISOString(),
|
|
357
405
|
execute: bool(optionsInput.execute, false),
|
|
358
406
|
};
|
|
407
|
+
requireTerminalEvidenceCas(options);
|
|
359
408
|
if (
|
|
360
409
|
options.command === "submit" &&
|
|
361
410
|
options.execute &&
|
|
@@ -377,7 +426,9 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
|
|
|
377
426
|
stateRef: options.stateRef,
|
|
378
427
|
protectedBase: options.branch,
|
|
379
428
|
now: options.now,
|
|
380
|
-
allowLegacyV3Readback:
|
|
429
|
+
allowLegacyV3Readback: ["observe", "recover-legacy-terminal"].includes(
|
|
430
|
+
options.command,
|
|
431
|
+
),
|
|
381
432
|
});
|
|
382
433
|
if (
|
|
383
434
|
options.expectedOldStateRoot &&
|
|
@@ -457,7 +508,7 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
|
|
|
457
508
|
}
|
|
458
509
|
|
|
459
510
|
function usage() {
|
|
460
|
-
return "Usage:\n buildchain dev warrant <submit|select|heartbeat|qualify|recover|close|settle|cancel-queued|observe> --repository owner/repo --branch dev/vN/vN.M [--execute] [--output FILE] [--json]\n\nRead candidate:\n observe --read-mode v4 --read-qualification FILE --read-qualification-root sha256:... --read-typescript-revision SHA --read-rust-revision SHA --read-validator-version TOKEN [--read-evidence-output FILE]\n";
|
|
511
|
+
return "Usage:\n buildchain dev warrant <submit|select|heartbeat|qualify|recover|recover-legacy-terminal|close|settle|reconcile-terminal-evidence|cancel-queued|observe> --repository owner/repo --branch dev/vN/vN.M [--execute] [--output FILE] [--json]\n\nLegacy terminal recovery:\n recover-legacy-terminal --expected-old sha256:... --legacy-terminal-recovery FILE [--execute]\n\nRead candidate:\n observe --read-mode v4 --read-qualification FILE --read-qualification-root sha256:... --read-typescript-revision SHA --read-rust-revision SHA --read-validator-version TOKEN [--read-evidence-output FILE]\n";
|
|
461
512
|
}
|
|
462
513
|
|
|
463
514
|
async function main() {
|
|
@@ -474,8 +525,10 @@ async function main() {
|
|
|
474
525
|
"heartbeat",
|
|
475
526
|
"qualify",
|
|
476
527
|
"recover",
|
|
528
|
+
"recover-legacy-terminal",
|
|
477
529
|
"close",
|
|
478
530
|
"settle",
|
|
531
|
+
"reconcile-terminal-evidence",
|
|
479
532
|
"cancel-queued",
|
|
480
533
|
"observe",
|
|
481
534
|
].includes(options.command)
|
|
@@ -3,7 +3,7 @@ import crypto from "node:crypto";
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import { admitExistingQueueEntry, createDevPrAdmissionReceipt, qualifyAtomicQueueAdmission, readDeliveryWarrantResult, runSourceQualification, runTargetedQueueAdmission, setActiveLeaseStatus, verifyCurrentDeliveryWarrant } from "./dev-pr-delivery-warrant.mjs";
|
|
6
|
+
import { admitExistingQueueEntry, createDevPrAdmissionReceipt, createPreReadinessQueueFence, qualifyAtomicQueueAdmission, readDeliveryWarrantResult, runSourceQualification, runTargetedQueueAdmission, setActiveLeaseStatus, setQueueAdmissionStatus, verifyCurrentDeliveryWarrant } from "./dev-pr-delivery-warrant.mjs";
|
|
7
7
|
import { projectCutQualification } from "./dev-pr-prequeue-guard.mjs";
|
|
8
8
|
const DEFAULT_BLOCK_LABELS = ["blocked", "do-not-merge", "work-in-progress"];
|
|
9
9
|
const DEFAULT_ALLOWED_HEAD_PREFIXES = ["feature/", "fix/", "chore/", "docs/", "ci/", "refactor/"];
|
|
@@ -197,12 +197,6 @@ function mergeableAccepted(pr, landingMode = "direct", projectCutQualified = fal
|
|
|
197
197
|
function rejectBaseMoveBeforeAtomicReplay(options, initialBaseSha, observedBaseSha) {
|
|
198
198
|
return observedBaseSha !== initialBaseSha && options.warrantMode !== "required";
|
|
199
199
|
}
|
|
200
|
-
async function setQueueAdmissionStatus(client, repository, sha, context, state) {
|
|
201
|
-
if (!context) return null;
|
|
202
|
-
await client.request("POST", `/repos/${repository.owner}/${repository.repo}/statuses/${sha}`, { body: { state, context, description: state === "success" ? "Buildchain admitted this exact PR head to the merge queue" : "Buildchain rejected merge queue admission for this exact PR head" } });
|
|
203
|
-
return { context, state, sha };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
200
|
function skip(reason, details = {}) {
|
|
207
201
|
return { action: "skip", reason, ...details };
|
|
208
202
|
}
|
|
@@ -790,8 +784,10 @@ export async function runDevPrAdmission(optionsInput = {}, clientInput) {
|
|
|
790
784
|
return targetedFailure({ options, pr: {}, state: "missing", reason: "pull-request-not-found" });
|
|
791
785
|
}
|
|
792
786
|
|
|
787
|
+
const readinessFence = createPreReadinessQueueFence({ client, options, sleep: delay });
|
|
793
788
|
const reject = async (state, reason, readiness = { observed: hasReadyLabel(pr, options.readyLabel), established: false }) => {
|
|
794
789
|
const result = targetedFailure({ options, pr, state, reason, readiness });
|
|
790
|
+
await readinessFence.finish(result);
|
|
795
791
|
if (!options.dryRun) result.diagnostic = await publishAdmissionDiagnostic(client, options, result.receipt, result.receiptRoot);
|
|
796
792
|
return result;
|
|
797
793
|
};
|
|
@@ -803,6 +799,12 @@ export async function runDevPrAdmission(optionsInput = {}, clientInput) {
|
|
|
803
799
|
if (pr.draft) return reject("blocked", "draft");
|
|
804
800
|
if (!headPrefixAllowed(pr, options.allowedHeadPrefixes)) return reject("blocked", "head-prefix-not-allowed");
|
|
805
801
|
|
|
802
|
+
try {
|
|
803
|
+
await readinessFence.establish();
|
|
804
|
+
} catch (error) {
|
|
805
|
+
return reject("blocked", error.code || "pre-readiness-queue-fence-failed");
|
|
806
|
+
}
|
|
807
|
+
|
|
806
808
|
let readiness = { observed: hasReadyLabel(pr, options.readyLabel), established: false };
|
|
807
809
|
if (!readiness.observed) {
|
|
808
810
|
if (options.dryRun) return reject("rejected", "missing-ready-label", readiness);
|
|
@@ -825,7 +827,7 @@ export async function runDevPrAdmission(optionsInput = {}, clientInput) {
|
|
|
825
827
|
}
|
|
826
828
|
|
|
827
829
|
if (options.qualificationOnly) {
|
|
828
|
-
|
|
830
|
+
const result = await runSourceQualification({
|
|
829
831
|
options, pullRequest: pr, readiness, client, reject,
|
|
830
832
|
evaluate: evaluatePullRequest,
|
|
831
833
|
admissionState: admissionStateFor,
|
|
@@ -833,6 +835,7 @@ export async function runDevPrAdmission(optionsInput = {}, clientInput) {
|
|
|
833
835
|
root: contentRoot,
|
|
834
836
|
publishDiagnostic: publishAdmissionDiagnostic,
|
|
835
837
|
});
|
|
838
|
+
return readinessFence.finish(result);
|
|
836
839
|
}
|
|
837
840
|
|
|
838
841
|
const initialQueue = await client.getMergeQueueState(options.targetBranch);
|
|
@@ -844,6 +847,7 @@ export async function runDevPrAdmission(optionsInput = {}, clientInput) {
|
|
|
844
847
|
}
|
|
845
848
|
const matchingEntry = initialQueue.entries.find((entry) =>
|
|
846
849
|
entry.pullRequestNumber === pr.number && entry.pullRequestHeadSha === options.expectedHeadSha);
|
|
850
|
+
await readinessFence.qualifyExisting(matchingEntry);
|
|
847
851
|
const existing = await admitExistingQueueEntry({
|
|
848
852
|
options, pullRequest: pr, readiness, client, entry: matchingEntry, warrant,
|
|
849
853
|
createReceipt: createAdmissionReceipt,
|
|
@@ -852,7 +856,7 @@ export async function runDevPrAdmission(optionsInput = {}, clientInput) {
|
|
|
852
856
|
});
|
|
853
857
|
if (existing) return existing;
|
|
854
858
|
|
|
855
|
-
|
|
859
|
+
const result = await runTargetedQueueAdmission({
|
|
856
860
|
options, pullRequest: pr, readiness, client, warrant,
|
|
857
861
|
runController: runDevPrAutoMerge,
|
|
858
862
|
admissionState: admissionStateFor,
|
|
@@ -860,6 +864,7 @@ export async function runDevPrAdmission(optionsInput = {}, clientInput) {
|
|
|
860
864
|
root: contentRoot,
|
|
861
865
|
publishDiagnostic: publishAdmissionDiagnostic,
|
|
862
866
|
});
|
|
867
|
+
return readinessFence.finish(result);
|
|
863
868
|
}
|
|
864
869
|
|
|
865
870
|
function finalizePatrolResult(result) {
|
|
@@ -1132,6 +1137,11 @@ function cliFlag(args, name) {
|
|
|
1132
1137
|
|
|
1133
1138
|
export function cliOptions(args = [], environment = process.env) {
|
|
1134
1139
|
const targetPullRequestNumber = cliValue(args, "pull-request", environment.BUILDCHAIN_DEV_PR_EXPECTED_PR_NUMBER);
|
|
1140
|
+
const warrantMode = cliValue(
|
|
1141
|
+
args,
|
|
1142
|
+
"warrant-mode",
|
|
1143
|
+
environment.BUILDCHAIN_DEV_PR_WARRANT_MODE || environment.WARRANT_MODE,
|
|
1144
|
+
);
|
|
1135
1145
|
return {
|
|
1136
1146
|
repository: cliValue(args, "repository", environment.BUILDCHAIN_DEV_PR_REPOSITORY || environment.GITHUB_REPOSITORY),
|
|
1137
1147
|
targetBranch: cliValue(args, "branch", environment.BUILDCHAIN_DEV_PR_TARGET_BRANCH || environment.GITHUB_REF_NAME),
|
|
@@ -1141,10 +1151,14 @@ export function cliOptions(args = [], environment = process.env) {
|
|
|
1141
1151
|
blockLabels: cliValue(args, "block-labels", environment.BUILDCHAIN_DEV_PR_BLOCK_LABELS),
|
|
1142
1152
|
allowedHeadPrefixes: cliValue(args, "allowed-head-prefixes", environment.BUILDCHAIN_DEV_PR_ALLOWED_HEAD_PREFIXES),
|
|
1143
1153
|
requiredChecks: cliValue(args, "required-checks", environment.BUILDCHAIN_DEV_PR_REQUIRED_CHECKS),
|
|
1144
|
-
queueAdmissionContext: cliValue(
|
|
1154
|
+
queueAdmissionContext: cliValue(
|
|
1155
|
+
args,
|
|
1156
|
+
"queue-admission-context",
|
|
1157
|
+
environment.BUILDCHAIN_DEV_PR_QUEUE_ADMISSION_CONTEXT || (warrantMode === "required" ? "Queue admission lease" : ""),
|
|
1158
|
+
),
|
|
1145
1159
|
activeLeaseContext: cliValue(args, "active-lease-context", environment.BUILDCHAIN_DEV_PR_ACTIVE_LEASE_CONTEXT),
|
|
1146
1160
|
diagnosticContext: cliValue(args, "diagnostic-context", environment.BUILDCHAIN_DEV_PR_DIAGNOSTIC_CONTEXT),
|
|
1147
|
-
warrantMode
|
|
1161
|
+
warrantMode,
|
|
1148
1162
|
warrantResultPath: cliValue(args, "warrant-result", environment.BUILDCHAIN_DEV_PR_WARRANT_RESULT_PATH),
|
|
1149
1163
|
projectCutProofPath: cliValue(args, "project-cut-proof", environment.BUILDCHAIN_DEV_PR_PROJECT_CUT_PROOF_PATH),
|
|
1150
1164
|
sourceProofPath: cliValue(args, "source-proof", environment.BUILDCHAIN_DEV_PR_SOURCE_PROOF_PATH),
|
|
@@ -206,6 +206,50 @@ export async function setActiveLeaseStatus(client, repository, sha, context, sta
|
|
|
206
206
|
return { context, state, sha };
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
export async function setQueueAdmissionStatus(client, repository, sha, context, state) {
|
|
210
|
+
if (!context) return null;
|
|
211
|
+
const descriptions = {
|
|
212
|
+
pending: "Buildchain is awaiting a qualified Warrant for this exact PR head",
|
|
213
|
+
success: "Buildchain admitted this exact PR head to the merge queue",
|
|
214
|
+
failure: "Buildchain rejected merge queue admission for this exact PR head",
|
|
215
|
+
};
|
|
216
|
+
await client.request("POST", `/repos/${repository.owner}/${repository.repo}/statuses/${sha}`, {
|
|
217
|
+
body: { state, context, description: descriptions[state] || descriptions.failure },
|
|
218
|
+
});
|
|
219
|
+
return { context, state, sha };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function createPreReadinessQueueFence({ client, options, sleep }) {
|
|
223
|
+
let status = null;
|
|
224
|
+
const write = async (state) => {
|
|
225
|
+
status = await setQueueAdmissionStatus(client, options.repository, options.expectedHeadSha, options.queueAdmissionContext, state);
|
|
226
|
+
return status;
|
|
227
|
+
};
|
|
228
|
+
return {
|
|
229
|
+
async establish() {
|
|
230
|
+
if (options.dryRun || options.warrantMode !== "required") return null;
|
|
231
|
+
if (!options.queueAdmissionContext) mismatch("queue-admission-context-required-before-readiness");
|
|
232
|
+
await write("pending");
|
|
233
|
+
for (let attempt = 1; attempt <= Math.max(1, options.pollMergeableAttempts); attempt += 1) {
|
|
234
|
+
const checks = await client.listCommitChecks(options.expectedHeadSha);
|
|
235
|
+
const readback = (checks?.statuses || []).find((entry) => entry.context === options.queueAdmissionContext);
|
|
236
|
+
if (readback?.state === "pending") return { ...status, readbackAttempts: attempt };
|
|
237
|
+
if (attempt < options.pollMergeableAttempts) await sleep(options.pollMergeableDelayMs);
|
|
238
|
+
}
|
|
239
|
+
await write("failure");
|
|
240
|
+
mismatch("pre-readiness-queue-fence-readback-mismatch");
|
|
241
|
+
},
|
|
242
|
+
async qualifyExisting(entry) {
|
|
243
|
+
if (entry && status?.state === "pending") await write("success");
|
|
244
|
+
},
|
|
245
|
+
async finish(result) {
|
|
246
|
+
const controllerFailed = result.controller?.evaluated?.some((entry) => entry.queueAdmissionStatus?.state === "failure");
|
|
247
|
+
if (!result.ok && status?.state === "pending" && !controllerFailed) await write("failure");
|
|
248
|
+
return result;
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
209
253
|
async function readBackAdmissionStatuses(client, options, expectedHeadSha, sleep) {
|
|
210
254
|
let last = { queueAdmission: null, activeLease: null };
|
|
211
255
|
const attempts = Math.max(1, options.pollMergeableAttempts);
|
|
@@ -164,15 +164,6 @@ async function qualifyProjectCut({
|
|
|
164
164
|
|
|
165
165
|
let reuseDecision = null;
|
|
166
166
|
if (currentBaseSha !== previousBaseSha) {
|
|
167
|
-
const observedPullRequestBase = String(
|
|
168
|
-
observedPullRequest.base?.sha || "",
|
|
169
|
-
).toLowerCase();
|
|
170
|
-
if (observedPullRequestBase !== currentBaseSha) {
|
|
171
|
-
mismatch("pre-enqueue-project-cut-base-stale", {
|
|
172
|
-
observedPullRequestBase,
|
|
173
|
-
currentBaseSha,
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
167
|
const proof = readOptionalJson(options.sourceProofPath);
|
|
177
168
|
if (!proof) mismatch("pre-enqueue-base-attribution-unknown");
|
|
178
169
|
if (
|
|
@@ -342,15 +333,6 @@ export async function qualifyPreEnqueueReadback({
|
|
|
342
333
|
) {
|
|
343
334
|
mismatch("head-sha-drift-after-project-cut");
|
|
344
335
|
}
|
|
345
|
-
const casPullRequestBase = String(
|
|
346
|
-
casPullRequest.base?.sha || "",
|
|
347
|
-
).toLowerCase();
|
|
348
|
-
if (casPullRequestBase && casPullRequestBase !== casBaseSha) {
|
|
349
|
-
mismatch("pre-enqueue-project-cut-base-stale", {
|
|
350
|
-
observedPullRequestBase: casPullRequestBase,
|
|
351
|
-
currentBaseSha: casBaseSha,
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
336
|
if (casPullRequest.mergeable === false) {
|
|
355
337
|
mismatch("pre-enqueue-merge-conflict-after-project-cut");
|
|
356
338
|
}
|
|
@@ -24,7 +24,7 @@ const AUTHORITY_WORKFLOW = "artifact-signing-authority.yml";
|
|
|
24
24
|
|
|
25
25
|
export function resolveAuthorityDispatchRef(value) {
|
|
26
26
|
const ref = required(value, "authority ref");
|
|
27
|
-
return /^[0-9a-f]{40}$/u.test(ref)
|
|
27
|
+
return /^[0-9a-f]{40}$/u.test(ref) || /^v\d+(?:\.\d+)?(?:-alpha)?$/u.test(ref)
|
|
28
28
|
? DEFAULT_ARTIFACT_SIGNING_AUTHORITY_REF
|
|
29
29
|
: ref;
|
|
30
30
|
}
|