Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading