diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5aee23c..459763b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,6 +18,9 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 10 steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 @@ -52,4 +55,10 @@ jobs: with: node-version: '22.23.2' - run: npm install --global npm@12.2.0 - - run: npm publish ./artifacts/*.tgz --access public --provenance --ignore-scripts + - id: select + run: | + state=$(node scripts/release-files.mjs select artifacts) + echo "state=$state" >> "$GITHUB_OUTPUT" + - if: steps.select.outputs.state == 'missing' + run: npm publish ./artifacts/*.tgz --access public --provenance --ignore-scripts + - run: node scripts/release-files.mjs verify artifacts diff --git a/scripts/release-files.mjs b/scripts/release-files.mjs new file mode 100644 index 0000000..f00f4ef --- /dev/null +++ b/scripts/release-files.mjs @@ -0,0 +1,41 @@ +import { createHash } from "node:crypto"; +import { readFile, readdir } from "node:fs/promises"; +import { join } from "node:path"; +import { setTimeout } from "node:timers/promises"; + +const [mode, directory] = process.argv.slice(2); +if (!["select", "verify"].includes(mode) || !directory) { + throw new Error("Expected select or verify and a package directory"); +} +const { name, version } = JSON.parse(await readFile("package.json", "utf8")); +const [filename, ...other] = await readdir(directory); +if ( + !/^\d+\.\d+\.\d+$/.test(version) || + filename !== `${name.replace(/^@/, "").replace("/", "-")}-${version}.tgz` || + other.length +) { + throw new Error("Expected one tarball for the checked package version"); +} +const data = await readFile(join(directory, filename)); +const integrity = `sha512-${createHash("sha512").update(data).digest("base64")}`; +const url = `https://registry.npmjs.org/${encodeURIComponent(name)}/${version}`; + +async function published() { + const response = await fetch(url, { signal: AbortSignal.timeout(30000) }); + if (response.status === 404) return false; + if (!response.ok) throw new Error(`npm lookup returned HTTP ${response.status}`); + const metadata = await response.json(); + if (metadata.name !== name || metadata.version !== version || metadata.dist?.integrity !== integrity) { + throw new Error("Published npm tarball differs from the checked release"); + } + return true; +} + +if (mode === "select") { + console.log((await published()) ? "present" : "missing"); +} else { + for (let attempt = 0; !(await published()); attempt++) { + if (attempt === 5) throw new Error("Published npm tarball is missing"); + await setTimeout(5000); + } +}