@kungfu-tech/buildchain 3.0.5 → 3.0.6-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.
Files changed (83) hide show
  1. package/README.md +31 -0
  2. package/actions/macos-credential-island/README.md +8 -0
  3. package/bin/buildchain.mjs +1 -1
  4. package/contracts/auditable-demo-media-profiles-v1.json +61 -0
  5. package/contracts/auditable-demo-scenario-v1.schema.json +164 -0
  6. package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +3 -3
  7. package/contracts/evidence/auditable-demo-web-delivery-v1.json +3 -3
  8. package/dist/site/buildchain-contract.json +107 -26
  9. package/dist/site/buildchain-site.json +126 -49
  10. package/dist/site/capability-registry.json +3 -3
  11. package/dist/site/cli-registry.json +9 -3
  12. package/dist/site/controller-registry.json +27 -3
  13. package/dist/site/kfd-claims.json +103 -16
  14. package/dist/site/kfd-upstream-aggregate.json +9 -9
  15. package/dist/site/manual-registry.json +10 -9
  16. package/dist/site/node-api-registry.json +540 -31
  17. package/dist/site/page-registry.json +110 -29
  18. package/dist/site/public-surface-audit.json +186 -12
  19. package/dist/site/publication-authority-registry.json +62 -1
  20. package/dist/site/publication-registry.json +4 -4
  21. package/dist/site/site-manifest.json +13 -13
  22. package/dist/site/workflow-registry.json +120 -5
  23. package/docs/MAP.md +5 -1
  24. package/docs/auditable-demo.md +90 -7
  25. package/docs/aws-us-elastic-runner-burst-plane.md +181 -16
  26. package/docs/cli-reference.md +35 -2
  27. package/docs/dev-alpha-candidate-patrol.md +14 -5
  28. package/docs/dev-qualification-patrol.md +108 -0
  29. package/docs/node-api-reference.md +123 -72
  30. package/docs/publish-transaction.md +7 -5
  31. package/docs/release-flow.md +4 -0
  32. package/docs/release-governance.md +47 -1
  33. package/docs/release-propagation.md +93 -0
  34. package/docs/reusable-build-surface.md +53 -12
  35. package/docs/stable-candidate-patrol.md +5 -2
  36. package/package.json +3 -2
  37. package/packages/core/buildchain-contract.js +36 -1
  38. package/packages/core/buildchain-publication-authority.js +3 -0
  39. package/packages/core/public-surface-audit.js +4 -35
  40. package/packages/core/release-propagation-agent-entry.js +185 -0
  41. package/packages/core/release-propagation-pickup.js +387 -0
  42. package/packages/core/release-propagation-push.js +293 -0
  43. package/packages/core/release-propagation-release.js +61 -18
  44. package/packages/core/release-propagation-stage-evidence.js +36 -0
  45. package/packages/core/release-propagation-work.js +8 -4
  46. package/packages/core/release-propagation.js +27 -2
  47. package/packages/core/workflow-call-contract.js +308 -0
  48. package/packages/core/workflow-yaml-contract.js +272 -0
  49. package/scripts/artifact-signing-controller-core.mjs +525 -0
  50. package/scripts/artifact-signing-controller.mjs +302 -0
  51. package/scripts/artifact-signing-delegation.mjs +51 -3
  52. package/scripts/auditable-demo-platform.mjs +564 -0
  53. package/scripts/auditable-demo-renditions.mjs +7 -2
  54. package/scripts/auditable-demo-transport-smoke.mjs +176 -0
  55. package/scripts/auditable-demo.mjs +27 -21
  56. package/scripts/aws-macos-jit-controller-core.mjs +389 -0
  57. package/scripts/aws-macos-jit-controller-runtime.mjs +82 -0
  58. package/scripts/aws-macos-jit-controller.mjs +558 -0
  59. package/scripts/aws-macos-jit-job-controller.mjs +401 -0
  60. package/scripts/aws-windows-jit-campaign-core.mjs +295 -0
  61. package/scripts/aws-windows-jit-campaign.mjs +202 -0
  62. package/scripts/aws-windows-jit-controller-core.mjs +20 -2
  63. package/scripts/aws-windows-jit-controller.mjs +185 -109
  64. package/scripts/aws-windows-jit-core.mjs +21 -3
  65. package/scripts/aws-windows-jit.mjs +2 -0
  66. package/scripts/build-standalone-binary.mjs +6 -0
  67. package/scripts/buildchain-cli-help.mjs +2 -1
  68. package/scripts/capture-package-release-propagation.mjs +24 -1
  69. package/scripts/check-inventory.mjs +6 -0
  70. package/scripts/compiler-cache-evidence.mjs +90 -7
  71. package/scripts/dev-alpha-candidate-patrol.mjs +80 -5
  72. package/scripts/dev-pr-auto-merge.mjs +15 -15
  73. package/scripts/dev-qualification-patrol.mjs +594 -0
  74. package/scripts/dispatch-artifact-signing-authority.mjs +310 -61
  75. package/scripts/finalize-native-artifact-signing-result.mjs +37 -15
  76. package/scripts/generate-site-bundle.mjs +5 -0
  77. package/scripts/inspect-artifact-signing-requests.mjs +12 -0
  78. package/scripts/locked-source-checkout.mjs +17 -3
  79. package/scripts/release-propagation.mjs +150 -2
  80. package/scripts/run-lifecycle-core.mjs +25 -0
  81. package/scripts/site-capability-metadata.mjs +1 -1
  82. package/scripts/stable-candidate-patrol.mjs +30 -4
  83. package/scripts/workflow-call-contract.mjs +133 -0
@@ -0,0 +1,185 @@
1
+ import {
2
+ assertPlainObject,
3
+ assertString,
4
+ normalizeChannel,
5
+ sha256Json,
6
+ } from "./release-propagation-common.js";
7
+ import { verifyReleasePropagationWork } from "./release-propagation-work.js";
8
+
9
+ export const SITE_UPSTREAM_AGENT_ENTRY_CONTRACT =
10
+ "kungfu-buildchain-site-upstream-agent-entry";
11
+ export const RELEASE_PROPAGATION_FAILURE_MATRIX_CONTRACT =
12
+ "kungfu-buildchain-release-propagation-failure-matrix";
13
+
14
+ const SOURCE_ALIASES = new Map([
15
+ ["buildchain", "buildchain"],
16
+ ["core", "kungfu-core"],
17
+ ["kfd", "kfd"],
18
+ ["kungfu", "kungfu-core"],
19
+ ["kungfu-core", "kungfu-core"],
20
+ ["paper", "paper"],
21
+ ["papers", "paper"],
22
+ ]);
23
+
24
+ const POLICIES = Object.freeze({
25
+ buildchain: Object.freeze({
26
+ mode: "downstream-manual",
27
+ trigger: "explicit-site-intent",
28
+ package: "@kungfu-tech/buildchain",
29
+ }),
30
+ "kungfu-core": Object.freeze({
31
+ mode: "downstream-manual",
32
+ trigger: "explicit-site-intent",
33
+ package: "@kungfu-tech/site",
34
+ }),
35
+ kfd: Object.freeze({
36
+ mode: "automatic-release-handoff",
37
+ trigger: "upstream-release-capture",
38
+ package: "@kungfu-tech/kfd",
39
+ }),
40
+ paper: Object.freeze({
41
+ mode: "automatic-release-handoff",
42
+ trigger: "upstream-release-capture",
43
+ packagePrefix: "@kungfu-tech/paper-",
44
+ }),
45
+ });
46
+
47
+ const FAILURE_ROWS = Object.freeze([
48
+ ["current", "no-op", "none", "retain-current-cut"],
49
+ ["duplicate-work", "no-op", "reuse", "resume-exact-work"],
50
+ ["superseded-work", "stop", "needs-decision", "follow-declared-successor"],
51
+ ["stale-downstream-base", "failure", "retry", "refresh-and-recapture"],
52
+ ["stale-expected-old", "failure", "retry", "refresh-and-recapture"],
53
+ [
54
+ "package-schema-mismatch",
55
+ "failure",
56
+ "hard-safety-gate",
57
+ "repair-upstream-release",
58
+ ],
59
+ ["pull-request-conflict", "failure", "retry", "refresh-and-recapture"],
60
+ ["verification-failure", "failure", "retry", "repair-same-work"],
61
+ ["release-race", "failure", "retry", "read-authoritative-release-and-resume"],
62
+ ["deployment-failure", "failure", "retry", "repair-same-work"],
63
+ ["online-readback-failure", "failure", "retry", "repair-same-work"],
64
+ ]);
65
+
66
+ export const RELEASE_PROPAGATION_FAILURE_MATRIX = Object.freeze({
67
+ schemaVersion: 1,
68
+ contract: RELEASE_PROPAGATION_FAILURE_MATRIX_CONTRACT,
69
+ rows: FAILURE_ROWS.map(([condition, outcome, disposition, recovery]) => ({
70
+ condition,
71
+ outcome,
72
+ disposition,
73
+ recovery,
74
+ })),
75
+ });
76
+
77
+ export function normalizeSiteUpstreamIntent(value) {
78
+ const raw = assertString(value, "site upstream intent").toLowerCase();
79
+ if (raw.startsWith("@kungfu-tech/paper-")) return "paper";
80
+ const normalized = SOURCE_ALIASES.get(raw);
81
+ if (!normalized) {
82
+ throw new Error(
83
+ "site upstream intent must identify paper, kfd, buildchain, or kungfu-core",
84
+ );
85
+ }
86
+ return normalized;
87
+ }
88
+
89
+ function automaticHandoff(sourceId, policy, handoffWork) {
90
+ if (handoffWork === null || handoffWork === undefined) {
91
+ return {
92
+ status: "handoff-required",
93
+ exactRelease: null,
94
+ work: null,
95
+ nextAction: {
96
+ action: "consume-release-handoff",
97
+ command:
98
+ "buildchain release-propagation entry plan --source-id <source> --handoff-work <work.json> --json",
99
+ },
100
+ };
101
+ }
102
+ const status = verifyReleasePropagationWork(
103
+ assertPlainObject(handoffWork, "automatic release handoff Work"),
104
+ );
105
+ const packageFact = status.work.upstream.release.package;
106
+ const packageMatches =
107
+ packageFact?.name === policy.package ||
108
+ (policy.packagePrefix &&
109
+ packageFact?.name?.startsWith(policy.packagePrefix));
110
+ if (!packageMatches) {
111
+ throw new Error(
112
+ `automatic ${sourceId} handoff package disagrees with the selected policy`,
113
+ );
114
+ }
115
+ return {
116
+ status: status.lifecycle === "complete" ? "complete" : "handoff-ready",
117
+ exactRelease: {
118
+ repository: status.work.upstream.release.repository,
119
+ package: packageFact,
120
+ releaseRoot: status.work.upstream.releaseRoot,
121
+ },
122
+ work: {
123
+ workId: status.workId,
124
+ workRoot: status.contentRoot,
125
+ lifecycle: status.lifecycle,
126
+ currentStage: status.currentStage,
127
+ expectedBaseSha: status.work.downstream.expectedBaseSha,
128
+ branch: status.work.downstream.branch,
129
+ },
130
+ nextAction: status.nextAction,
131
+ };
132
+ }
133
+
134
+ export function planSiteUpstreamAgentEntry({
135
+ sourceId: sourceInput,
136
+ channel = "",
137
+ handoffWork = null,
138
+ } = {}) {
139
+ const sourceId = normalizeSiteUpstreamIntent(sourceInput);
140
+ const policy = POLICIES[sourceId];
141
+ const normalizedChannel = channel
142
+ ? normalizeChannel(channel, "site upstream channel")
143
+ : sourceId === "paper" || sourceId === "kfd"
144
+ ? ""
145
+ : "release";
146
+ const route =
147
+ policy.mode === "automatic-release-handoff"
148
+ ? automaticHandoff(sourceId, policy, handoffWork)
149
+ : {
150
+ status: "manual-resolution-required",
151
+ exactRelease: null,
152
+ work: null,
153
+ nextAction: {
154
+ action: "resolve-published-release",
155
+ command: `buildchain release-propagation pickup plan --config <config.json> --source-id ${sourceId} --channel ${normalizedChannel} --current-version <exact-version> --json`,
156
+ },
157
+ };
158
+ const body = {
159
+ schemaVersion: 1,
160
+ contract: SITE_UPSTREAM_AGENT_ENTRY_CONTRACT,
161
+ sourceId,
162
+ channel: normalizedChannel,
163
+ policy,
164
+ automaticTrigger: policy.mode === "automatic-release-handoff",
165
+ ...route,
166
+ faultMatrixRoot: sha256Json(RELEASE_PROPAGATION_FAILURE_MATRIX),
167
+ };
168
+ return { ...body, entryRoot: sha256Json(body) };
169
+ }
170
+
171
+ export function classifyReleasePropagationCondition(condition) {
172
+ const normalized = assertString(condition, "release propagation condition");
173
+ const row = RELEASE_PROPAGATION_FAILURE_MATRIX.rows.find(
174
+ (entry) => entry.condition === normalized,
175
+ );
176
+ if (!row) {
177
+ throw new Error(`unsupported release propagation condition: ${normalized}`);
178
+ }
179
+ return {
180
+ schemaVersion: 1,
181
+ contract: "kungfu-buildchain-release-propagation-condition",
182
+ ...row,
183
+ matrixRoot: sha256Json(RELEASE_PROPAGATION_FAILURE_MATRIX),
184
+ };
185
+ }
@@ -0,0 +1,387 @@
1
+ import {
2
+ assertExactFields,
3
+ assertPlainObject,
4
+ assertString,
5
+ normalizeChannel,
6
+ optionalString,
7
+ } from "./release-propagation-common.js";
8
+ import { normalizeExecutionProfile } from "./release-propagation-execution-profile.js";
9
+ import { assertCommitSha } from "./release-propagation-work-control.js";
10
+ import {
11
+ createReleasePropagationWork,
12
+ planReleasePropagation,
13
+ } from "./release-propagation.js";
14
+ import { normalizeUpstreamRelease } from "./release-propagation-release.js";
15
+
16
+ export const MANUAL_UPSTREAM_PICKUP_CONFIG_CONTRACT =
17
+ "kungfu-buildchain-manual-upstream-pickup";
18
+ export const MANUAL_UPSTREAM_PICKUP_PLAN_CONTRACT =
19
+ "kungfu-buildchain-manual-upstream-pickup-plan";
20
+
21
+ function assertRepository(value, label) {
22
+ const repository = assertString(value, label);
23
+ if (!/^[^/\s]+\/[^/\s]+$/.test(repository)) {
24
+ throw new Error(`${label} must be owner/repo`);
25
+ }
26
+ return repository;
27
+ }
28
+
29
+ function normalizeStringList(values, label) {
30
+ if (!Array.isArray(values) || values.length === 0) {
31
+ throw new Error(`${label} must be a non-empty array`);
32
+ }
33
+ const normalized = values.map((value, index) =>
34
+ assertString(value, `${label}[${index}]`),
35
+ );
36
+ const canonical = [...new Set(normalized)].sort();
37
+ if (JSON.stringify(normalized) !== JSON.stringify(canonical)) {
38
+ throw new Error(`${label} must be sorted and unique`);
39
+ }
40
+ return canonical;
41
+ }
42
+
43
+ function normalizeRuntime(value) {
44
+ const runtime = assertExactFields(
45
+ value,
46
+ ["package", "version"],
47
+ "manual pickup runtime",
48
+ );
49
+ return {
50
+ package: assertString(runtime.package, "manual pickup runtime.package"),
51
+ version: assertString(runtime.version, "manual pickup runtime.version"),
52
+ };
53
+ }
54
+
55
+ function normalizeDownstream(value) {
56
+ const downstream = assertExactFields(
57
+ value,
58
+ ["id", "repository", "baseRef", "executionProfile"],
59
+ "manual pickup downstream",
60
+ );
61
+ return {
62
+ id: assertString(downstream.id, "manual pickup downstream.id"),
63
+ repository: assertRepository(
64
+ downstream.repository,
65
+ "manual pickup downstream.repository",
66
+ ),
67
+ baseRef: assertString(
68
+ downstream.baseRef,
69
+ "manual pickup downstream.baseRef",
70
+ ),
71
+ executionProfile: normalizeExecutionProfile(
72
+ downstream.executionProfile,
73
+ "manual pickup downstream.executionProfile",
74
+ ),
75
+ };
76
+ }
77
+
78
+ function normalizeSource(value, index) {
79
+ const label = `manual pickup sources[${index}]`;
80
+ const source = assertExactFields(
81
+ value,
82
+ [
83
+ "id",
84
+ "repository",
85
+ "package",
86
+ "lockPath",
87
+ "distTags",
88
+ "workflowPaths",
89
+ "workflowRefs",
90
+ ],
91
+ label,
92
+ );
93
+ const distTags = assertExactFields(
94
+ source.distTags,
95
+ ["alpha", "release"],
96
+ `${label}.distTags`,
97
+ );
98
+ return {
99
+ id: assertString(source.id, `${label}.id`),
100
+ repository: assertRepository(source.repository, `${label}.repository`),
101
+ package: assertString(source.package, `${label}.package`),
102
+ lockPath: assertString(source.lockPath, `${label}.lockPath`),
103
+ distTags: {
104
+ alpha: assertString(distTags.alpha, `${label}.distTags.alpha`),
105
+ release: assertString(distTags.release, `${label}.distTags.release`),
106
+ },
107
+ workflowPaths: normalizeStringList(
108
+ source.workflowPaths,
109
+ `${label}.workflowPaths`,
110
+ ),
111
+ workflowRefs: normalizeStringList(
112
+ source.workflowRefs,
113
+ `${label}.workflowRefs`,
114
+ ),
115
+ };
116
+ }
117
+
118
+ export function normalizeManualUpstreamPickupConfig(input) {
119
+ const config = assertExactFields(
120
+ input,
121
+ ["schemaVersion", "contract", "runtime", "downstream", "sources"],
122
+ "manual upstream pickup config",
123
+ );
124
+ if (
125
+ config.schemaVersion !== 1 ||
126
+ config.contract !== MANUAL_UPSTREAM_PICKUP_CONFIG_CONTRACT
127
+ ) {
128
+ throw new Error(
129
+ `manual upstream pickup config must use ${MANUAL_UPSTREAM_PICKUP_CONFIG_CONTRACT} v1`,
130
+ );
131
+ }
132
+ if (!Array.isArray(config.sources) || config.sources.length === 0) {
133
+ throw new Error(
134
+ "manual upstream pickup config sources must be a non-empty array",
135
+ );
136
+ }
137
+ const sources = config.sources.map(normalizeSource);
138
+ const ids = sources.map((source) => source.id);
139
+ if (new Set(ids).size !== ids.length)
140
+ throw new Error("manual pickup source ids must be unique");
141
+ return {
142
+ schemaVersion: 1,
143
+ contract: MANUAL_UPSTREAM_PICKUP_CONFIG_CONTRACT,
144
+ runtime: normalizeRuntime(config.runtime),
145
+ downstream: normalizeDownstream(config.downstream),
146
+ sources,
147
+ };
148
+ }
149
+
150
+ function decodePayload(attestation, label) {
151
+ const encoded = attestation?.bundle?.dsseEnvelope?.payload;
152
+ if (!encoded) throw new Error(`${label} is missing a DSSE payload`);
153
+ try {
154
+ return JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
155
+ } catch {
156
+ throw new Error(`${label} DSSE payload is not valid JSON`);
157
+ }
158
+ }
159
+
160
+ function repositoryFromUrl(value) {
161
+ return String(value || "")
162
+ .replace(/^git\+/, "")
163
+ .replace(/^https:\/\/github\.com\//, "")
164
+ .replace(/\.git$/, "");
165
+ }
166
+
167
+ export function resolveNpmRegistryRelease({
168
+ source: sourceInput,
169
+ channel,
170
+ packageMetadata,
171
+ attestations,
172
+ }) {
173
+ const source = normalizeSource(sourceInput, 0);
174
+ const normalizedChannel = normalizeChannel(channel, "manual pickup channel");
175
+ const metadata = assertPlainObject(packageMetadata, "npm package metadata");
176
+ if (metadata.name !== source.package)
177
+ throw new Error("npm package name disagrees with manual pickup source");
178
+ if (
179
+ repositoryFromUrl(metadata.repository?.url || metadata.repository) !==
180
+ source.repository
181
+ ) {
182
+ throw new Error(
183
+ "npm package repository disagrees with manual pickup source",
184
+ );
185
+ }
186
+ const version = assertString(
187
+ metadata.version,
188
+ "npm package metadata.version",
189
+ );
190
+ const integrity = assertString(
191
+ metadata.dist?.integrity,
192
+ "npm package metadata.dist.integrity",
193
+ );
194
+ const attestationUrl = assertString(
195
+ metadata.dist?.attestations?.url,
196
+ "npm package metadata.dist.attestations.url",
197
+ );
198
+ const response = assertPlainObject(attestations, "npm attestations response");
199
+ const slsa = (response.attestations || []).find(
200
+ (entry) => entry.predicateType === "https://slsa.dev/provenance/v1",
201
+ );
202
+ if (!slsa) throw new Error("npm package does not expose SLSA provenance v1");
203
+ const statement = decodePayload(slsa, "npm SLSA provenance");
204
+ if (statement.predicateType !== "https://slsa.dev/provenance/v1") {
205
+ throw new Error(
206
+ "npm SLSA statement predicate type disagrees with its envelope",
207
+ );
208
+ }
209
+ const purlName = encodeURIComponent(source.package).replace("%2F", "/");
210
+ const expectedSubject = `pkg:npm/${purlName}@${version}`;
211
+ const subject = (statement.subject || []).find(
212
+ (entry) => entry.name === expectedSubject,
213
+ );
214
+ if (!subject?.digest?.sha512)
215
+ throw new Error("npm SLSA provenance is missing the exact package subject");
216
+ const workflow =
217
+ statement.predicate?.buildDefinition?.externalParameters?.workflow || {};
218
+ const workflowRepository = repositoryFromUrl(workflow.repository);
219
+ if (workflowRepository !== source.repository) {
220
+ throw new Error(
221
+ "npm SLSA workflow repository disagrees with manual pickup source",
222
+ );
223
+ }
224
+ if (!source.workflowPaths.includes(workflow.path)) {
225
+ throw new Error(
226
+ "npm SLSA workflow path is not admitted by the manual pickup source",
227
+ );
228
+ }
229
+ if (!source.workflowRefs.includes(workflow.ref)) {
230
+ throw new Error(
231
+ "npm SLSA workflow ref is not admitted by the manual pickup source",
232
+ );
233
+ }
234
+ const dependency = (
235
+ statement.predicate?.buildDefinition?.resolvedDependencies || []
236
+ ).find(
237
+ (entry) =>
238
+ repositoryFromUrl(String(entry.uri || "").split("@")[0]) ===
239
+ source.repository,
240
+ );
241
+ const sourceSha = assertCommitSha(
242
+ dependency?.digest?.gitCommit,
243
+ "npm SLSA source commit",
244
+ );
245
+ const runUrl = assertString(
246
+ statement.predicate?.runDetails?.metadata?.invocationId,
247
+ "npm SLSA invocation URL",
248
+ );
249
+ return normalizeUpstreamRelease({
250
+ repository: source.repository,
251
+ channel: normalizedChannel,
252
+ tag: "",
253
+ tagTargetSha: "",
254
+ sourceSha,
255
+ package: {
256
+ name: source.package,
257
+ version,
258
+ integrity,
259
+ gitHead: optionalString(metadata.gitHead),
260
+ },
261
+ registryProvenance: {
262
+ registry: "https://registry.npmjs.org",
263
+ attestationUrl,
264
+ predicateType: slsa.predicateType,
265
+ subjectSha512: subject.digest.sha512,
266
+ repository: source.repository,
267
+ sourceSha,
268
+ workflowPath: workflow.path,
269
+ workflowRef: workflow.ref,
270
+ runUrl,
271
+ },
272
+ });
273
+ }
274
+
275
+ function pickupGraph(config, source) {
276
+ return {
277
+ schemaVersion: 1,
278
+ contract: "kungfu-buildchain-release-propagation-graph",
279
+ nodes: [
280
+ { id: source.id, repository: source.repository, package: source.package },
281
+ {
282
+ id: config.downstream.id,
283
+ repository: config.downstream.repository,
284
+ baseRef: config.downstream.baseRef,
285
+ lockPath: source.lockPath,
286
+ executionProfile: config.downstream.executionProfile,
287
+ },
288
+ ],
289
+ edges: [
290
+ {
291
+ id: `${source.id}-manual-pickup`,
292
+ from: source.id,
293
+ to: config.downstream.id,
294
+ channels: ["alpha", "release"],
295
+ channelPolicy: "preserve",
296
+ lockPath: source.lockPath,
297
+ prBaseRef: config.downstream.baseRef,
298
+ },
299
+ ],
300
+ };
301
+ }
302
+
303
+ export function createManualUpstreamPickupPlan({
304
+ config: configInput,
305
+ sourceId,
306
+ channel,
307
+ currentVersion,
308
+ upstreamRelease,
309
+ }) {
310
+ const config = normalizeManualUpstreamPickupConfig(configInput);
311
+ const source = config.sources.find((entry) => entry.id === sourceId);
312
+ if (!source)
313
+ throw new Error(`manual pickup source is not configured: ${sourceId}`);
314
+ const release = normalizeUpstreamRelease(upstreamRelease);
315
+ const normalizedChannel = normalizeChannel(channel, "manual pickup channel");
316
+ if (
317
+ release.repository !== source.repository ||
318
+ release.package?.name !== source.package
319
+ ) {
320
+ throw new Error("manual pickup release disagrees with the selected source");
321
+ }
322
+ if (release.channel !== normalizedChannel)
323
+ throw new Error("manual pickup release channel disagrees with the request");
324
+ const installedVersion = assertString(
325
+ currentVersion,
326
+ "manual pickup currentVersion",
327
+ );
328
+ const status =
329
+ installedVersion === release.package.version
330
+ ? "current"
331
+ : "update-available";
332
+ const propagationPlan =
333
+ status === "current"
334
+ ? null
335
+ : planReleasePropagation({
336
+ graph: pickupGraph(config, source),
337
+ upstreamRelease: release,
338
+ sourceNode: source.id,
339
+ });
340
+ return {
341
+ schemaVersion: 1,
342
+ contract: MANUAL_UPSTREAM_PICKUP_PLAN_CONTRACT,
343
+ invocation: "downstream-manual",
344
+ automaticTrigger: false,
345
+ runtime: config.runtime,
346
+ source: {
347
+ id: source.id,
348
+ repository: source.repository,
349
+ package: source.package,
350
+ channel: normalizedChannel,
351
+ distTag: source.distTags[normalizedChannel],
352
+ },
353
+ currentVersion: installedVersion,
354
+ resolvedVersion: release.package.version,
355
+ status,
356
+ upstreamRelease: release,
357
+ propagationPlan,
358
+ };
359
+ }
360
+
361
+ export function createManualUpstreamPickupCapture({
362
+ plan,
363
+ expectedDownstreamBaseSha,
364
+ }) {
365
+ const pickup = assertPlainObject(plan, "manual pickup plan");
366
+ if (
367
+ pickup.contract !== MANUAL_UPSTREAM_PICKUP_PLAN_CONTRACT ||
368
+ pickup.schemaVersion !== 1
369
+ ) {
370
+ throw new Error("manual pickup plan must use the v1 contract");
371
+ }
372
+ if (pickup.status === "current") {
373
+ return { plan: pickup, work: null, nextAction: "none" };
374
+ }
375
+ if (pickup.status !== "update-available" || !pickup.propagationPlan) {
376
+ throw new Error("manual pickup plan has an invalid status");
377
+ }
378
+ const expectedBaseSha = assertCommitSha(
379
+ expectedDownstreamBaseSha,
380
+ "expectedDownstreamBaseSha",
381
+ );
382
+ const work = createReleasePropagationWork({
383
+ plan: pickup.propagationPlan,
384
+ expectedDownstreamBaseSha: expectedBaseSha,
385
+ });
386
+ return { plan: pickup, work, nextAction: "claim" };
387
+ }