From 5a975e3e77c2bc1fd91749b0c1298513a6236b5d Mon Sep 17 00:00:00 2001 From: Andrew Goldis Date: Wed, 23 Sep 2026 15:06:47 -0700 Subject: [PATCH 1/3] ci: publish to npm from a GitHub workflow Same flow as @currents/cmd and @currents/playwright: a manual workflow_dispatch picks the npm tag, and npm trusted publishing (OIDC) authenticates, so no npm token is stored. alpha and beta need a matching version suffix. The unit tests run before publishing. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LdJrRhBLEH9Uwt97JMGVTR --- .github/workflows/publish.yaml | 53 ++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/publish.yaml diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..948073b --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,53 @@ +name: Publish NPM Package + +on: + workflow_dispatch: + inputs: + channel: + required: true + type: choice + description: NPM tag to publish the package to + options: + - alpha + - beta + - latest + +permissions: + id-token: write # Required for OIDC + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '24.x' + registry-url: 'https://registry.npmjs.org' + + # pre-git would install git hooks in the checkout + - run: npm ci --ignore-scripts + + - name: Check version format + env: + CHANNEL: ${{ github.event.inputs.channel }} + run: | + PACKAGE_VERSION=$(node -p "require('./package.json').version") + if [[ "$CHANNEL" == "alpha" || "$CHANNEL" == "beta" ]]; then + if [[ "$PACKAGE_VERSION" != *"-$CHANNEL"* ]]; then + echo "Error: Package version ($PACKAGE_VERSION) does not include the required -$CHANNEL suffix for the selected channel." + exit 1 + fi + fi + echo "Version check passed: $PACKAGE_VERSION is valid for channel $CHANNEL" + + - run: npm run unit + + - name: Publish to NPM + env: + CHANNEL: ${{ github.event.inputs.channel }} + run: | + npm pack --dry-run + npm publish --tag "$CHANNEL" From 32984ccb2e8990f2dcd16b88d8d31f2e0a5b0c8e Mon Sep 17 00:00:00 2001 From: Andrew Goldis Date: Wed, 23 Sep 2026 16:21:31 -0700 Subject: [PATCH 2/3] ci: publish from a job that runs no dependency code Only the publish job gets id-token: write, and it runs checkout, setup-node and npm publish with scripts off. Installing dependencies and the unit tests move to a test job without it, so a dev dependency cannot request the OIDC token npm trusted publishing accepts. The version check compares the first prerelease identifier exactly: beta needs 1.1.0-beta.N, 1.0.0-betafoo no longer passes, and latest rejects any prerelease. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LdJrRhBLEH9Uwt97JMGVTR --- .github/workflows/publish.yaml | 43 ++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 948073b..25ff016 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -13,11 +13,10 @@ on: - latest permissions: - id-token: write # Required for OIDC contents: read jobs: - publish: + test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -25,29 +24,47 @@ jobs: - uses: actions/setup-node@v4 with: node-version: '24.x' - registry-url: 'https://registry.npmjs.org' - - # pre-git would install git hooks in the checkout - - run: npm ci --ignore-scripts - name: Check version format env: CHANNEL: ${{ github.event.inputs.channel }} run: | PACKAGE_VERSION=$(node -p "require('./package.json').version") - if [[ "$CHANNEL" == "alpha" || "$CHANNEL" == "beta" ]]; then - if [[ "$PACKAGE_VERSION" != *"-$CHANNEL"* ]]; then - echo "Error: Package version ($PACKAGE_VERSION) does not include the required -$CHANNEL suffix for the selected channel." - exit 1 - fi + # first prerelease identifier: 1.1.0-beta.0 -> beta, 1.1.0 -> empty + PRERELEASE=$(node -p "const v = require('./package.json').version.split('+')[0]; const i = v.indexOf('-'); i < 0 ? '' : v.slice(i + 1).split('.')[0]") + if [[ "$CHANNEL" == "latest" && -n "$PRERELEASE" ]]; then + echo "Error: $PACKAGE_VERSION is a prerelease and cannot be published to latest." + exit 1 + fi + if [[ "$CHANNEL" != "latest" && "$PRERELEASE" != "$CHANNEL" ]]; then + echo "Error: Package version ($PACKAGE_VERSION) must be a -$CHANNEL.N prerelease for the selected channel." + exit 1 fi echo "Version check passed: $PACKAGE_VERSION is valid for channel $CHANNEL" + # pre-git would install git hooks in the checkout + - run: npm ci --ignore-scripts - run: npm run unit + # Only this job can request the OIDC token npm trusted publishing uses, and + # it runs no dependency code: the package has no build step. + publish: + needs: test + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '24.x' + registry-url: 'https://registry.npmjs.org' + - name: Publish to NPM env: CHANNEL: ${{ github.event.inputs.channel }} run: | - npm pack --dry-run - npm publish --tag "$CHANNEL" + npm pack --dry-run --ignore-scripts + npm publish --ignore-scripts --tag "$CHANNEL" From 510de6d3d68a3d8ddddd1771febd37e4750ddeda Mon Sep 17 00:00:00 2001 From: Andrew Goldis Date: Wed, 23 Sep 2026 16:28:54 -0700 Subject: [PATCH 3/3] ci: match the whole version against the channel latest takes only X.Y.Z, and alpha or beta only X.Y.Z-alpha.N or X.Y.Z-beta.N, so 1.1.0-beta and 1.1.0-beta.foo no longer pass. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LdJrRhBLEH9Uwt97JMGVTR --- .github/workflows/publish.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 25ff016..d4d6f2d 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -30,14 +30,14 @@ jobs: CHANNEL: ${{ github.event.inputs.channel }} run: | PACKAGE_VERSION=$(node -p "require('./package.json').version") - # first prerelease identifier: 1.1.0-beta.0 -> beta, 1.1.0 -> empty - PRERELEASE=$(node -p "const v = require('./package.json').version.split('+')[0]; const i = v.indexOf('-'); i < 0 ? '' : v.slice(i + 1).split('.')[0]") - if [[ "$CHANNEL" == "latest" && -n "$PRERELEASE" ]]; then - echo "Error: $PACKAGE_VERSION is a prerelease and cannot be published to latest." - exit 1 + # latest: 1.1.0, alpha and beta: 1.1.0-beta.0 + if [[ "$CHANNEL" == "latest" ]]; then + PATTERN='^[0-9]+\.[0-9]+\.[0-9]+$' + else + PATTERN="^[0-9]+\.[0-9]+\.[0-9]+-$CHANNEL\.[0-9]+$" fi - if [[ "$CHANNEL" != "latest" && "$PRERELEASE" != "$CHANNEL" ]]; then - echo "Error: Package version ($PACKAGE_VERSION) must be a -$CHANNEL.N prerelease for the selected channel." + if [[ ! "$PACKAGE_VERSION" =~ $PATTERN ]]; then + echo "Error: $PACKAGE_VERSION cannot be published to $CHANNEL. Expected a version like 1.1.0 for latest, or 1.1.0-$CHANNEL.0 for $CHANNEL." exit 1 fi echo "Version check passed: $PACKAGE_VERSION is valid for channel $CHANNEL"