Skip to content

Commit ba77fd6

Browse files
Add a tag-driven release workflow that publishes to npm
Publishing from a laptop is blocked by the account's 2FA one-time password, which cannot be typed into a non-interactive session. An automation or granular access token stored as the NPM_TOKEN repo secret bypasses the OTP, so CI can publish where a local `npm publish` cannot. The job re-runs the full lint/build/test gate rather than trusting an earlier green run, because a tag can point at any commit. Two guards protect the registry, which never forgets a bad publish: the tag must match the version in packages/cli/package.json, and the version must not already exist. It then repeats the CI packaging smoke test (install the tarball, assert zero runtime dependencies, run the simulated end-to-end flow) before publishing. Prerelease versions publish under the "next" dist-tag so a beta cannot become what a plain `npx etherfind` resolves to. No --provenance and no OIDC trusted publishing: both require a public repository and this one is private. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent d95a104 commit ba77fd6

1 file changed

Lines changed: 120 additions & 0 deletions

File tree

.github/workflows/release.yml

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
name: Release
2+
3+
# Publishing is gated on a version tag so a release is always a deliberate act
4+
# with a commit to point at. workflow_dispatch exists to rehearse the whole
5+
# pipeline (dry_run defaults to true) without touching the registry.
6+
on:
7+
push:
8+
tags: ["v*"]
9+
workflow_dispatch:
10+
inputs:
11+
dry_run:
12+
description: "Run every check and pack the tarball, but do not publish"
13+
type: boolean
14+
default: true
15+
16+
permissions:
17+
contents: write # create the GitHub Release and upload the tarball
18+
19+
# Two releases racing each other would fight over the same version number.
20+
concurrency:
21+
group: release
22+
cancel-in-progress: false
23+
24+
jobs:
25+
release:
26+
name: publish etherfind to npm
27+
runs-on: ubuntu-latest
28+
steps:
29+
- uses: actions/checkout@v4
30+
- uses: actions/setup-node@v4
31+
with:
32+
node-version: 20
33+
cache: npm
34+
# Writes an .npmrc pointing at the public registry and binds it to
35+
# NODE_AUTH_TOKEN in the publish step. Without registry-url the token
36+
# is silently ignored and the publish fails as unauthenticated.
37+
registry-url: https://registry.npmjs.org
38+
39+
- run: npm ci
40+
41+
# Re-run the full CI gate here rather than trusting an earlier green run:
42+
# a tag can point at any commit, including one CI never saw.
43+
- run: npm run lint
44+
- run: npm run build
45+
- run: npm test
46+
47+
- name: Check the tag matches the package version
48+
if: startsWith(github.ref, 'refs/tags/')
49+
run: |
50+
tag="${GITHUB_REF_NAME#v}"
51+
pkg="$(node -p "require('./packages/cli/package.json').version")"
52+
if [ "$tag" != "$pkg" ]; then
53+
echo "::error::tag $GITHUB_REF_NAME implies version $tag, but packages/cli/package.json says $pkg"
54+
exit 1
55+
fi
56+
echo "releasing etherfind@$pkg"
57+
58+
# npm rejects a duplicate version anyway, but it does so *after* the
59+
# publish step has already been entered. Failing here keeps the reason
60+
# obvious: bump the version, don't retry the same tag.
61+
- name: Refuse to republish an existing version
62+
run: |
63+
pkg="$(node -p "require('./packages/cli/package.json').version")"
64+
if npm view "etherfind@$pkg" version >/dev/null 2>&1; then
65+
echo "::error::etherfind@$pkg is already on the registry; bump the version first"
66+
exit 1
67+
fi
68+
69+
# Same smoke test as the CI packaging job: a missing "files" entry or a
70+
# leaked runtime dependency must fail before the publish, not after it,
71+
# because a published version can never be replaced.
72+
- name: Pack and verify the tarball
73+
run: |
74+
mkdir -p /tmp/tarballs
75+
npm pack --workspace etherfind --pack-destination /tmp/tarballs
76+
mkdir -p /tmp/smoke && cd /tmp/smoke
77+
npm init -y > /dev/null
78+
npm install /tmp/tarballs/etherfind-*.tgz --no-audit --no-fund
79+
./node_modules/.bin/etherfind --version
80+
./node_modules/.bin/etherfind --help
81+
node -e "const p=require('/tmp/smoke/node_modules/etherfind/package.json'); if (Object.keys(p.dependencies||{}).length) { console.error('published package must have no runtime dependencies'); process.exit(1); }"
82+
./node_modules/.bin/etherfind --simulate --json | tee run.ndjson
83+
grep -q '"event":"final"' run.ndjson
84+
grep -q '"ok":true' run.ndjson
85+
86+
# On a tag push the `inputs` context is empty, so `!inputs.dry_run` is
87+
# true and the publish runs. On workflow_dispatch it honours the checkbox.
88+
#
89+
# NPM_TOKEN must be an *automation* or *granular access* token: those are
90+
# the only kinds that bypass the account's 2FA one-time password, which
91+
# cannot be entered from CI.
92+
#
93+
# No --provenance: it requires a public repository, and this repo is
94+
# private. Add the flag (plus `id-token: write` above) if that changes.
95+
- name: Publish to npm
96+
if: ${{ !inputs.dry_run }}
97+
run: |
98+
pkg="$(node -p "require('./packages/cli/package.json').version")"
99+
# A prerelease version must not take the "latest" dist-tag, or every
100+
# plain `npx etherfind` would immediately start serving the beta.
101+
case "$pkg" in
102+
*-*) tag=next ;;
103+
*) tag=latest ;;
104+
esac
105+
echo "publishing etherfind@$pkg under dist-tag $tag"
106+
npm publish --workspace etherfind --access public --tag "$tag"
107+
env:
108+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
109+
110+
- name: Create the GitHub release
111+
if: ${{ startsWith(github.ref, 'refs/tags/') && !inputs.dry_run }}
112+
run: |
113+
pkg="$(node -p "require('./packages/cli/package.json').version")"
114+
case "$pkg" in
115+
*-*) prerelease=--prerelease ;;
116+
*) prerelease= ;;
117+
esac
118+
gh release create "$GITHUB_REF_NAME" --generate-notes --verify-tag $prerelease /tmp/tarballs/etherfind-*.tgz
119+
env:
120+
GH_TOKEN: ${{ github.token }}

0 commit comments

Comments
 (0)