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
9 changes: 0 additions & 9 deletions .github/canary.sh

This file was deleted.

97 changes: 69 additions & 28 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
name: Release
on:
workflow_dispatch:
inputs:
mode:
description: 'Release mode (version = open the Version Packages PR only, no npm publish)'
type: choice
options: [version, staged, publish]
default: version
push:
branches: ['main', 'alpha', 'beta', 'rc']
paths:
Expand Down Expand Up @@ -34,7 +28,8 @@ jobs:
pull-requests: write
issues: write
outputs:
released: ${{ steps.changesets.outputs.staged == 'true' || steps.changesets.outputs.published == 'true' }}
staged: ${{ steps.changesets.outputs.staged }}
staged_packages: ${{ steps.changesets.outputs.staged_packages }}
steps:
- name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
Expand All @@ -48,49 +43,38 @@ jobs:
cache: 'false'
force-install: 'true'

- name: Resolve release command
id: cmd
shell: bash
# MODE comes from env to avoid script injection from workflow inputs.
env:
MODE: ${{ inputs.mode || 'publish' }}
run: |
case "$MODE" in
publish) echo "publish=pnpm exec changeset publish" >> "$GITHUB_OUTPUT" ;;
staged) echo "publish=pnpm release:stage:ci" >> "$GITHUB_OUTPUT" ;;
version) echo "publish=" >> "$GITHUB_OUTPUT" ;;
*) echo "Unknown mode: $MODE" >&2; exit 1 ;;
esac

- name: Build
if: ${{ steps.cmd.outputs.publish != '' }}
run: pnpm run build

- name: Test
if: ${{ steps.cmd.outputs.publish != '' }}
run: pnpm run test --coverage

- name: Upload coverage reports to Codecov
if: ${{ steps.cmd.outputs.publish != '' }}
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

# createGithubReleases stays off: a staged version isn't public until
# `promote` confirms npm approval, so nothing should announce it yet.
- name: Release
id: changesets
uses: changesets/action@63a615b9cd06ba9a3e6d13796c7fbcb080a60a0b # v1
with:
version: pnpm exec changeset version
publish: ${{ steps.cmd.outputs.publish }}
publish: 'node scripts/release.mjs'
commit: 'ci(changesets): version packages'
title: 'ci(changesets): version packages'
setupGitUser: false
createGithubReleases: false
env:
NPM_CONFIG_PROVENANCE: true
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# Canary skips staging on purpose, so it stays automatic on every push.
# If the npm Trusted Publisher for these packages is ever locked to
# stage-only, this step will need its own non-stage-restricted entry.
- name: Publish canary
if: ${{ steps.changesets.outputs.published != 'true' && steps.changesets.outputs.staged != 'true' && github.event_name == 'push' && github.ref == 'refs/heads/main' }}
if: ${{ steps.changesets.outputs.staged != 'true' && github.event_name == 'push' && github.ref == 'refs/heads/main' }}
continue-on-error: true
shell: bash
env:
Expand All @@ -108,10 +92,67 @@ jobs:
pnpm run build
pnpm exec changeset publish --tag canary

promote:
name: Promote staged release
needs: [release]
if: ${{ needs.release.outputs.staged == 'true' }}
environment:
name: npm-release-approval
timeout-minutes: 60
runs-on: ubuntu-latest
permissions:
contents: write # gh release create
outputs:
approved: ${{ steps.verify.outputs.approved }}
steps:
- name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0

- name: Setup Tools
uses: kubb-labs/config/.github/setup@d2daeb58eb7dc8c19d595c926b4788b7f682fb51 # main
with:
node-version: '22.22.3'
cache: 'false'

# A green environment review only means a maintainer clicked approve,
# not that they actually ran `npm stage approve`. This confirms the
# staged versions are really live on the registry before anything
# public fires.
- name: Verify staged versions are live on npm
id: verify
run: node scripts/verifyRegistry.mjs
env:
STAGED_PACKAGES: ${{ needs.release.outputs.staged_packages }}
# How often to re-poll npm while waiting for a staged version to
# propagate, and how many times to try before giving up.
VERIFY_INTERVAL_SECONDS: '300' # in seconds
VERIFY_RETRY_ATTEMPTS: '10'

# Tags are created here, not at stage time, so a rejected or expired
# staged package never leaves a dangling tag behind (see
# changesets/changesets#2025). The versions are already confirmed live
# above, so tagging now always points at something real.
- name: Tag released versions
run: |
pnpm exec changeset tag
git push --tags

# Every staged package gets its own release here, since packages in
# this repo version and changelog independently (see the empty `fixed`
# and `linked` groups in .changeset/config.json) β€” unlike kubb-labs/kubb,
# where every package shares one fixed version and one combined release.
- name: Create GitHub Releases
run: node scripts/createReleases.mjs
env:
STAGED_PACKAGES: ${{ needs.release.outputs.staged_packages }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

deploy-platform:
name: Dispatch content-updated to platform
needs: [release]
if: ${{ needs.release.outputs.released == 'true' }}
needs: [release, promote]
if: ${{ needs.promote.outputs.approved == 'true' }}
runs-on: ubuntu-latest
permissions: {}
steps:
Expand Down
20 changes: 20 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,23 @@ pnpm changeset
```

Pick the packages you changed, choose the bump (patch for fixes, minor for features, major for breaking changes), and write a short summary aimed at users. Commit the generated file under `.changeset/`. Docs-only or internal changes that touch no published package do not need one.

## Releasing

This section covers what happens after a PR with a changeset merges. Contributors don't need it, but maintainers do.

Merging a changeset into `main` queues or updates the "Version Packages" PR, opened automatically by the release workflow (`.github/workflows/release.yml`). Merging that PR triggers the release job, which stages every changed package with `pnpm stage publish` (npm's staged publishing). A staged package is not installable yet. Nothing becomes public until a maintainer approves it.

To approve a release:

1. A maintainer with npm publish access and two-factor authentication runs `npm stage approve` (or approves from npmjs.com) for each staged package.
2. The same maintainer approves the `promote` job's environment review on the workflow run in the Actions tab.
3. The `promote` job then verifies the versions are actually live on npm, tags the released versions, and creates a GitHub Release, and only then dispatches the content refresh to [kubb-labs/platform](https://github.com/kubb-labs/platform).

Packages in this repo version and changelog independently (see the empty `fixed` and `linked` groups in `.changeset/config.json`), so a release here creates one GitHub Release per staged package, tagged `<package>@<version>`, with notes taken from that package's own `CHANGELOG.md`. This differs from [kubb-labs/kubb](https://github.com/kubb-labs/kubb), where every package shares one fixed version and a release covers all of them combined. `scripts/createReleases.mjs` is shared verbatim between the two repos; the `RELEASE_MODE` environment variable picks the mode, and this repo simply never sets it to `combined`.

If a staged version turns out to be wrong, reject it with `npm stage reject` instead of approving it. Nothing downstream fires for a rejected version.

Reviewers for the `promote` job's environment are managed on GitHub, under the repository's Settings > Environments > `npm-release-approval` (this is separate from npm's own settings on npmjs.com).

Canary releases are the one exception to this flow. Every push to `main` stamps each package to its own unique `-canary.<timestamp>` version and publishes it under the `canary` dist-tag directly, without staging, so canary installs stay immediate and automatic. See the comment above the `Publish canary` step in `release.yml` for why this is safe to leave unstaged.
20 changes: 20 additions & 0 deletions internals/changelog/format.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// The heading grammar shared by the changelog writer (index.mjs) and anything
// that reads CHANGELOG.md back out (scripts/createReleases.mjs). Kept in one
// place so the two can't drift into two independently-escaped, independently
// anchored regexes for the same headings.

function escapeForRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}

// The lookahead after the version rejects a longer, hyphen-suffixed version
// sharing the same numeric prefix (e.g. searching for "5.0.0" must not match
// a "## v5.0.0-rc.1" heading), while still matching the exact version
// followed by whitespace, a separator, or end of line.
export function versionHeadingPattern(version) {
return new RegExp(`^##\\s+v?${escapeForRegExp(version)}(?![\\w.-]).*$`, 'm')
}

export function packageHeadingPattern(name) {
return new RegExp(`^###\\s+${escapeForRegExp(name)}\\s*$`, 'm')
}
26 changes: 26 additions & 0 deletions internals/changelog/format.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest'
import { packageHeadingPattern, versionHeadingPattern } from './format.mjs'

describe('versionHeadingPattern', () => {
it('matches the exact version heading', () => {
expect(versionHeadingPattern('5.0.0-beta.99').test('## 5.0.0-beta.99')).toBe(true)
})

it('does not match a longer version sharing the same numeric prefix', () => {
expect(versionHeadingPattern('5.0.0').test('## v5.0.0-rc.1')).toBe(false)
})

it('matches a bare version with no prerelease suffix', () => {
expect(versionHeadingPattern('5.0.0').test('## 5.0.0')).toBe(true)
})
})

describe('packageHeadingPattern', () => {
it('matches an exact package heading', () => {
expect(packageHeadingPattern('@kubb/plugin-axios').test('### @kubb/plugin-axios')).toBe(true)
})

it('does not match a different package heading with a shared prefix', () => {
expect(packageHeadingPattern('@kubb/plugin-ts').test('### @kubb/plugin-ts-extra')).toBe(false)
})
})
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@
"lint": "oxlint -c oxlint.config.ts ./packages ./examples",
"lint:fix": "oxlint -c oxlint.config.ts --fix ./packages ./examples",
"postgenerate": "pnpm run lint:fix && pnpm run format",
"release:stage": "turbo run release:stage --filter=./packages/* --continue --concurrency=1 --",
"release:stage:ci": "pnpm release:stage && pnpm exec changeset tag && echo \"staged=true\" >> \"${GITHUB_OUTPUT:-/dev/null}\"",
"start": "turbo run start --filter=./packages/*",
"test": "vitest run --config ./configs/vitest.config.ts",
"test:bench": "vitest bench --config ./configs/vitest.bench.config.ts",
Expand Down
3 changes: 0 additions & 3 deletions packages/plugin-axios/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,6 @@
"clean": "node -e \"require('node:fs').rmSync('./dist', {recursive:true,force:true})\"",
"lint": "oxlint .",
"lint:fix": "oxlint --fix .",
"release": "pnpm publish --no-git-check",
"release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
"release:stage": "pnpm stage publish --no-git-check",
"start": "tsdown --watch",
"test": "vitest --passWithNoTests",
"typecheck": "tsc -p ./tsconfig.json --noEmit --emitDeclarationOnly false"
Expand Down
3 changes: 0 additions & 3 deletions packages/plugin-cypress/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,6 @@
"clean": "node -e \"require('node:fs').rmSync('./dist', {recursive:true,force:true})\"",
"lint": "oxlint .",
"lint:fix": "oxlint --fix .",
"release": "pnpm publish --no-git-check",
"release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
"release:stage": "pnpm stage publish --no-git-check",
"start": "tsdown --watch",
"test": "vitest --passWithNoTests",
"typecheck": "tsc -p ./tsconfig.json --noEmit --emitDeclarationOnly false"
Expand Down
3 changes: 0 additions & 3 deletions packages/plugin-faker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,6 @@
"clean": "node -e \"require('node:fs').rmSync('./dist', {recursive:true,force:true})\"",
"lint": "oxlint .",
"lint:fix": "oxlint --fix .",
"release": "pnpm publish --no-git-check",
"release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
"release:stage": "pnpm stage publish --no-git-check",
"start": "tsdown --watch",
"test": "vitest --passWithNoTests",
"typecheck": "tsc -p ./tsconfig.json --noEmit --emitDeclarationOnly false"
Expand Down
3 changes: 0 additions & 3 deletions packages/plugin-fetch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,6 @@
"clean": "node -e \"require('node:fs').rmSync('./dist', {recursive:true,force:true})\"",
"lint": "oxlint .",
"lint:fix": "oxlint --fix .",
"release": "pnpm publish --no-git-check",
"release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
"release:stage": "pnpm stage publish --no-git-check",
"start": "tsdown --watch",
"test": "vitest --passWithNoTests",
"typecheck": "tsc -p ./tsconfig.json --noEmit --emitDeclarationOnly false"
Expand Down
3 changes: 0 additions & 3 deletions packages/plugin-mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,6 @@
"clean": "node -e \"require('node:fs').rmSync('./dist', {recursive:true,force:true})\"",
"lint": "oxlint .",
"lint:fix": "oxlint --fix .",
"release": "pnpm publish --no-git-check",
"release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
"release:stage": "pnpm stage publish --no-git-check",
"start": "tsdown --watch",
"test": "vitest --passWithNoTests",
"typecheck": "tsc -p ./tsconfig.json --noEmit --emitDeclarationOnly false"
Expand Down
3 changes: 0 additions & 3 deletions packages/plugin-msw/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,6 @@
"clean": "node -e \"require('node:fs').rmSync('./dist', {recursive:true,force:true})\"",
"lint": "oxlint .",
"lint:fix": "oxlint --fix .",
"release": "pnpm publish --no-git-check",
"release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
"release:stage": "pnpm stage publish --no-git-check",
"start": "tsdown --watch",
"test": "vitest --passWithNoTests",
"typecheck": "tsc -p ./tsconfig.json --noEmit --emitDeclarationOnly false"
Expand Down
3 changes: 0 additions & 3 deletions packages/plugin-react-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,6 @@
"clean": "node -e \"require('node:fs').rmSync('./dist', {recursive:true,force:true})\"",
"lint": "oxlint .",
"lint:fix": "oxlint --fix .",
"release": "pnpm publish --no-git-check",
"release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
"release:stage": "pnpm stage publish --no-git-check",
"start": "tsdown --watch",
"test": "vitest --passWithNoTests",
"typecheck": "tsc -p ./tsconfig.json --noEmit --emitDeclarationOnly false"
Expand Down
3 changes: 0 additions & 3 deletions packages/plugin-redoc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,6 @@
"clean": "node -e \"require('node:fs').rmSync('./dist', {recursive:true,force:true})\"",
"lint": "oxlint .",
"lint:fix": "oxlint --fix .",
"release": "pnpm publish --no-git-check",
"release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
"release:stage": "pnpm stage publish --no-git-check",
"start": "tsdown --watch",
"test": "vitest --passWithNoTests",
"typecheck": "tsc -p ./tsconfig.json --noEmit --emitDeclarationOnly false"
Expand Down
3 changes: 0 additions & 3 deletions packages/plugin-swr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,6 @@
"clean": "node -e \"require('node:fs').rmSync('./dist', {recursive:true,force:true})\"",
"lint": "oxlint .",
"lint:fix": "oxlint --fix .",
"release": "pnpm publish --no-git-check",
"release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
"release:stage": "pnpm stage publish --no-git-check",
"start": "tsdown --watch",
"test": "vitest --passWithNoTests",
"typecheck": "tsc -p ./tsconfig.json --noEmit --emitDeclarationOnly false"
Expand Down
3 changes: 0 additions & 3 deletions packages/plugin-ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,6 @@
"clean": "node -e \"require('node:fs').rmSync('./dist', {recursive:true,force:true})\"",
"lint": "oxlint .",
"lint:fix": "oxlint --fix .",
"release": "pnpm publish --no-git-check",
"release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
"release:stage": "pnpm stage publish --no-git-check",
"start": "tsdown --watch",
"test": "vitest --passWithNoTests",
"typecheck": "tsc -p ./tsconfig.json --noEmit --emitDeclarationOnly false"
Expand Down
3 changes: 0 additions & 3 deletions packages/plugin-vue-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,6 @@
"clean": "node -e \"require('node:fs').rmSync('./dist', {recursive:true,force:true})\"",
"lint": "oxlint .",
"lint:fix": "oxlint --fix .",
"release": "pnpm publish --no-git-check",
"release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
"release:stage": "pnpm stage publish --no-git-check",
"start": "tsdown --watch",
"test": "vitest --passWithNoTests",
"typecheck": "tsc -p ./tsconfig.json --noEmit --emitDeclarationOnly false"
Expand Down
3 changes: 0 additions & 3 deletions packages/plugin-zod/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,6 @@
"clean": "node -e \"require('node:fs').rmSync('./dist', {recursive:true,force:true})\"",
"lint": "oxlint .",
"lint:fix": "oxlint --fix .",
"release": "pnpm publish --no-git-check",
"release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
"release:stage": "pnpm stage publish --no-git-check",
"start": "tsdown --watch",
"test": "vitest --passWithNoTests",
"typecheck": "tsc -p ./tsconfig.json --noEmit --emitDeclarationOnly false"
Expand Down
Loading
Loading