From 3b76ee9d2b36b6aa935d3d23f53ef624e86de32b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 17:36:45 +0000 Subject: [PATCH] Publish from CI on a GitHub Release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shipping a version becomes tag-and-release instead of someone running npm publish from a laptop. Authentication uses npm trusted publishing over OIDC, so no long-lived token exists — not in repository secrets, not on a machine. It needs id-token: write and npm 11.5.1 or newer, which is why the workflow upgrades npm before publishing rather than trusting the runner's bundled version. Provenance is attested automatically. The workflow refuses to publish when the release tag disagrees with package.json. That check costs seconds and cannot be done after the fact, because a published npm version can never be replaced. Tests run through the existing prepublishOnly hook, so a failing suite stops the publish too. Trusted publishing also requires a one-time Trusted Publisher entry on npmjs.com; until that exists the publish step fails with an authentication error, which is the safe direction. The README documents both the release steps and that setup. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01QJR2DDBimsijgYgZS3bUS8 --- .github/workflows/release.yml | 68 +++++++++++++++++++++++++++++++++++ README.md | 33 +++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e45d3ad --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,68 @@ +name: Release + +# Publishes to npm when a GitHub Release is published, so shipping a version is +# "tag and release" rather than someone running npm publish from a laptop. +# +# Authentication uses npm trusted publishing (OIDC): GitHub Actions proves its +# identity to the registry directly, so there is no long-lived token stored in +# repository secrets or on anyone's machine. It requires a one-time setup on +# npmjs.com — see "Releasing" in the README. + +on: + release: + types: [published] + workflow_dispatch: + +permissions: + contents: read + +jobs: + publish: + name: Publish to npm + runs-on: ubuntu-latest + permissions: + contents: read + # Required for OIDC. Without it the registry has nothing to verify. + id-token: write + + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v5 + with: + node-version: '22' + registry-url: https://registry.npmjs.org + cache: npm + + # Trusted publishing needs npm 11.5.1 or newer, which is more recent than + # the npm bundled with some Node versions. The printed version makes it + # obvious in the log if this ever regresses. + - name: Use an npm that supports trusted publishing + run: | + npm install -g npm@latest + npm --version + + - name: Install dependencies + run: npm ci + + # A release tagged v1.2.3 must publish 1.2.3. Catching a mismatch here + # costs seconds; catching it afterwards is not possible, because a + # published npm version can never be replaced. + - name: Check the tag matches package.json + if: github.event_name == 'release' + env: + TAG: ${{ github.event.release.tag_name }} + run: | + version="$(node -p "require('./package.json').version")" + if [ "${TAG#v}" != "$version" ]; then + echo "::error::Release tag '$TAG' does not match package.json version '$version'." + echo "Bump the version and retag, or delete and recreate the release." >&2 + exit 1 + fi + echo "Publishing $version from tag $TAG." + + # The tests run through the prepublishOnly hook, so a failing suite stops + # the publish. Trusted publishing attests provenance automatically, so no + # --provenance flag is needed. + - name: Publish + run: npm publish diff --git a/README.md b/README.md index 7c54197..4bc2ff3 100644 --- a/README.md +++ b/README.md @@ -274,6 +274,39 @@ The tests cover the pure logic — size parsing and formatting, access-URL rewri Every push and pull request runs the suite on Node 18, 20 and 22 via GitHub Actions, along with a syntax check and an audit of production dependencies. See [`.github/workflows/ci.yml`](.github/workflows/ci.yml). +## Releasing + +Releases publish from CI, so no npm token exists on anyone's machine or in +repository secrets. Authentication uses npm +[trusted publishing](https://docs.npmjs.com/trusted-publishers): GitHub Actions +proves its identity to the registry over OIDC, and npm attests the package's +provenance automatically. + +To ship a version: + +```bash +npm version minor # or patch / major - commits and tags +git push --follow-tags +``` + +Then publish a GitHub Release for that tag. [`release.yml`](.github/workflows/release.yml) +installs, verifies the tag matches `package.json`, runs the tests through the +`prepublishOnly` hook, and publishes. A tag that disagrees with the version +fails before anything reaches the registry, which matters because a published +npm version can never be replaced. + +### One-time setup + +Trusted publishing has to be enabled on the registry side once, by a package +maintainer: + +1. Open the package on npmjs.com, then **Settings → Trusted Publisher**. +2. Choose GitHub Actions and enter the organization (`rahmanow`), the repository + (`shadowtools`), and the workflow filename (`release.yml`). + +Until that is configured, the workflow's publish step fails with an +authentication error — which is the safe direction to fail in. + ## Certificate pinning Outline servers use a self-signed TLS certificate for the Management API, so the usual chain validation cannot apply and the tool disables it (`rejectUnauthorized: false`). On its own that would leave the connection open to a man-in-the-middle: anything that can answer on the server's address would be trusted.