Skip to content

Commit 5eeb02c

Browse files
ci(docker-publish): wait until npm serves the CLI's install tree before the image build (#25674)
Fixes #25613 Clause-②: no Metadata-protocol: none On a release, the Docker image job starts about a minute after `changeset publish` returns. At that point npm can still answer 404 for some of the fixed group's tarballs. On 18.1.0 that made the image build fail: `npm install -g @objectstack/cli@18.1.0` got E404 on `spec-18.1.0.tgz` (release run 40078805767, job 120834761830). The same URL answered 200 a few minutes later. ## What changed `.github/workflows/docker-publish.yml` gets a new step, `Wait until npm serves the CLI's install tree`, between `Validate version input` and the image build. Its inline Node script: - walks `@objectstack/cli@VERSION`'s `@objectstack/*` dependency tree on the registry, starting from the CLI's own manifest and following `dependencies` (each range's operator is stripped, because the group is lockstep); - for every package, needs both the version manifest and a `HEAD` on its `dist.tarball` to answer 200; - reads a newly found dependency at once and waits only on a miss, backing off 5 s → 10 s → … → 60 s, bounded at 10 minutes; - on a timeout, writes one `::error::` per package still missing and exits 1; - treats a network error as a miss, so it is retried like a 404. Both callers get it: `workflow_call` from `release.yml` and `workflow_dispatch`. A rebuild of a version that is already out passes in seconds. ## Local verification These runs used the extracted script, with Node 22 against the live registry: | case | result | | :-- | :-- | | `VERSION=18.1.0` | `npm serves @objectstack/cli@18.1.0 and 54 @objectstack/* dependencies.` exit 0, about 20 s | | `VERSION=18.0.0` | the same line for 18.0.0, exit 0 | | `VERSION=99.0.0`, deadline shortened to 20 s | waits 5 s, then 10 s, then `::error::npm does not serve @objectstack/cli@99.0.0 …`, exit 1 | - The workflow parses as YAML, and the step order reads back as written. - `node scripts/pm/dispatch-gates.mjs --ran`: 39 derived families, 39 run, all exit 0. - `check-governed-merges.mjs --branch HEAD`: GOVERNED, tier H (`.github/workflows/**`), 66 changed lines. ## Acceptance notes - **Tier H.** This PR needs an approval from one of GOVERNED_APPROVERS before the seat readies and lands it. - **Recovery of the 18.1.0 image** is out of scope here. The card names two ways: a re-run of the failed job, or the next landing's release-integrity backfill. - **Claim.** The seat's claim on #25613 is comment 6385843623, and it names this branch. - **`skip-changeset`.** No published package is touched; the PR changes only a workflow. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LKHDniRQSWjRJyDtacwpEd Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7cd2ebf commit 5eeb02c

1 file changed

Lines changed: 66 additions & 0 deletions

File tree

‎.github/workflows/docker-publish.yml‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,72 @@ jobs:
5656
fi
5757
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
5858
59+
- name: Wait until npm serves the CLI's install tree
60+
# On a release, this job starts about a minute after `changeset
61+
# publish` returns. npm can still answer 404 for some of the group's
62+
# tarballs at that point, and the image build's `npm install -g` then
63+
# fails (18.1.0: spec-18.1.0.tgz, run 40078805767, #25613). So poll the
64+
# registry before building: walk @objectstack/cli@VERSION's
65+
# @objectstack/* dependency tree, and wait until every manifest and
66+
# tarball answers 200. The wait is bounded at 10 minutes, and on a
67+
# timeout the error names each package still missing. A rebuild of a
68+
# version that is already out passes at once.
69+
env:
70+
VERSION: ${{ steps.version.outputs.version }}
71+
run: |
72+
node --input-type=module - <<'NODE'
73+
const version = process.env.VERSION;
74+
const registry = 'https://registry.npmjs.org';
75+
const deadline = Date.now() + 10 * 60 * 1000;
76+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
77+
// A lockstep group: an @objectstack/* range names the version it was
78+
// published with (`^18.1.0` or `18.1.0`), so strip the operator.
79+
const pinOf = (range) => range.replace(/^[\^~=v]+/, '');
80+
// A network error counts as a miss and is retried like a 404.
81+
async function ready(name, ver) {
82+
try {
83+
const res = await fetch(`${registry}/${name.replace('/', '%2f')}/${ver}`);
84+
if (!res.ok) return null;
85+
const manifest = await res.json();
86+
const tarball = await fetch(manifest.dist.tarball, { method: 'HEAD' });
87+
return tarball.ok ? manifest : null;
88+
} catch {
89+
return null;
90+
}
91+
}
92+
const seen = new Map();
93+
let pending = [['@objectstack/cli', version]];
94+
let delay = 5000;
95+
while (pending.length > 0) {
96+
const missing = [];
97+
const found = [];
98+
for (const [name, ver] of pending) {
99+
const manifest = await ready(name, ver);
100+
if (!manifest) { missing.push([name, ver]); continue; }
101+
seen.set(name, ver);
102+
for (const [dep, range] of Object.entries(manifest.dependencies ?? {})) {
103+
if (dep.startsWith('@objectstack/') && !seen.has(dep)
104+
&& ![...pending, ...found].some(([n]) => n === dep)) {
105+
found.push([dep, pinOf(range)]);
106+
}
107+
}
108+
}
109+
pending = [...missing, ...found];
110+
// Newly found dependencies are read at once; only a miss waits.
111+
if (missing.length === 0) continue;
112+
if (Date.now() + delay > deadline) {
113+
for (const [name, ver] of missing) {
114+
console.log(`::error::npm does not serve ${name}@${ver} (manifest or tarball) after 10 minutes`);
115+
}
116+
process.exit(1);
117+
}
118+
console.log(`waiting ${delay / 1000}s for ${missing.length} package(s): ${missing.map(([n, v]) => `${n}@${v}`).join(', ')}`);
119+
await sleep(delay);
120+
delay = Math.min(delay * 2, 60000);
121+
}
122+
console.log(`npm serves @objectstack/cli@${version} and ${seen.size - 1} @objectstack/* dependencies.`);
123+
NODE
124+
59125
- name: Set up QEMU (arm64 emulation)
60126
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
61127

0 commit comments

Comments
 (0)