Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ jobs:
pull-requests: write
outputs:
hasChangesets: ${{ steps.changesets.outputs.hasChangesets }}
allPublished: ${{ steps.published.outputs.allPublished }}
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.0.0
with:
Expand All @@ -43,6 +44,16 @@ jobs:
node scripts/release/seal.mjs self-test
node scripts/release/publisher.mjs self-test

# Runs before Changesets, which rewrites manifests in the working tree
# for the Version Packages PR. The question is about the commit that was
# pushed, not about what a pending release would become.
- name: Report whether the pushed version is already published
id: published
run: |
result="$(node scripts/release/published.mjs)"
echo "$result"
echo "allPublished=$(node -p 'JSON.parse(process.argv[1]).allPublished' "$result")" >> "$GITHUB_OUTPUT"

- name: Open or update the fixed-set Version Packages PR
id: changesets
uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0
Expand All @@ -53,7 +64,10 @@ jobs:

verify:
needs: version
if: ${{ needs.version.outputs.hasChangesets == 'false' }}
# No pending Changesets AND something left to publish. The first condition
# alone is satisfied by any ordinary merge, which is how every commit to
# `main` came to park a deployment at a gate that could only fail.
if: ${{ needs.version.outputs.hasChangesets == 'false' && needs.version.outputs.allPublished == 'false' }}
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
65 changes: 65 additions & 0 deletions scripts/release/published.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env node

/**
* Report whether the working tree's version of the fixed set is already on the
* registry.
*
* `release.yml` fires on every push to `main`, and the release chain is gated
* only on there being no pending Changesets. That is true of an ordinary merge
* as well as a Version Packages merge, so every unrelated commit ran the whole
* chain and parked a deployment at the `npm-production` gate. Approving one is
* worse than pointless: the sealed commit no longer matches the commit named in
* the published packages' provenance, so `[REGISTRY_PROVENANCE]` refuses the
* set — correctly, because the publisher must not certify bytes it did not
* produce. A gate that can only fail is a trap standing open on `main`.
*
* **Unreachable is reported as not published.** A registry this cannot read is
* a registry it cannot claim anything about, and the safe direction is to let
* the release chain run: the worst case is the pending deployment this exists
* to avoid, whereas wrongly reporting "published" would silently skip a real
* release. Partial publication reports not-published too, which is what keeps
* the documented resume path reachable.
*/

import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { join } from "node:path";

import { ROOT, loadReleaseLine } from "./config.mjs";

function registryVersions(name) {
try {
const output = execFileSync("npm", ["view", name, "versions", "--json"], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
timeout: 60_000,
});
const parsed = JSON.parse(output);
// npm answers with a bare string when a package has exactly one version.
return Array.isArray(parsed) ? parsed : [parsed];
} catch {
return null;
}
}

function report() {
const config = loadReleaseLine();
const corePath = join(ROOT, config.packages[0].path, "package.json");
const version = JSON.parse(readFileSync(corePath, "utf8")).version;
const missing = [];
for (const spec of config.packages) {
const versions = registryVersions(spec.name);
if (versions === null || !versions.includes(version)) {
missing.push(spec.name);
}
}
process.stdout.write(
`${JSON.stringify({
version,
allPublished: missing.length === 0,
missing,
})}\n`,
);
}

report();
Loading