Skip to content
Draft
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
13 changes: 13 additions & 0 deletions .changeset/trusted-publishing-provenance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@knocklabs/client": patch
"@knocklabs/eslint-config": patch
"@knocklabs/expo": patch
"@knocklabs/prettier-config": patch
"@knocklabs/react": patch
"@knocklabs/react-core": patch
"@knocklabs/react-native": patch
"@knocklabs/types": patch
"@knocklabs/typescript-config": patch
---

Publish from GitHub Actions through npm trusted publishing (OIDC) with signed provenance, and point each package's `repository` field at its directory in this repo.
26 changes: 14 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,22 @@ concurrency:
group: publish-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

# `id-token: write` lets Yarn authenticate to npm through trusted publishing
# (OIDC) and sign provenance. Pushes and release PRs use the knock-eng-bot
# token, so the built-in GITHUB_TOKEN only needs read access.
permissions:
contents: read
id-token: write

jobs:
release:
name: Release
runs-on: ubuntu-latest
# npm's trusted publisher config does not check the branch, so publishing is
# gated on this environment instead. Its deployment branch policy allows only
# main, canary, and rc, and each npm trusted publisher requires the same
# environment name.
environment: production-release

steps:
# Checkout the repository with full history and tag data so that
Expand Down Expand Up @@ -46,7 +58,6 @@ jobs:
- name: Setup Node.js 20.x
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
registry-url: "https://registry.npmjs.org"
node-version-file: "package.json"
cache: "yarn"

Expand Down Expand Up @@ -103,17 +114,10 @@ jobs:
[ "$TAG" = "stable" ] && TAG=latest
echo "NPM_CONFIG_TAG=$TAG" >> "$GITHUB_ENV"

# Configure Yarn to authenticate with the npm registry using a token.
- name: Set NPM Auth
run: |
yarn config set npmRegistryServer "https://registry.npmjs.org"
yarn config set npmAlwaysAuth true
yarn config set npmAuthToken $NPM_TOKEN
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

# Create a versioning PR if changeset files exist, or publish packages
# to npm if version bump commits have already been merged into the branch.
# Publishing authenticates through npm trusted publishing, so no npm token
# is configured. Yarn uses `npmAuthToken` ahead of OIDC when one is set.
- name: Create release PR or publish
uses: changesets/action@63a615b9cd06ba9a3e6d13796c7fbcb080a60a0b # v1.8.0
with:
Expand All @@ -123,5 +127,3 @@ jobs:
publish: yarn release:publish
env:
GITHUB_TOKEN: ${{ secrets.KNOCK_ENG_BOT_GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
1,000 changes: 1,000 additions & 0 deletions .yarn/releases/yarn-4.18.1.cjs

Large diffs are not rendered by default.

948 changes: 0 additions & 948 deletions .yarn/releases/yarn-4.9.1.cjs

This file was deleted.

13 changes: 12 additions & 1 deletion .yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,15 @@ enableScripts: false

nodeLinker: node-modules

yarnPath: .yarn/releases/yarn-4.9.1.cjs
# Yarn 4.15 made a one-day minimum release age the default. Keep resolving
# brand-new versions, as Yarn did before that release.
npmMinimalAgeGate: 0

# Releases publish from GitHub Actions through npm trusted publishing (OIDC),
# see .github/workflows/release.yml. Publish to npmjs.org directly rather than
# Yarn's default registry.yarnpkg.com alias. Provenance is requested by the
# `release:publish` script rather than here, because Yarn fails to generate it
# outside CI and that would break local `yarn npm publish --dry-run`.
npmPublishRegistry: "https://registry.npmjs.org"

yarnPath: .yarn/releases/yarn-4.18.1.cjs
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ yarn format

- **Monorepo**: Uses Yarn workspaces + Turborepo
- **Node version**: 24.13.0 (see `engines` in package.json)
- **Package manager**: Yarn 4.1.0
- **Package manager**: Yarn 4.18.1
- **Testing**: Vitest
- **Releases**: Changesets for versioning

Expand Down
36 changes: 34 additions & 2 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,38 @@ npm install @knocklabs/some-package

---

## Publishing to npm

Releases publish from [`release.yml`](.github/workflows/release.yml) through npm trusted publishing (OIDC). CI holds no npm token: each run gets a short-lived token from GitHub, and every published version carries a signed provenance statement. Only runs in the `production-release` environment can publish, and that environment only allows `main`, `canary`, and `rc`.

### Setting Up Trusted Publishing

Each package needs a trusted publisher on npm that points at this repo, `release.yml`, and the `production-release` environment.

Before you add any, check that the `production-release` environment (repo Settings → Environments) only allows the `main`, `canary`, and `rc` branches, and that admins can't bypass it. npm doesn't check the branch, so until that policy is in place, any branch that runs `release.yml` in this environment can publish.

Then set up the trusted publishers from your machine:

```bash
npm install -g npm@latest # npm trust needs npm 11.15.0+
npm login # your npm account needs 2FA
yarn release:bootstrap # all publishable packages, or name specific ones
```

`npm trust` asks for 2FA once, and its 5-minute skip window covers every package. npm drops a new trusted publisher that doesn't publish within 2 days, so run this while the Version PR that releases the packages is open, and merge it within 2 days. Add `--dry-run` to print the npm commands without running them.

### Adding a New Published Package

npm can't publish the first version of a new package name through trusted publishing. `yarn release:bootstrap` works around this:

1. Create the package under `packages/` with a `repository` field that includes `directory`, and a version above `0.0.0`. Add a changeset for it.
2. Once the Version PR that releases it is open, run `yarn release:bootstrap @knocklabs/<name>`. It publishes a `0.0.0` placeholder so the name exists, then adds the trusted publisher.
3. Merge the Version PR within 2 days. The real first version publishes from CI like every other package.

If a release includes a package that isn't on npm yet, `release:publish` fails before it builds anything and names the command to run.

---

## Safeguards in CI

- `main` is blocked from publishing if `.changeset/pre.json` exists (pre-release guard).
Expand All @@ -156,8 +188,8 @@ npx changeset pre exit
# Apply version bumps and changelog updates
npx changeset version

# Publish packages (CI will handle this normally)
npx changeset publish
# Set up npm trusted publishing (publishing itself only runs in CI)
yarn release:bootstrap [package...] [--dry-run]
```

> `npx changeset version`: Applies all pending changesets, bumps versions, and updates changelogs.
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@
"test:integration:runner": "cd ./integration && yarn test:integration && cd ..",
"type:check": "turbo type:check",
"release:version": "yarn changeset version && yarn install --mode=update-lockfile",
"release:publish": "yarn build:packages && yarn workspaces foreach -Rpt --no-private --from '@knocklabs/*' npm publish --access public --tolerate-republish --tag \"$NPM_CONFIG_TAG\" && yarn changeset tag",
"release:bootstrap": "node scripts/npm-trusted-publishing.mjs bootstrap",
"release:publish": "node scripts/npm-trusted-publishing.mjs check && yarn build:packages && yarn workspaces foreach -Rpt --no-private --from '@knocklabs/*' npm publish --access public --provenance --tolerate-republish --tag \"$NPM_CONFIG_TAG\" && yarn changeset tag",
"postinstall": "manypkg check"
},
"prettier": "@knocklabs/prettier-config",
"packageManager": "yarn@4.9.1",
"packageManager": "yarn@4.18.1",
"workspaces": [
"examples/*",
"packages/*",
Expand Down
3 changes: 2 additions & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
],
"repository": {
"type": "git",
"url": "git+https://github.com/knocklabs/javascript.git"
"url": "git+https://github.com/knocklabs/javascript.git",
"directory": "packages/client"
},
"bugs": {
"url": "https://github.com/knocklabs/javascript/issues"
Expand Down
5 changes: 5 additions & 0 deletions packages/eslint-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
{
"name": "@knocklabs/eslint-config",
"version": "0.0.8",
"repository": {
"type": "git",
"url": "git+https://github.com/knocklabs/javascript.git",
"directory": "packages/eslint-config"
},
"files": [
"library.js",
"next.js",
Expand Down
3 changes: 2 additions & 1 deletion packages/expo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/knocklabs/javascript.git"
"url": "git+https://github.com/knocklabs/javascript.git",
"directory": "packages/expo"
},
"bugs": {
"url": "https://github.com/knocklabs/javascript/issues"
Expand Down
6 changes: 5 additions & 1 deletion packages/prettier-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
"name": "@knocklabs/prettier-config",
"version": "0.0.1",
"main": "index.js",
"repository": "https://github.com/knocklabs/javascript/blob/main/packages/prettier-config",
"repository": {
"type": "git",
"url": "git+https://github.com/knocklabs/javascript.git",
"directory": "packages/prettier-config"
},
"author": "@knocklabs",
"license": "MIT",
"exports": {
Expand Down
3 changes: 2 additions & 1 deletion packages/react-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/knocklabs/javascript.git"
"url": "git+https://github.com/knocklabs/javascript.git",
"directory": "packages/react-core"
},
"bugs": {
"url": "https://github.com/knocklabs/javascript/issues"
Expand Down
3 changes: 2 additions & 1 deletion packages/react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/knocklabs/javascript.git"
"url": "git+https://github.com/knocklabs/javascript.git",
"directory": "packages/react-native"
},
"bugs": {
"url": "https://github.com/knocklabs/javascript/issues"
Expand Down
3 changes: 2 additions & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/knocklabs/javascript.git"
"url": "git+https://github.com/knocklabs/javascript.git",
"directory": "packages/react"
},
"bugs": {
"url": "https://github.com/knocklabs/javascript/issues"
Expand Down
5 changes: 5 additions & 0 deletions packages/types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
"author": "@knocklabs",
"version": "0.1.6",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/knocklabs/javascript.git",
"directory": "packages/types"
},
"types": "./src/index.d.ts",
"scripts": {
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
Expand Down
5 changes: 5 additions & 0 deletions packages/typescript-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
"name": "@knocklabs/typescript-config",
"version": "0.0.2",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/knocklabs/javascript.git",
"directory": "packages/typescript-config"
},
"publishConfig": {
"access": "public"
}
Expand Down
Loading
Loading