Files
przeprogramowani 43d6a69088 fix(release): wait for npm metadata before verifying publish (#50)
* fix(release): wait for npm metadata before verifying publish

Direct publish-npm verified immediately after npm accepted 1.22.1,
while registry metadata still lacked dist.tarball. Poll until
integrity exists, then keep the strict pack/gitHead compare. When
the version is already on npm and matches the pack from cli_sha,
skip publish and only complete tag/Release.

* chore(release): prepare v1.22.2

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-16 18:22:01 +02:00

147 lines
7.2 KiB
YAML

name: Publish npm
# Direct publication of an exact, already-tested source SHA with the
# repository's existing NPM_TOKEN. No Toolkit receipt, lease or coordinator
# contract: those are handled outside this job. The package is packed and
# smoke-installed from the exact SHA first, publication happens once, and the
# actual registry bytes are verified afterwards (never republished).
on:
workflow_dispatch:
inputs:
cli_sha:
description: Exact full source SHA to publish (must be an ancestor of master)
required: true
type: string
permissions:
contents: read
concurrency:
group: publish-npm
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: write
steps:
- name: Require an exact full SHA
run: node -e 'if (!/^[a-f0-9]{40}$/.test(process.env.CLI_SHA || "")) throw new Error("cli_sha must be a full SHA")'
env:
CLI_SHA: ${{ inputs.cli_sha }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ inputs.cli_sha }}
fetch-depth: 0
persist-credentials: false
- name: Require the SHA to be on master
run: git merge-base --is-ancestor "${{ inputs.cli_sha }}" origin/master
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: "1.3.8"
- uses: actions/setup-node@v4
with:
node-version: "22"
- run: npm install --global npm@11.12.1 --ignore-scripts
- run: bun install --frozen-lockfile
- run: bun run build
- name: Pack outside the checkout and smoke the pack in an isolated installation
run: |
set -eu
OUT="$RUNNER_TEMP/release-package"; mkdir -p "$OUT"
npm pack --json --ignore-scripts --pack-destination "$OUT" > "$OUT/pack.json"
node -e '
const [p] = JSON.parse(require("fs").readFileSync(process.env.OUT + "/pack.json", "utf8"));
if (p.name !== "@przeprogramowani/10x-cli") throw new Error("Unexpected package name");
require("fs").writeFileSync(process.env.OUT + "/candidate.json", JSON.stringify({ version: p.version, filename: p.filename, integrity: p.integrity, files: p.files.map((f) => f.path).sort() }));
console.log(p.version, p.integrity, p.files.map((f) => f.path).sort().join(","));
'
SMOKE="$RUNNER_TEMP/release-pack-smoke"; mkdir -p "$SMOKE"
FILE=$(node -p 'JSON.parse(require("fs").readFileSync(process.env.OUT + "/candidate.json","utf8")).filename')
npm install --ignore-scripts --no-audit --no-fund --prefix "$SMOKE" "$OUT/$FILE"
node "$SMOKE/node_modules/@przeprogramowani/10x-cli/dist/index.mjs" --version
node "$SMOKE/node_modules/@przeprogramowani/10x-cli/dist/index.mjs" --help > /dev/null
echo "OUT=$OUT" >> "$GITHUB_ENV"
env:
OUT: ${{ runner.temp }}/release-package
- name: Decide whether to publish or resume a matching registry copy
id: decide
run: node scripts/publish-npm-verify.mjs decide
env:
OUT: ${{ runner.temp }}/release-package
CLI_SHA: ${{ inputs.cli_sha }}
- name: Publish the exact directory once
if: steps.decide.outputs.action == 'publish'
run: |
set -eu
VERSION=$(node -p 'JSON.parse(require("fs").readFileSync(process.env.OUT + "/candidate.json","utf8")).version')
if npm view "@przeprogramowani/10x-cli@$VERSION" version >/dev/null 2>&1; then
echo "::error::@przeprogramowani/10x-cli@$VERSION already published; never republish"; exit 1
fi
node -e 'require("fs").writeFileSync(process.env.NPM_CONFIG_USERCONFIG, "registry=https://registry.npmjs.org/\n//registry.npmjs.org/:_authToken=" + process.env.NPM_TOKEN + "\n", { mode: 0o600 })'
npm publish . --ignore-scripts --access public
env:
NPM_CONFIG_USERCONFIG: ${{ runner.temp }}/release-npmrc
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Verify actual registry bytes against the pack
run: node scripts/publish-npm-verify.mjs verify
env:
OUT: ${{ runner.temp }}/release-package
CLI_SHA: ${{ inputs.cli_sha }}
- name: Tag the published source
# The tag may already exist (pushed by hand, or a previous attempt).
# Peel it to its commit: an annotated tag's ref SHA is the tag object,
# not the commit, so comparing ref SHAs alone reports a false mismatch.
run: |
set -eu
VERSION=$(node -p 'JSON.parse(require("fs").readFileSync(process.env.OUT + "/candidate.json","utf8")).version')
TAG="v${VERSION}"
git fetch --quiet origin "refs/tags/${TAG}:refs/tags/${TAG}" 2>/dev/null || true
EXISTING=$(git rev-parse --verify --quiet "refs/tags/${TAG}^{commit}" || true)
if [ -z "$EXISTING" ]; then
gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" -f ref="refs/tags/${TAG}" -f sha="${CLI_SHA}" > /dev/null
echo "Created tag ${TAG} at ${CLI_SHA}"
elif [ "$EXISTING" != "${CLI_SHA}" ]; then
echo "::error::Tag ${TAG} exists at ${EXISTING}, not the published source ${CLI_SHA}"; exit 1
else
echo "Tag ${TAG} already points at ${CLI_SHA}"
fi
echo "TAG=${TAG}" >> "$GITHUB_ENV"
env:
GH_TOKEN: ${{ github.token }}
CLI_SHA: ${{ inputs.cli_sha }}
- name: Create the GitHub Release with the verified tarball
# Idempotent: an existing release for the tag is left untouched.
run: |
set -eu
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then
echo "Release ${TAG} already exists; not modified"; exit 0
fi
FILE=$(node -p 'JSON.parse(require("fs").readFileSync(process.env.OUT + "/candidate.json","utf8")).filename')
INTEGRITY=$(node -p 'JSON.parse(require("fs").readFileSync(process.env.OUT + "/published-result.json","utf8")).registryIntegrity')
NOTES="$RUNNER_TEMP/release-notes.md"
{
echo "Published on npm from commit \`${CLI_SHA}\`: https://www.npmjs.com/package/@przeprogramowani/10x-cli/v/${TAG#v}"
echo
echo "Tarball integrity: \`${INTEGRITY}\` (the attached tarball is byte-identical to the registry artifact)."
echo
} > "$NOTES"
gh release create "$TAG" --repo "$GITHUB_REPOSITORY" --verify-tag --target "$CLI_SHA" \
--title "$TAG" --notes-file "$NOTES" --generate-notes \
"$OUT/$FILE#npm package tarball (${TAG#v})"
gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json url,tagName,assets --jq '{url,tagName,assets:[.assets[].name]}'
env:
GH_TOKEN: ${{ github.token }}
CLI_SHA: ${{ inputs.cli_sha }}
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: published-npm-${{ github.run_id }}-${{ github.run_attempt }}
path: |
${{ runner.temp }}/release-package/candidate.json
${{ runner.temp }}/release-package/published-result.json
if-no-files-found: error
retention-days: 90