@kungfu-tech/buildchain 4.1.0 → 4.1.1-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/architecture/maintainability-debt.json +3 -3
- package/architecture/release-topology.json +3 -1
- package/dist/site/buildchain-contract.json +3 -3
- package/dist/site/buildchain-site.json +9 -9
- package/dist/site/kfd-claims.json +2 -2
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +1 -1
- package/dist/site/page-registry.json +4 -4
- package/dist/site/public-surface-audit.json +2 -2
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +5 -5
- package/dist/site/workflow-registry.json +4 -4
- package/docs/release-governance.md +18 -17
- package/docs/stable-candidate-patrol.md +29 -28
- package/package.json +1 -1
- package/packages/core/providers/github/graphql-errors.js +34 -0
- package/packages/core/providers/github/qualification-artifacts.js +88 -0
- package/packages/core/providers/github-cli-api.js +55 -21
- package/packages/core/release/commands/stable-release-gate.mjs +7 -1
- package/packages/core/release/next-development/enqueue.js +2 -3
- package/packages/core/release/promote-candidate/next-development-queue-observation.js +25 -0
- package/packages/core/release/promote-candidate/next-development-queue.js +32 -0
- package/packages/core/release/qualification/canary-evidence.js +89 -0
- package/packages/core/release/stable-release-gate.js +3 -3
- package/scripts/check-inventory.mjs +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kungfu-tech/buildchain",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.1-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,34 @@
|
|
|
1
|
+
export function createGitHubGraphqlError(errors) {
|
|
2
|
+
const details = errors.map((error) => ({
|
|
3
|
+
message: typeof error.message === "string" ? error.message : "",
|
|
4
|
+
...(error.type ? { type: error.type } : {}),
|
|
5
|
+
...(error.extensions?.code
|
|
6
|
+
? { extensions: { code: error.extensions.code } }
|
|
7
|
+
: {}),
|
|
8
|
+
}));
|
|
9
|
+
return Object.assign(
|
|
10
|
+
new Error(
|
|
11
|
+
details
|
|
12
|
+
.map((error) => error.message)
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
.join("; ") || "GitHub GraphQL request failed",
|
|
15
|
+
),
|
|
16
|
+
{ errors: details },
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isTransientGitHubGraphqlError(error) {
|
|
21
|
+
const errors = error?.errors;
|
|
22
|
+
return (
|
|
23
|
+
Array.isArray(errors) &&
|
|
24
|
+
errors.length > 0 &&
|
|
25
|
+
errors.every((detail) => {
|
|
26
|
+
const code = detail.type || detail.extensions?.code;
|
|
27
|
+
return code
|
|
28
|
+
? code === "INTERNAL"
|
|
29
|
+
: /^Something went wrong while executing your query on /u.test(
|
|
30
|
+
detail.message || "",
|
|
31
|
+
);
|
|
32
|
+
})
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
const LIMIT = 8 * 1024 * 1024;
|
|
5
|
+
|
|
6
|
+
export function readPublicBuildArchive(archive, digest) {
|
|
7
|
+
if (
|
|
8
|
+
archive.length > LIMIT ||
|
|
9
|
+
`sha256:${createHash("sha256").update(archive).digest("hex")}` !== digest
|
|
10
|
+
)
|
|
11
|
+
throw new Error("public build artifact digest or size mismatch");
|
|
12
|
+
const script = [
|
|
13
|
+
"import io,sys,zipfile",
|
|
14
|
+
"z=zipfile.ZipFile(io.BytesIO(sys.stdin.buffer.read()))",
|
|
15
|
+
"entries=[e for e in z.infolist() if e.filename=='build-summary.json']",
|
|
16
|
+
"assert len(entries)==1 and entries[0].file_size <= 1048576, 'invalid public build archive'",
|
|
17
|
+
"sys.stdout.buffer.write(z.read(entries[0]))",
|
|
18
|
+
].join("\n");
|
|
19
|
+
return JSON.parse(
|
|
20
|
+
execFileSync(
|
|
21
|
+
process.platform === "win32" ? "python" : "python3",
|
|
22
|
+
["-c", script],
|
|
23
|
+
{ input: archive, encoding: "utf8", maxBuffer: 1048576 },
|
|
24
|
+
),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function fetchQualificationArchive({
|
|
29
|
+
apiUrl,
|
|
30
|
+
token,
|
|
31
|
+
endpoint,
|
|
32
|
+
fetchImpl,
|
|
33
|
+
}) {
|
|
34
|
+
let response = await fetchImpl(`${apiUrl.replace(/\/+$/, "")}${endpoint}`, {
|
|
35
|
+
redirect: "manual",
|
|
36
|
+
headers: {
|
|
37
|
+
accept: "application/vnd.github+json",
|
|
38
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
if (response.status === 302) {
|
|
42
|
+
const location = new URL(response.headers.get("location"));
|
|
43
|
+
if (location.protocol !== "https:")
|
|
44
|
+
throw new Error("invalid artifact download redirect");
|
|
45
|
+
response = await fetchImpl(location.href, { redirect: "error" });
|
|
46
|
+
}
|
|
47
|
+
if (!response.ok)
|
|
48
|
+
throw new Error(
|
|
49
|
+
`public build artifact download failed: ${response.status}`,
|
|
50
|
+
);
|
|
51
|
+
if (Number(response.headers.get("content-length")) > LIMIT)
|
|
52
|
+
throw new Error("public build archive exceeds size limit");
|
|
53
|
+
const chunks = [];
|
|
54
|
+
let size = 0;
|
|
55
|
+
for await (const chunk of response.body) {
|
|
56
|
+
size += chunk.length;
|
|
57
|
+
if (size > LIMIT)
|
|
58
|
+
throw new Error("public build archive exceeds size limit");
|
|
59
|
+
chunks.push(chunk);
|
|
60
|
+
}
|
|
61
|
+
return Buffer.concat(chunks);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function readPublicBuildArtifact({
|
|
65
|
+
api,
|
|
66
|
+
fetchArchive,
|
|
67
|
+
repository,
|
|
68
|
+
run,
|
|
69
|
+
}) {
|
|
70
|
+
const prefix = `/repos/${repository}/actions`;
|
|
71
|
+
const assets = await api(`${prefix}/runs/${run.id}/artifacts?per_page=100`);
|
|
72
|
+
const matches = (assets.artifacts || []).filter(
|
|
73
|
+
(asset) => asset.name === `buildchain-summary-${run.head_sha}`,
|
|
74
|
+
);
|
|
75
|
+
if (
|
|
76
|
+
matches.length !== 1 ||
|
|
77
|
+
matches[0].expired ||
|
|
78
|
+
matches[0].size_in_bytes > LIMIT
|
|
79
|
+
)
|
|
80
|
+
throw new Error(
|
|
81
|
+
"public build summary artifact missing, ambiguous or expired",
|
|
82
|
+
);
|
|
83
|
+
const asset = matches[0];
|
|
84
|
+
return readPublicBuildArchive(
|
|
85
|
+
await fetchArchive(`${prefix}/artifacts/${asset.id}/zip`),
|
|
86
|
+
asset.digest,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -1,26 +1,60 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createGitHubGraphqlError } from "./github/graphql-errors.js";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
function apiFailure(cause) {
|
|
5
|
+
let response;
|
|
6
|
+
try {
|
|
7
|
+
response = JSON.parse(String(cause.stdout || ""));
|
|
8
|
+
} catch {
|
|
9
|
+
// Process diagnostics are not a provider response and may contain secrets.
|
|
10
|
+
}
|
|
11
|
+
const messages = Array.isArray(response?.errors)
|
|
12
|
+
? response.errors
|
|
13
|
+
.map((error) => error.message)
|
|
14
|
+
.filter((message) => typeof message === "string")
|
|
15
|
+
: [];
|
|
16
|
+
const message =
|
|
17
|
+
messages.join("; ") ||
|
|
18
|
+
(typeof response?.message === "string" ? response.message : "") ||
|
|
19
|
+
`GitHub API command failed with exit code ${cause.status ?? 1}`;
|
|
20
|
+
const error = messages.length
|
|
21
|
+
? createGitHubGraphqlError(response.errors)
|
|
22
|
+
: new Error(message);
|
|
23
|
+
error.exitCode = cause.status ?? 1;
|
|
24
|
+
const status = Number(response?.status);
|
|
25
|
+
if (Number.isInteger(status) && status >= 100 && status <= 599)
|
|
26
|
+
error.status = status;
|
|
27
|
+
if (cause.code) error.code = cause.code;
|
|
28
|
+
return error;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createGitHubCliApi(execute = execFileSync, env = process.env) {
|
|
4
32
|
function invoke(method, endpoint, body, flags = []) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
let output;
|
|
34
|
+
try {
|
|
35
|
+
output = execute(
|
|
36
|
+
"gh",
|
|
37
|
+
[
|
|
38
|
+
"api",
|
|
39
|
+
"--method",
|
|
40
|
+
method,
|
|
41
|
+
endpoint,
|
|
42
|
+
"-H",
|
|
43
|
+
"Accept: application/vnd.github+json",
|
|
44
|
+
...flags,
|
|
45
|
+
...(body === undefined ? [] : ["--input", "-"]),
|
|
46
|
+
],
|
|
47
|
+
{
|
|
48
|
+
env,
|
|
49
|
+
encoding: "utf8",
|
|
50
|
+
input: body === undefined ? undefined : JSON.stringify(body),
|
|
51
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
52
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw apiFailure(error);
|
|
57
|
+
}
|
|
24
58
|
return output?.trim() ? JSON.parse(output) : {};
|
|
25
59
|
}
|
|
26
60
|
return {
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { fetchQualificationArchive } from "../../providers/github/qualification-artifacts.js";
|
|
6
|
+
import { resolvePublicBuildCanaryEvidence } from "../qualification/canary-evidence.js";
|
|
5
7
|
import {
|
|
6
8
|
assertStableReleaseGate,
|
|
7
9
|
evaluateStableReleaseGate,
|
|
@@ -278,12 +280,16 @@ export async function collectStableReleaseGateReport({
|
|
|
278
280
|
const canaries = await resolveCanaryEvidence({
|
|
279
281
|
api,
|
|
280
282
|
repository,
|
|
281
|
-
policy,
|
|
283
|
+
policy: { ...policy, requiredCanaries: policy.requiredCanaries.filter((entry) => entry.source !== "public-build") },
|
|
282
284
|
candidateTag: candidate.tag,
|
|
283
285
|
candidateSha,
|
|
284
286
|
releaseCandidateRunId,
|
|
285
287
|
releaseCandidateRunUrl,
|
|
286
288
|
});
|
|
289
|
+
canaries.push(...await resolvePublicBuildCanaryEvidence({
|
|
290
|
+
api, policy, candidateSha, repository: repository.fullName,
|
|
291
|
+
fetchArchive: (endpoint) => fetchQualificationArchive({ apiUrl, token, endpoint, fetchImpl }),
|
|
292
|
+
}));
|
|
287
293
|
return assertStableReleaseGate({
|
|
288
294
|
policy,
|
|
289
295
|
channel,
|
|
@@ -2,6 +2,7 @@ import { requireCurrentIndependentApproval } from "./approval.js";
|
|
|
2
2
|
import { REVIEWER } from "./review-policy.js";
|
|
3
3
|
import { observe } from "./observation.js";
|
|
4
4
|
import { enqueueNextDevelopmentPullRequest } from "../promote-candidate/next-development-queue.js";
|
|
5
|
+
import { createGitHubGraphqlError } from "../../providers/github/graphql-errors.js";
|
|
5
6
|
export async function enqueueVerifiedDevelopmentReview({
|
|
6
7
|
client,
|
|
7
8
|
repository,
|
|
@@ -39,9 +40,7 @@ export async function enqueueVerifiedDevelopmentReview({
|
|
|
39
40
|
graphql: async (query, variables) => {
|
|
40
41
|
const result = client.post("graphql", { query, variables });
|
|
41
42
|
if (result.errors?.length)
|
|
42
|
-
throw
|
|
43
|
-
result.errors.map((error) => error.message).join("; "),
|
|
44
|
-
);
|
|
43
|
+
throw createGitHubGraphqlError(result.errors);
|
|
45
44
|
return result.data;
|
|
46
45
|
},
|
|
47
46
|
},
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export async function observeNextDevelopmentQueue({
|
|
2
|
+
mutationOctokit,
|
|
3
|
+
pull,
|
|
4
|
+
headSha,
|
|
5
|
+
}) {
|
|
6
|
+
const { node } = await mutationOctokit.graphql(
|
|
7
|
+
`query BuildchainObserveQueuedPullRequest($id: ID!) {
|
|
8
|
+
node(id: $id) { ... on PullRequest {
|
|
9
|
+
id headRefOid baseRefName state merged mergeQueueEntry { id }
|
|
10
|
+
} }
|
|
11
|
+
}`,
|
|
12
|
+
{ id: pull.node_id },
|
|
13
|
+
);
|
|
14
|
+
if (
|
|
15
|
+
node?.id !== pull.node_id ||
|
|
16
|
+
node.headRefOid !== headSha ||
|
|
17
|
+
(pull.base?.ref && node.baseRefName !== pull.base.ref) ||
|
|
18
|
+
!["OPEN", "MERGED"].includes(node.state) ||
|
|
19
|
+
node.merged !== (node.state === "MERGED")
|
|
20
|
+
)
|
|
21
|
+
throw new Error(
|
|
22
|
+
"next-development queue readback cannot prove the exact open or merged pull request",
|
|
23
|
+
);
|
|
24
|
+
return node.merged || Boolean(node.mergeQueueEntry?.id);
|
|
25
|
+
}
|
|
@@ -4,6 +4,21 @@ const MAX_TRANSPORT_RETRIES = 4;
|
|
|
4
4
|
export function nextDevelopmentQueueFailure(error) {
|
|
5
5
|
const status = Number(error?.status || error?.response?.status || 0);
|
|
6
6
|
const message = String(error?.message || "");
|
|
7
|
+
if (
|
|
8
|
+
[401, 403, 404, 409, 422].includes(status) &&
|
|
9
|
+
!(status === 403 && /rate limit/iu.test(message))
|
|
10
|
+
)
|
|
11
|
+
return "rejected";
|
|
12
|
+
if (Array.isArray(error?.errors) && error.errors.length) {
|
|
13
|
+
if (isTransientGitHubGraphqlError(error)) return "transient";
|
|
14
|
+
const kinds = error.errors.map((detail) => {
|
|
15
|
+
const code = detail.type || detail.extensions?.code;
|
|
16
|
+
return code && code !== "UNPROCESSABLE"
|
|
17
|
+
? "rejected"
|
|
18
|
+
: nextDevelopmentQueueFailure({ message: detail.message });
|
|
19
|
+
});
|
|
20
|
+
return kinds.every((kind) => kind === kinds[0]) ? kinds[0] : "rejected";
|
|
21
|
+
}
|
|
7
22
|
if (/already.*queue|queue.*already/iu.test(message)) return "queued";
|
|
8
23
|
if (/already.*merged|merged.*already/iu.test(message)) return "merged";
|
|
9
24
|
if (
|
|
@@ -28,6 +43,16 @@ export function assertNextDevelopmentPull(pull, headSha, base) {
|
|
|
28
43
|
throw new Error("next-development pull request was closed without merge");
|
|
29
44
|
}
|
|
30
45
|
|
|
46
|
+
async function recoverQueuedMutation(options) {
|
|
47
|
+
try {
|
|
48
|
+
return await observeNextDevelopmentQueue(options);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (nextDevelopmentQueueFailure(error) !== "transient")
|
|
51
|
+
throw Object.assign(error, { releaseTailClass: "conflict" });
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
31
56
|
export async function enqueueNextDevelopmentPullRequest({
|
|
32
57
|
mutationOctokit,
|
|
33
58
|
pull,
|
|
@@ -48,6 +73,11 @@ export async function enqueueNextDevelopmentPullRequest({
|
|
|
48
73
|
} catch (error) {
|
|
49
74
|
const kind = nextDevelopmentQueueFailure(error);
|
|
50
75
|
if (kind === "queued" || kind === "merged") return;
|
|
76
|
+
if (
|
|
77
|
+
kind === "transient" &&
|
|
78
|
+
(await recoverQueuedMutation({ mutationOctokit, pull, headSha }))
|
|
79
|
+
)
|
|
80
|
+
return;
|
|
51
81
|
if (
|
|
52
82
|
kind === "rejected" ||
|
|
53
83
|
poll === maxPolls ||
|
|
@@ -61,3 +91,5 @@ export async function enqueueNextDevelopmentPullRequest({
|
|
|
61
91
|
}
|
|
62
92
|
}
|
|
63
93
|
}
|
|
94
|
+
import { isTransientGitHubGraphqlError } from "../../providers/github/graphql-errors.js";
|
|
95
|
+
import { observeNextDevelopmentQueue } from "./next-development-queue-observation.js";
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readPublicBuildArtifact } from "../../providers/github/qualification-artifacts.js";
|
|
2
|
+
import {
|
|
3
|
+
resolveStableCandidateQualificationCandidate,
|
|
4
|
+
validatePublicBuildRun,
|
|
5
|
+
} from "./public-build.js";
|
|
6
|
+
|
|
7
|
+
async function readCanary({
|
|
8
|
+
api,
|
|
9
|
+
fetchArchive,
|
|
10
|
+
repository,
|
|
11
|
+
candidateSha,
|
|
12
|
+
canary,
|
|
13
|
+
statuses,
|
|
14
|
+
}) {
|
|
15
|
+
const status = statuses.find((entry) => entry.context === canary.context);
|
|
16
|
+
const evidence = {
|
|
17
|
+
id: canary.id,
|
|
18
|
+
candidateSha,
|
|
19
|
+
status: "missing",
|
|
20
|
+
attestor: status?.creator?.login || "",
|
|
21
|
+
};
|
|
22
|
+
if (!status || status.state !== "success") return evidence;
|
|
23
|
+
const target =
|
|
24
|
+
/^https:\/\/github\.com\/([^/]+\/[^/]+)\/actions\/runs\/([1-9][0-9]*)\/?$/u.exec(
|
|
25
|
+
status.target_url || "",
|
|
26
|
+
);
|
|
27
|
+
if (!target || target[1] !== repository || canary.repository !== repository)
|
|
28
|
+
return { ...evidence, status: "mismatched" };
|
|
29
|
+
const prefix = `/repos/${repository}/actions`;
|
|
30
|
+
const run = await api(`${prefix}/runs/${target[2]}`);
|
|
31
|
+
const workflow = await api(`${prefix}/workflows/${run.workflow_id}`);
|
|
32
|
+
validatePublicBuildRun(run, workflow, repository);
|
|
33
|
+
if (
|
|
34
|
+
![workflow.name, workflow.path?.split("/").pop()].includes(canary.workflow)
|
|
35
|
+
)
|
|
36
|
+
throw new Error("public build canary workflow differs from policy");
|
|
37
|
+
const summary = await readPublicBuildArtifact({
|
|
38
|
+
api,
|
|
39
|
+
fetchArchive,
|
|
40
|
+
repository,
|
|
41
|
+
run,
|
|
42
|
+
});
|
|
43
|
+
const runtimeSha = resolveStableCandidateQualificationCandidate({
|
|
44
|
+
repositoryName: repository,
|
|
45
|
+
sourceRun: run,
|
|
46
|
+
buildSummary: summary,
|
|
47
|
+
});
|
|
48
|
+
if (runtimeSha !== candidateSha)
|
|
49
|
+
throw new Error("public build canary does not qualify the exact candidate");
|
|
50
|
+
return {
|
|
51
|
+
...evidence,
|
|
52
|
+
status: "success",
|
|
53
|
+
completedAt: run.updated_at,
|
|
54
|
+
evidenceUrl: status.target_url,
|
|
55
|
+
repository,
|
|
56
|
+
workflow: workflow.name,
|
|
57
|
+
workflowId: workflow.id,
|
|
58
|
+
runtimeRef: runtimeSha,
|
|
59
|
+
runtimeRefSource: "public-build-summary",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function resolvePublicBuildCanaryEvidence({
|
|
64
|
+
api,
|
|
65
|
+
fetchArchive,
|
|
66
|
+
repository,
|
|
67
|
+
candidateSha,
|
|
68
|
+
policy,
|
|
69
|
+
}) {
|
|
70
|
+
const canaries = policy.requiredCanaries.filter(
|
|
71
|
+
(entry) => entry.source === "public-build",
|
|
72
|
+
);
|
|
73
|
+
if (!canaries.length) return [];
|
|
74
|
+
const statuses = await api(
|
|
75
|
+
`/repos/${repository}/commits/${candidateSha}/statuses?per_page=100`,
|
|
76
|
+
);
|
|
77
|
+
return Promise.all(
|
|
78
|
+
canaries.map((canary) =>
|
|
79
|
+
readCanary({
|
|
80
|
+
api,
|
|
81
|
+
fetchArchive,
|
|
82
|
+
repository,
|
|
83
|
+
candidateSha,
|
|
84
|
+
canary,
|
|
85
|
+
statuses,
|
|
86
|
+
}),
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -74,10 +74,10 @@ export function loadStableReleasePolicy({ cwd = process.cwd(), input = "" } = {}
|
|
|
74
74
|
if (!id) {
|
|
75
75
|
throw new Error(`requiredCanaries[${index}].id is required`);
|
|
76
76
|
}
|
|
77
|
-
if (!new Set(["release-candidate", "commit-status"]).has(source)) {
|
|
78
|
-
throw new Error(`requiredCanaries[${index}].source must be release-candidate or
|
|
77
|
+
if (!new Set(["release-candidate", "commit-status", "public-build"]).has(source)) {
|
|
78
|
+
throw new Error(`requiredCanaries[${index}].source must be release-candidate, commit-status or public-build`);
|
|
79
79
|
}
|
|
80
|
-
if (source
|
|
80
|
+
if (source !== "release-candidate" && !string(canary.context)) {
|
|
81
81
|
throw new Error(`requiredCanaries[${index}].context is required for commit-status canaries`);
|
|
82
82
|
}
|
|
83
83
|
return {
|
|
@@ -281,7 +281,7 @@ for (const [channel, ref] of [["alpha", `v${selfDogfoodMajor}-alpha`], ["stable"
|
|
|
281
281
|
const workflow = fs.readFileSync(path.join(root, `.github/workflows/self-build-${channel}-dogfood.yml`), "utf8");
|
|
282
282
|
if (!workflow.includes(`/.github/workflows/build.yml@${ref}`) || /steps:|buildchain-channel:|runner-preset:|working-directory:/u.test(workflow)) throw new Error(`${channel} self-dogfood must remain a thin public TOML build caller`);
|
|
283
283
|
if (!workflow.includes(`group: buildchain-${channel}-self-dogfood-`) || !workflow.includes("cancel-in-progress: false")) throw new Error(`${channel} self-dogfood must serialize its own runs`);
|
|
284
|
-
const expectedInputs =
|
|
284
|
+
const expectedInputs = [];
|
|
285
285
|
const call = parseWorkflowCallJobs(workflow).find((job) => job.id === `${channel}-consumer`);
|
|
286
286
|
if (JSON.stringify(Object.keys(call.with || {})) !== JSON.stringify(expectedInputs)) throw new Error(`${channel} self-dogfood input contract drift`);
|
|
287
287
|
}
|