@xemahq/repo-build-tooling 0.15.1 → 0.17.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 CHANGED
@@ -30,10 +30,12 @@ pnpm exec xema-scrub-swagger-paths dist
30
30
  | `xema-service-bootstrap [--check] [--filter <key>] [--root <dir>]` | Generates `src/generated/*.generated.ts` service and worker descriptors from the repository's `xema-biome.json` manifests; `--check` fails on drift |
31
31
  | `xema-scrub-swagger-paths <dist-dir>...` | Rewrites `@nestjs/swagger` plugin output that names a private install path, then fails the build if any leak remains (`--self-test` available) |
32
32
  | `xema-composed-image <command>` | Build-time half of a composed-process image: `members`, `filters`, `dirs`, `pins`, `index`, `load`, `stage`, `pnpm-context`, `sync-injected`, `prisma-check`, `migrate`, `coordinate`, `load-image`, `smoke-refusal`, `dockerfile` |
33
+ | `xema-promote-image --service <name> --dev-ref <repo> --release-ref <repo> --tag <tag> --commit <sha> --main-ref <ref> --signer-repository <owner/repo> --develop-workflow <file> --release-workflow <file>... --promotion-predicate-out <path> -- <tag command>` | Publishes the develop image's digest under the release tag instead of rebuilding: `reused` (the release tag is signed by a main workflow, or is the proven develop digest), `copied` (the proven develop digest, digest-preserving; fails closed if the release digest differs) or `miss` (build it). A develop digest is proven only if its SLSA v1 provenance is signed by the develop workflow at `refs/heads/develop`, its revision is an ancestor of the main ref, and the tag command prints the same tag at that revision and at `--commit`; any failed leg is an error, never a miss. On a promotion it writes the `https://xema.dev/attestations/image-promotion/v1` predicate for the caller to attest. Writes `result`, `digest`, `source`, `revision`, `evidence`, `provenance_digest`, `promotion_predicate`, `promotion_predicate_type` to `$GITHUB_OUTPUT`. Needs `docker`, `cosign`, `git` and full history. It does not sign |
34
+ | `xema-arm64-supplement-needed` | Decides whether an arm64 supplement is still owed at `REGISTRY/IMAGE:CONTENT_TAG` (env: `REGISTRY`, `IMAGE`, `CONTENT_TAG`, `REGISTRY_USER`, `REGISTRY_TOKEN`). `needed=false` only when the coordinate serves an index whose linux/arm64 child is proven by its own image config; any other resolvable state is `needed=true`; a 401/403/5xx or an unresolvable child is an error. Writes `needed=true\|false` to `$GITHUB_OUTPUT`. Replaces the generated `arm64-supplement-needed.mjs` copy. |
33
35
  | `xema-check-no-vendored-deps` | Fails on a `workspace:` range for a package the repository does not own |
34
36
  | `xema-check-workspace-ranges` | Fails on a `workspace:` range the local package does not satisfy |
35
37
 
36
- `xema-composed-image members --write|--check` accepts `--manifests <glob>` (repeatable, default `**/xema-biome.json`), `--canonical-dockerfile <path>` and `--cross-repo-manifest <path>`. `xema-composed-image dockerfile` requires `--copy`, `--cache-id` and `--build-source`; it pins the version of this tool itself, and there is no flag for it (`--tooling-version` is refused as an unknown option). `dockerfile --prebuilt` renders the Dockerfile and `Dockerfile.dockerignore` of an image assembled from `stage <dir> [--output <rel>]… [--also <pkg>]…` output (requires `--cache-id`, `--build-source`; `--copy` and `--stage-dir` optional); `load --each` loads every member in its own process. `stage` and `filters` accept `--also <workspace package name>` (repeatable): the package's closure is built and staged (built outputs only, no Prisma checks) alongside the members; an unknown name is refused. `pnpm-context <dir> (--subject <svc>… | --image-dir <composed>)` runs on the CI runner after `pnpm install --frozen-lockfile` and builds the offline `xema-pnpm` build context (`staged.json`, `corepack/pnpm.tgz`, and, with `--image-dir` only, a warmed `cache/` of registry metadata; `--subject` writes no `cache/` and `staged.json` says `cache: false`); it prints `XEMA_PNPM_CONTEXT=<dir>` and `XEMA_PNPM_STORE=<store root>` on stdout (everything else on stderr) and refuses a non-empty `<dir>`. Run `xema-composed-image` with no arguments for the full usage.
38
+ `xema-composed-image members --write|--check` accepts `--manifests <glob>` (repeatable, default `**/xema-biome.json`), `--canonical-dockerfile <path>` and `--cross-repo-manifest <path>`. `xema-composed-image dockerfile` requires `--copy`, `--cache-id` and `--build-source`; it pins the version of this tool itself, and there is no flag for it (`--tooling-version` is refused as an unknown option). `dockerfile --prebuilt` renders the Dockerfile and `Dockerfile.dockerignore` of an image assembled from `stage <dir> [--output <rel>]… [--also <pkg>]…` output (requires `--cache-id`, `--build-source`; `--copy` and `--stage-dir` optional); `load --each` loads every member in its own process. `stage` and `filters` accept `--also <workspace package name>` (repeatable): the package's closure is built and staged (built outputs only, no Prisma checks) alongside the members; an unknown name is refused. `pnpm-context <dir> (--subject <svc>… | --image-dir <composed>)` runs on the CI runner after `pnpm install --frozen-lockfile` and builds the offline `xema-pnpm` build context (`staged.json`, `corepack/pnpm.tgz`, and, with `--image-dir` only, a warmed `cache/` of registry metadata; `--subject` writes no `cache/` and `staged.json` says `cache: false`); it prints `XEMA_PNPM_CONTEXT=<dir>` and `XEMA_PNPM_STORE=<store root>` on stdout (everything else on stderr) and refuses a non-empty `<dir>`. `dockerfile --prebuilt` renders an OFFLINE image: every install RUN is `--network=none` and reads the runner's pnpm store and the `pnpm-context` output as the named build contexts `xema-pnpm-store` and `xema-pnpm`; without them the build fails with a message (no network fallback). `pnpm rebuild` runs only for `onlyBuiltDependencies` in the members' prod closure (less prisma, @prisma/client, @prisma/engines, @nestjs/core, esbuild, @swc/core) and the render needs `pnpm install` first; a node-gyp module with no optional prebuilt packages is refused. `pnpm-context --image-dir` reads the deps stage's COPY directories from the committed Dockerfile. Run `xema-composed-image` with no arguments for the full usage.
37
39
 
38
40
  Every step refuses rather than skips. The package has no dependencies.
39
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/repo-build-tooling",
3
- "version": "0.15.1",
3
+ "version": "0.17.0",
4
4
  "description": "Dev-time build tooling shared by every Xema repository. Ships as plain ESM, depending only on @xemahq/release-policy for registry access, so the published artifact is the reviewed source.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Neuralchowder Inc. <developer@xema.dev> (https://xema.dev)",
@@ -21,18 +21,22 @@
21
21
  "src"
22
22
  ],
23
23
  "bin": {
24
+ "xema-arm64-supplement-needed": "src/arm64-supplement-needed.mjs",
24
25
  "xema-check-no-vendored-deps": "src/check-no-vendored-deps.mjs",
25
26
  "xema-check-workspace-ranges": "src/check-workspace-range-matches-local.mjs",
26
27
  "xema-composed-image": "src/composed-image.mjs",
28
+ "xema-promote-image": "src/promote-image.mjs",
27
29
  "xema-readiness": "src/readiness.mjs",
28
30
  "xema-scrub-swagger-paths": "src/scrub-swagger-plugin-paths.mjs",
29
31
  "xema-service-bootstrap": "src/service-bootstrap.mjs"
30
32
  },
31
33
  "exports": {
34
+ "./arm64-supplement-needed": "./src/arm64-supplement-needed.mjs",
32
35
  "./check-no-vendored-deps": "./src/check-no-vendored-deps.mjs",
33
36
  "./check-workspace-range-matches-local": "./src/check-workspace-range-matches-local.mjs",
34
37
  "./composed-image": "./src/composed-image.mjs",
35
38
  "./package.json": "./package.json",
39
+ "./promote-image": "./src/promote-image.mjs",
36
40
  "./readiness": "./src/readiness.mjs",
37
41
  "./scrub-swagger-plugin-paths": "./src/scrub-swagger-plugin-paths.mjs",
38
42
  "./service-bootstrap": "./src/service-bootstrap.mjs"
@@ -41,9 +45,9 @@
41
45
  "@xemahq/release-policy": "^14.0.0"
42
46
  },
43
47
  "scripts": {
44
- "test": "node src/scrub-swagger-plugin-paths.mjs --self-test && node --test src/check-workspace-range-matches-local.test.mjs && node --test src/check-no-vendored-deps.test.mjs && node --test src/readiness.test.mjs && node --test src/composed-image.test.mjs && node --test src/composed-image-coordinate-cli.test.mjs && node --test src/declaration-refusal.test.mjs && node --test src/smoke-refusal.test.mjs && node --test src/load-image.test.mjs && node --test src/composed-image-coordinate.test.mjs && node --test src/composed-image-dockerfile.test.mjs && node --test src/service-bootstrap.test.mjs && node --test src/service-bootstrap-protocols.test.mjs && node --test src/composed-image-prebuilt-dockerfile.test.mjs && node --test src/composed-image-in-image-closure.test.mjs && node --test src/composed-image-stage.test.mjs && node --test src/composed-image-pnpm-context.test.mjs && node --test src/composed-image-sync-injected.test.mjs && node --test src/composed-image-prisma-check.test.mjs && node --test src/composed-migrate.test.mjs"
48
+ "test": "node src/scrub-swagger-plugin-paths.mjs --self-test && node --test src/check-workspace-range-matches-local.test.mjs && node --test src/check-no-vendored-deps.test.mjs && node --test src/readiness.test.mjs && node --test src/composed-image.test.mjs && node --test src/composed-image-coordinate-cli.test.mjs && node --test src/declaration-refusal.test.mjs && node --test src/smoke-refusal.test.mjs && node --test src/load-image.test.mjs && node --test src/composed-image-coordinate.test.mjs && node --test src/composed-image-dockerfile.test.mjs && node --test src/service-bootstrap.test.mjs && node --test src/service-bootstrap-protocols.test.mjs && node --test src/composed-image-prebuilt-dockerfile.test.mjs && node --test src/composed-image-in-image-closure.test.mjs && node --test src/composed-image-stage.test.mjs && node --test src/composed-image-pnpm-context.test.mjs && node --test src/composed-image-sync-injected.test.mjs && node --test src/composed-image-prisma-check.test.mjs && node --test src/composed-migrate.test.mjs && node --test src/promote-image.test.mjs && node --test src/arm64-supplement-needed.test.mjs"
45
49
  },
46
50
  "xemaRelease": {
47
- "sourceDigest": "sha256:fa45efe38d666b11ffa8841595cec6f19d065d77f286bf2b7c1e6eb3c93aed0e"
51
+ "sourceDigest": "sha256:150d82fb857d535b44f1e29686108056532bc9a7827b51260c88bf3270a00eeb"
48
52
  }
49
53
  }
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env node
2
+ // ═══════════════════════════════════════════════════════════════════════════
3
+ // DOES THIS COORDINATE STILL NEED AN arm64 SUPPLEMENT?
4
+ //
5
+ // An arm64 supplement lane that rebuilds and re-merges UNCONDITIONALLY does
6
+ // more than waste a run. `imagetools create -t :<contentHash>` republishes the
7
+ // canonical coordinate by design, so supplementing an ALREADY-COMPLETE index
8
+ // MOVES A TAG somebody may already have sealed.
9
+ //
10
+ // ── WHY A RE-RUN MOVES THE DIGEST AT ALL ────────────────────────────────────
11
+ //
12
+ // Two builds of the SAME source do not produce the same arm64 manifest unless
13
+ // the build is reproducible. Absent a `SOURCE_DATE_EPOCH`, the image config's
14
+ // `created` and `history[].created` are WALL-CLOCK, so a rebuild changes the
15
+ // config digest and therefore the manifest digest even when every layer byte is
16
+ // identical. The amd64 side does not move only because it is never rebuilt by a
17
+ // supplement lane — the merge REFERENCES its existing per-commit tag.
18
+ //
19
+ // Making the arm64 build reproducible is a separate and much larger decision:
20
+ // it would move every coordinate's digest on the next build. This probe instead
21
+ // makes the question MOOT for a coordinate that is already final, which is the
22
+ // cheap half and the one that stops a sealed tag moving under a release.
23
+ //
24
+ // ── WHY A PROOF AND NOT A READ ──────────────────────────────────────────────
25
+ //
26
+ // An index descriptor DECLARES a child's platform. The merge is exactly where a
27
+ // mislabelled arm64 child would enter, so trusting the declaration would let one
28
+ // bad index become permanent: the supplement lane is the only thing that can
29
+ // repair it, and a descriptor-only check would skip the repair forever.
30
+ //
31
+ // So a skip is EARNED — by resolving the child manifest BY DIGEST and reading
32
+ // the architecture out of its image CONFIG. A mislabelled index reads as NOT
33
+ // SUPPLEMENTED and is rebuilt.
34
+ //
35
+ // ── FAIL LOUD, NEVER GUESS ──────────────────────────────────────────────────
36
+ //
37
+ // Only 200 and 404 are answers. A 401/403 on a private package is
38
+ // indistinguishable from an image that was never pushed, so anything else
39
+ // throws. That direction is deliberate: a supplement lane is off the deploy
40
+ // graph, so a red run there costs nothing, whereas a guessed answer either
41
+ // moves a sealed tag or silently leaves a coordinate single-platform.
42
+ //
43
+ // The same posture governs the ENTRYPOINT GUARD at the foot of this file. It
44
+ // uses `invokedDirectly` (realpath on both sides) because the alternative fails
45
+ // in the dangerous direction: a guard that does not match runs no `main()`,
46
+ // writes no `needed=` output, and a workflow gating its merge on
47
+ // `needed == 'true'` then SKIPS a supplement that was genuinely owed —
48
+ // silently, and with a green tick.
49
+ //
50
+ // Registry transport (retry, redaction, the GHCR pull token) is
51
+ // `@xemahq/release-policy/registry`, not a copy of it.
52
+ // ═══════════════════════════════════════════════════════════════════════════
53
+
54
+ import { appendFileSync } from 'node:fs';
55
+
56
+ import {
57
+ acquireGhcrPullToken,
58
+ describeErrorChain,
59
+ registryFetch,
60
+ } from '@xemahq/release-policy/registry';
61
+
62
+ import { invokedDirectly } from './readiness.mjs';
63
+
64
+ const SHA256_HEX = /^[a-f0-9]{64}$/u;
65
+ const DIGEST = /^sha256:[a-f0-9]{64}$/u;
66
+
67
+ const MANIFEST_ACCEPT = [
68
+ 'application/vnd.oci.image.index.v1+json',
69
+ 'application/vnd.oci.image.manifest.v1+json',
70
+ 'application/vnd.docker.distribution.manifest.list.v2+json',
71
+ 'application/vnd.docker.distribution.manifest.v2+json',
72
+ ].join(', ');
73
+
74
+ /** The platform a supplement exists to add. */
75
+ export const SUPPLEMENT_OS = 'linux';
76
+ export const SUPPLEMENT_ARCHITECTURE = 'arm64';
77
+
78
+ async function getJson({
79
+ registry,
80
+ repository,
81
+ reference,
82
+ bearer,
83
+ fetchImpl,
84
+ sleep,
85
+ }) {
86
+ const response = await registryFetch(
87
+ fetchImpl,
88
+ `https://${registry}/v2/${repository}/manifests/${reference}`,
89
+ {
90
+ method: 'GET',
91
+ headers: { Authorization: `Bearer ${bearer}`, Accept: MANIFEST_ACCEPT },
92
+ },
93
+ { sleep },
94
+ );
95
+ if (response.status === 404) return { status: 404, document: undefined };
96
+ if (response.status !== 200) {
97
+ throw new Error(
98
+ `${registry}: manifest lookup for ${repository}:${reference} returned HTTP ${response.status}`,
99
+ );
100
+ }
101
+ let document;
102
+ try {
103
+ document = await response.json();
104
+ } catch {
105
+ throw new Error(
106
+ `${registry}: manifest ${repository}:${reference} was not JSON`,
107
+ );
108
+ }
109
+ return { status: 200, document };
110
+ }
111
+
112
+ /**
113
+ * Children whose DESCRIPTOR claims the supplemented platform.
114
+ *
115
+ * BuildKit attestation manifests declare `unknown/unknown` and are excluded by
116
+ * the same predicate that selects the runnable child — deliberately, rather
117
+ * than by naming them, so an unrecognised child is never silently treated as a
118
+ * platform it did not claim.
119
+ *
120
+ * @param {unknown} document a manifest or an index
121
+ * @returns {string[]} candidate child digests
122
+ */
123
+ export function declaredArm64Children(document) {
124
+ const manifests = document?.manifests;
125
+ if (!Array.isArray(manifests)) return [];
126
+ return manifests
127
+ .filter(
128
+ (child) =>
129
+ child?.platform?.os === SUPPLEMENT_OS &&
130
+ child?.platform?.architecture === SUPPLEMENT_ARCHITECTURE,
131
+ )
132
+ .map((child) => child?.digest)
133
+ .filter((digest) => typeof digest === 'string' && DIGEST.test(digest));
134
+ }
135
+
136
+ /**
137
+ * Prove one child really is a linux/arm64 image by reading its CONFIG.
138
+ *
139
+ * @returns {Promise<boolean>}
140
+ */
141
+ async function childIsArm64({
142
+ registry,
143
+ repository,
144
+ digest,
145
+ bearer,
146
+ fetchImpl,
147
+ sleep,
148
+ }) {
149
+ const { status, document } = await getJson({
150
+ registry,
151
+ repository,
152
+ reference: digest,
153
+ bearer,
154
+ fetchImpl,
155
+ sleep,
156
+ });
157
+ // A child an index REFERENCES must resolve. A 404 here is a broken index,
158
+ // not an absent supplement — say which, rather than quietly rebuilding.
159
+ if (status === 404) {
160
+ throw new Error(
161
+ `${registry}: ${repository} index references child ${digest}, which the registry does not serve`,
162
+ );
163
+ }
164
+ const configDigest = document?.config?.digest;
165
+ if (typeof configDigest !== 'string' || !DIGEST.test(configDigest)) {
166
+ throw new Error(
167
+ `${registry}: ${repository}@${digest} declares no usable config digest`,
168
+ );
169
+ }
170
+ const response = await registryFetch(
171
+ fetchImpl,
172
+ `https://${registry}/v2/${repository}/blobs/${configDigest}`,
173
+ { method: 'GET', headers: { Authorization: `Bearer ${bearer}` } },
174
+ { sleep },
175
+ );
176
+ if (response.status !== 200) {
177
+ throw new Error(
178
+ `${registry}: config blob ${repository}@${configDigest} returned HTTP ${response.status}`,
179
+ );
180
+ }
181
+ let config;
182
+ try {
183
+ config = await response.json();
184
+ } catch {
185
+ throw new Error(
186
+ `${registry}: config blob ${repository}@${configDigest} was not JSON`,
187
+ );
188
+ }
189
+ return (
190
+ config?.os === SUPPLEMENT_OS &&
191
+ config?.architecture === SUPPLEMENT_ARCHITECTURE
192
+ );
193
+ }
194
+
195
+ /**
196
+ * Is an arm64 supplement still owed at `registry/repository:contentHash`?
197
+ *
198
+ * `needed: false` ONLY when the canonical coordinate already serves an index
199
+ * whose linux/arm64 child is proven by its own config. Every other resolvable
200
+ * state — absent, single-platform, index without arm64, index whose arm64 child
201
+ * does not prove out — is `needed: true`.
202
+ *
203
+ * @returns {Promise<{ needed: boolean, reason: string }>}
204
+ */
205
+ export async function arm64SupplementNeeded({
206
+ registry,
207
+ repository,
208
+ contentHash,
209
+ user,
210
+ token,
211
+ fetchImpl = fetch,
212
+ sleep,
213
+ }) {
214
+ if (!SHA256_HEX.test(contentHash ?? '')) {
215
+ throw new Error(
216
+ `'${contentHash}' is not a 64-character lowercase content hash`,
217
+ );
218
+ }
219
+ const bearer = await acquireGhcrPullToken({
220
+ registry,
221
+ repository,
222
+ user,
223
+ token,
224
+ fetchImpl,
225
+ });
226
+ const { status, document } = await getJson({
227
+ registry,
228
+ repository,
229
+ reference: contentHash,
230
+ bearer,
231
+ fetchImpl,
232
+ sleep,
233
+ });
234
+ if (status === 404) {
235
+ return { needed: true, reason: 'the canonical coordinate does not exist' };
236
+ }
237
+ if (!Array.isArray(document?.manifests)) {
238
+ return {
239
+ needed: true,
240
+ reason: `the canonical coordinate is a single manifest (${document?.mediaType ?? 'unknown media type'}), not an index`,
241
+ };
242
+ }
243
+ const candidates = declaredArm64Children(document);
244
+ if (candidates.length === 0) {
245
+ return {
246
+ needed: true,
247
+ reason: `the index declares no ${SUPPLEMENT_OS}/${SUPPLEMENT_ARCHITECTURE} child`,
248
+ };
249
+ }
250
+ for (const digest of candidates) {
251
+ if (
252
+ await childIsArm64({
253
+ registry,
254
+ repository,
255
+ digest,
256
+ bearer,
257
+ fetchImpl,
258
+ sleep,
259
+ })
260
+ ) {
261
+ return {
262
+ needed: false,
263
+ reason: `the index already carries a proven ${SUPPLEMENT_OS}/${SUPPLEMENT_ARCHITECTURE} child (${digest})`,
264
+ };
265
+ }
266
+ }
267
+ return {
268
+ needed: true,
269
+ reason: `the index declares ${candidates.length} ${SUPPLEMENT_OS}/${SUPPLEMENT_ARCHITECTURE} child(ren) but none proved out against its own config`,
270
+ };
271
+ }
272
+
273
+ /**
274
+ * The CLI entry layer: the only reader of the process environment.
275
+ *
276
+ * @param {NodeJS.ProcessEnv} env
277
+ */
278
+ async function main(env) {
279
+ const registry = env.REGISTRY;
280
+ const image = env.IMAGE;
281
+ const contentHash = env.CONTENT_TAG;
282
+ if (!registry) throw new Error('REGISTRY is required.');
283
+ if (!image) throw new Error('IMAGE is required.');
284
+ const prefix = `${registry}/`;
285
+ if (!image.startsWith(prefix)) {
286
+ throw new Error(`IMAGE '${image}' is not hosted on '${registry}'`);
287
+ }
288
+ const repository = image.slice(prefix.length);
289
+
290
+ const { needed, reason } = await arm64SupplementNeeded({
291
+ registry,
292
+ repository,
293
+ contentHash,
294
+ user: env.REGISTRY_USER,
295
+ token: env.REGISTRY_TOKEN,
296
+ });
297
+
298
+ // THE CORPUS LINE. A skipped supplement and a supplement that silently did
299
+ // nothing exit the same way and print the same tick; the coordinate it
300
+ // graded and the reason are the only things that tell them apart.
301
+ console.log(
302
+ `arm64 supplement ${needed ? 'NEEDED' : 'ALREADY PUBLISHED'} for ` +
303
+ `${image}:${contentHash} — ${reason}`,
304
+ );
305
+ if (!needed) {
306
+ console.log(
307
+ '::notice::Skipping the arm64 build and the index merge. Re-merging an ' +
308
+ 'already-complete coordinate would republish the tag under a new digest ' +
309
+ 'and move any seal that names it.',
310
+ );
311
+ }
312
+
313
+ const out = env.GITHUB_OUTPUT;
314
+ if (out) appendFileSync(out, `needed=${needed ? 'true' : 'false'}\n`);
315
+ }
316
+
317
+ if (invokedDirectly(process.argv[1], import.meta.url)) {
318
+ main(process.env).catch((error) => {
319
+ console.error(
320
+ `::error title=arm64 supplement probe failed::${error.message}`,
321
+ );
322
+ for (const line of describeErrorChain(error)) console.error(` ${line}`);
323
+ process.exit(1);
324
+ });
325
+ }
@@ -0,0 +1,257 @@
1
+ // Cases for `arm64-supplement-needed.mjs` (the `xema-arm64-supplement-needed` bin).
2
+ // Registry transport (retry, redaction, abort) is tested in @xemahq/release-policy.
3
+ //
4
+ // The probe decides whether the arm64 lane REBUILDS and RE-MERGES. A wrong
5
+ // answer is expensive in both directions and neither shows up as a failure:
6
+ //
7
+ // wrongly `true` -> the lane republishes `:<contentHash>` under a NEW digest
8
+ // and MOVES a seal that names it. `seal-image-digests`
9
+ // then refuses every edition carrying that coordinate —
10
+ // which is exactly what happened to app-forge-api on
11
+ // 2026-09-21 and blocked five of nine editions.
12
+ // wrongly `false` -> the coordinate stays amd64-only, forever: the amd64
13
+ // matrix is content-addressed and never re-selects an
14
+ // existing coordinate, so this lane never re-arms for it.
15
+ //
16
+ // So both directions are asserted, and the mislabelled-child case is asserted
17
+ // SEPARATELY from the absent-child case: it is the only one that fails if the
18
+ // config proof is downgraded to reading the index descriptor.
19
+
20
+ import assert from 'node:assert/strict';
21
+ import test from 'node:test';
22
+
23
+ import {
24
+ arm64SupplementNeeded,
25
+ declaredArm64Children,
26
+ } from './arm64-supplement-needed.mjs';
27
+
28
+ const HASH = 'a'.repeat(64);
29
+ const ARM_CHILD = `sha256:${'1'.repeat(64)}`;
30
+ const AMD_CHILD = `sha256:${'2'.repeat(64)}`;
31
+ const ARM_CONFIG = `sha256:${'3'.repeat(64)}`;
32
+ const AMD_CONFIG = `sha256:${'4'.repeat(64)}`;
33
+
34
+ const json = (body, status = 200) => ({
35
+ status,
36
+ json: async () => body,
37
+ });
38
+
39
+ const INDEX_BOTH = {
40
+ mediaType: 'application/vnd.oci.image.index.v1+json',
41
+ manifests: [
42
+ { digest: AMD_CHILD, platform: { os: 'linux', architecture: 'amd64' } },
43
+ { digest: ARM_CHILD, platform: { os: 'linux', architecture: 'arm64' } },
44
+ {
45
+ digest: `sha256:${'9'.repeat(64)}`,
46
+ platform: { os: 'unknown', architecture: 'unknown' },
47
+ annotations: { 'vnd.docker.reference.type': 'attestation-manifest' },
48
+ },
49
+ ],
50
+ };
51
+
52
+ /**
53
+ * A registry whose arm64 child's CONFIG declares `configArchitecture`.
54
+ * Setting it to `amd64` is the mislabelled index the proof exists to catch.
55
+ */
56
+ function registry({
57
+ manifest = INDEX_BOTH,
58
+ manifestStatus = 200,
59
+ configArchitecture = 'arm64',
60
+ childStatus = 200,
61
+ tokenStatus = 200,
62
+ } = {}) {
63
+ const seen = [];
64
+ const fetchImpl = async (url, init = {}) => {
65
+ const href = String(url);
66
+ seen.push(href);
67
+ if (href.includes('/token?') || href.includes('/token&') || href.endsWith('/token')) {
68
+ return json({ token: 'bearer-value' }, tokenStatus);
69
+ }
70
+ if (href.endsWith(`/manifests/${HASH}`)) {
71
+ return manifestStatus === 200
72
+ ? json(manifest)
73
+ : { status: manifestStatus, json: async () => ({}) };
74
+ }
75
+ if (href.endsWith(`/manifests/${ARM_CHILD}`)) {
76
+ return childStatus === 200
77
+ ? json({ config: { digest: ARM_CONFIG } })
78
+ : { status: childStatus, json: async () => ({}) };
79
+ }
80
+ if (href.endsWith(`/blobs/${ARM_CONFIG}`)) {
81
+ return json({ os: 'linux', architecture: configArchitecture });
82
+ }
83
+ if (href.endsWith(`/blobs/${AMD_CONFIG}`)) {
84
+ return json({ os: 'linux', architecture: 'amd64' });
85
+ }
86
+ assert.fail(`unexpected request: ${href} ${JSON.stringify(init.method)}`);
87
+ };
88
+ return { fetchImpl, seen };
89
+ }
90
+
91
+ const noSleep = async () => {};
92
+
93
+ const probe = (options) =>
94
+ arm64SupplementNeeded({
95
+ registry: 'ghcr.io',
96
+ repository: 'xema-dev/app-forge-api',
97
+ contentHash: HASH,
98
+ user: 'x',
99
+ token: 'y',
100
+ fetchImpl: registry(options).fetchImpl,
101
+ sleep: noSleep,
102
+ });
103
+
104
+ test('a complete index with a PROVEN arm64 child needs no supplement', async () => {
105
+ const { needed, reason } = await probe();
106
+ assert.equal(needed, false);
107
+ assert.match(reason, /already carries a proven linux\/arm64 child/u);
108
+ });
109
+
110
+ test('an index whose arm64 child is MISLABELLED still needs a supplement', async () => {
111
+ // The descriptor says linux/arm64; the image config says amd64. Reading the
112
+ // descriptor alone would skip the rebuild and make a broken index permanent,
113
+ // because this lane is the only thing that can repair one.
114
+ const { needed, reason } = await probe({ configArchitecture: 'amd64' });
115
+ assert.equal(needed, true);
116
+ assert.match(reason, /none proved out against its own config/u);
117
+ });
118
+
119
+ test('an index with amd64 only needs a supplement', async () => {
120
+ const { needed, reason } = await probe({
121
+ manifest: {
122
+ mediaType: 'application/vnd.oci.image.index.v1+json',
123
+ manifests: [
124
+ { digest: AMD_CHILD, platform: { os: 'linux', architecture: 'amd64' } },
125
+ ],
126
+ },
127
+ });
128
+ assert.equal(needed, true);
129
+ assert.match(reason, /declares no linux\/arm64 child/u);
130
+ });
131
+
132
+ test('an index carrying ONLY attestations needs a supplement', async () => {
133
+ const { needed } = await probe({
134
+ manifest: {
135
+ mediaType: 'application/vnd.oci.image.index.v1+json',
136
+ manifests: [
137
+ {
138
+ digest: `sha256:${'9'.repeat(64)}`,
139
+ platform: { os: 'unknown', architecture: 'unknown' },
140
+ annotations: { 'vnd.docker.reference.type': 'attestation-manifest' },
141
+ },
142
+ ],
143
+ },
144
+ });
145
+ assert.equal(needed, true);
146
+ });
147
+
148
+ test('a single-platform manifest needs a supplement', async () => {
149
+ const { needed, reason } = await probe({
150
+ manifest: {
151
+ mediaType: 'application/vnd.oci.image.manifest.v1+json',
152
+ config: { digest: AMD_CONFIG },
153
+ },
154
+ });
155
+ assert.equal(needed, true);
156
+ assert.match(reason, /single manifest/u);
157
+ });
158
+
159
+ test('an ABSENT coordinate needs a supplement', async () => {
160
+ const { needed, reason } = await probe({ manifestStatus: 404 });
161
+ assert.equal(needed, true);
162
+ assert.match(reason, /does not exist/u);
163
+ });
164
+
165
+ test('a 403 THROWS rather than answering', async () => {
166
+ // A 403 on a private package is indistinguishable from an image that was
167
+ // never pushed. Answering either way is a guess; one moves a sealed tag and
168
+ // the other strands a coordinate amd64-only.
169
+ await assert.rejects(probe({ manifestStatus: 403 }), /returned HTTP 403/u);
170
+ });
171
+
172
+ test('a 500 THROWS rather than answering', async () => {
173
+ await assert.rejects(probe({ manifestStatus: 500 }), /returned HTTP 500/u);
174
+ });
175
+
176
+ test('an index referencing an UNRESOLVABLE child THROWS', async () => {
177
+ await assert.rejects(
178
+ probe({ childStatus: 404 }),
179
+ /does not serve/u,
180
+ );
181
+ });
182
+
183
+ test('a non-sha256 content hash THROWS before any request', async () => {
184
+ await assert.rejects(
185
+ arm64SupplementNeeded({
186
+ registry: 'ghcr.io',
187
+ repository: 'xema-dev/app-forge-api',
188
+ contentHash: 'not-a-hash',
189
+ user: 'x',
190
+ token: 'y',
191
+ fetchImpl: async () => assert.fail('no request may be made'),
192
+ }),
193
+ /not a 64-character lowercase content hash/u,
194
+ );
195
+ });
196
+
197
+ test('credentials never leave the configured registry host', async () => {
198
+ await assert.rejects(
199
+ arm64SupplementNeeded({
200
+ registry: 'evil.example.com',
201
+ repository: 'xema-dev/app-forge-api',
202
+ contentHash: HASH,
203
+ user: 'x',
204
+ token: 'y',
205
+ fetchImpl: async () => assert.fail('no request may be made'),
206
+ }),
207
+ /cannot send credentials to registry/u,
208
+ );
209
+ });
210
+
211
+ test('declaredArm64Children selects only linux/arm64 descriptors', () => {
212
+ assert.deepEqual(declaredArm64Children(INDEX_BOTH), [ARM_CHILD]);
213
+ assert.deepEqual(declaredArm64Children({ mediaType: 'x' }), []);
214
+ assert.deepEqual(declaredArm64Children(undefined), []);
215
+ // A descriptor with the right architecture but the wrong OS is not a
216
+ // supplement for this lane.
217
+ assert.deepEqual(
218
+ declaredArm64Children({
219
+ manifests: [
220
+ { digest: ARM_CHILD, platform: { os: 'windows', architecture: 'arm64' } },
221
+ ],
222
+ }),
223
+ [],
224
+ );
225
+ });
226
+
227
+ test('HTTP 404 on the manifest request makes exactly 1 manifest call', async () => {
228
+ const { fetchImpl, seen } = registry({ manifestStatus: 404 });
229
+ const { needed } = await arm64SupplementNeeded({
230
+ registry: 'ghcr.io',
231
+ repository: 'xema-dev/app-forge-api',
232
+ contentHash: HASH,
233
+ user: 'x',
234
+ token: 'y',
235
+ fetchImpl,
236
+ sleep: noSleep,
237
+ });
238
+ assert.equal(needed, true);
239
+ assert.equal(seen.filter((href) => href.endsWith(`/manifests/${HASH}`)).length, 1);
240
+ });
241
+
242
+ test('HTTP 403 on the manifest request makes 1 manifest call', async () => {
243
+ const { fetchImpl, seen } = registry({ manifestStatus: 403 });
244
+ await assert.rejects(
245
+ arm64SupplementNeeded({
246
+ registry: 'ghcr.io',
247
+ repository: 'xema-dev/app-forge-api',
248
+ contentHash: HASH,
249
+ user: 'x',
250
+ token: 'y',
251
+ fetchImpl,
252
+ sleep: noSleep,
253
+ }),
254
+ /HTTP 403/u,
255
+ );
256
+ assert.equal(seen.filter((href) => href.endsWith(`/manifests/${HASH}`)).length, 1);
257
+ });