Skip to content

chore(release): OIDC-based tag-triggered release workflow#75

Merged
ssilvius merged 2 commits intomainfrom
chore/oidc-release-workflow
Apr 11, 2026
Merged

chore(release): OIDC-based tag-triggered release workflow#75
ssilvius merged 2 commits intomainfrom
chore/oidc-release-workflow

Conversation

@ssilvius
Copy link
Copy Markdown
Contributor

Summary

  • Adds .github/workflows/release.yml -- tag-triggered (v*) release workflow that publishes all 9 packages via npm OIDC trusted publishers. No npm tokens, anywhere.
  • Adds RELEASING.md -- one-time trusted-publisher setup per package, release procedure, troubleshooting, and OIDC design notes.
  • Mirrors the cleaned-up pattern from `rafters-studio/rafters` after six fix commits' worth of hard-won lessons.

Why

The previous plan assumed token-based publishing via `~/.npmrc`. That path is dead -- modern npm publishing uses OIDC trusted publishers via GitHub Actions. The `rafters-studio/rafters` repo took six fix commits to stabilize OIDC publishing (`fix: remove registry-url to stop .npmrc blocking OIDC`, `fix: clear NODE_AUTH_TOKEN for npm OIDC publishing`, etc.). This PR lands the stabilized version for mail and documents every gotcha so the next agent does not repeat the same mistakes.

The workflow

```yaml
permissions:
id-token: write # required for OIDC exchange
contents: write # required to create GitHub release

on:
push:
tags: ['v*'] # tags are the only thing that ships
```

Key rules enforced by the workflow:

  • No `registry-url` on `setup-node`. It writes a `.npmrc` with `_authToken=${NODE_AUTH_TOKEN}` that blocks OIDC.
  • No `NODE_AUTH_TOKEN` env anywhere. Any value (including empty) short-circuits the OIDC exchange.
  • `NPM_CONFIG_PROVENANCE=true` on the publish step triggers the OIDC trusted-publisher flow for every `npm publish` invocation.
  • `pnpm changeset publish` iterates all 9 packages and publishes each one with provenance.

The one-time setup

Before the first `0.1.0` release can succeed, every package needs a trusted publisher configured on npmjs.org. This is per-package and must be done before the first publish of each of:

  • `@rafters/mail`, `@rafters/mail-resend`, `@rafters/mail-cloudflare`, `@rafters/mail-react-email`, `@rafters/mail-workers-ai`, `@rafters/better-auth-resend`, `@rafters/mail-imap`, `@rafters/mail-imap-cloudflare`, `@rafters/mail-imap-server`

Trusted publisher config per package:

  • Publisher: GitHub Actions
  • Owner: `rafters-studio`
  • Repo: `mail`
  • Workflow filename: `release.yml`
  • Environment: (blank)

Step-by-step with the pending-publisher flow for brand-new packages is in `RELEASING.md`.

Release procedure

Once trusted publishers are configured:

```bash
git checkout main && git pull
pnpm changeset version # consume changesets, bump versions
git add . && git commit -m "chore(release): 0.1.0"
git tag v0.1.0
git push origin main --tags # tag push triggers workflow
```

RELEASING.md content

  • One-time setup -- npm trusted-publisher configuration per package with exact fields
  • Release procedure -- copy-pasteable bash for cutting a release
  • Troubleshooting -- 5 common OIDC failure modes with diagnoses (401, 403, EBADENGINE, OIDC token expired, CI gate failures)
  • OIDC design notes -- explicit do/don't list with reasoning

Test plan

  • Workflow YAML is valid -- written against the actions/setup-node@v4, pnpm/action-setup@v4, actions/checkout@v4 contracts
  • Mirrors the stabilized rafters pattern after its 6 fix commits
  • `legion-simplify` gate: clean
  • Format check: clean
  • End-to-end verification requires the one-time trusted-publisher setup on npmjs.org (out of scope for this PR -- tracked in RELEASING.md)

Co-dependencies

ssilvius and others added 2 commits April 11, 2026 04:33
Adds .github/workflows/release.yml and RELEASING.md. Mirrors the
cleaned-up pattern from rafters-studio/rafters (which took six fix
commits to stabilize), with the gotchas documented in both the
workflow file header and RELEASING.md so the next agent does not
repeat the same mistakes.

Key design:
- Tag-triggered on v* tag push. Tags are the only thing that ships
  to npm; no automatic publish on merge to main.
- permissions: id-token: write for the OIDC exchange.
- NPM_CONFIG_PROVENANCE=true on the publish step so every npm publish
  invocation uses trusted-publisher auth.
- No registry-url on setup-node (it writes a .npmrc that blocks OIDC).
- No NODE_AUTH_TOKEN env anywhere (any value short-circuits OIDC).
- Uses pnpm changeset publish so the changeset-based 9-package release
  flow keeps working; changesets iterates every package with an
  unpublished version and runs npm publish for each.

RELEASING.md covers:
- The one-time trusted-publisher setup required per package on
  npmjs.org before the first publish of each of the nine packages.
- The manual release procedure (changeset version -> commit -> tag
  -> push).
- Troubleshooting for the most common OIDC failure modes.
- Design notes on what not to do and why.

Note: the nine packages still need per-package trusted-publisher
configuration on npmjs.org before the first ever 0.1.0 release can
succeed. See the "One-time setup" section of RELEASING.md for the
step-by-step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Corrects the "one-time setup" section. A trusted publisher cannot be
configured on a package that does not yet exist on npm -- npm has no
pending-publisher flow for packages that have never been published.
The first publish of each package must be a manual npm publish from
a maintainer machine with personal npm login. After the first publish
creates the package on the registry, the trusted publisher can be
configured, and every subsequent release goes through the OIDC
workflow.

New bootstrap section covers:
- Prerequisites (npm login, org creation, build first)
- Dependency order for the nine packages (informational, not enforced)
- Copy-pasteable per-package first-publish commands

The trusted publisher configuration section moved to run AFTER the
first publish, which is when the npmjs.org access page for each
package actually exists.

Everything after "Release procedure" is unchanged -- once bootstrap
is done, subsequent releases are tag-triggered OIDC as before.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ssilvius ssilvius merged commit 11b4e0a into main Apr 11, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant