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
115 changes: 97 additions & 18 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,98 @@ on:
push:
branches: [main]

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
release:
name: Release
mode:
name: Detect release mode
runs-on: ubuntu-latest
permissions:
contents: read # read-only detection (changeset files + remote tag lookup)
outputs:
has_changesets: ${{ steps.detect.outputs.has_changesets }}
needs_publish: ${{ steps.detect.outputs.needs_publish }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with:
persist-credentials: false

- name: Detect pending changesets and unpublished version
id: detect
run: |
shopt -s nullglob
has_changesets=false
for f in .changeset/*.md; do
[[ "$(basename "$f")" == "README.md" ]] && continue
has_changesets=true
break
done

# Only publish when the current version has never been tagged, so
# docs-only merges don't leave a run stuck awaiting approval.
needs_publish=false
if [[ "$has_changesets" == "false" ]]; then
version=$(jq -r .version package.json)
if [[ -z "$(git ls-remote --tags origin "refs/tags/v${version}")" ]]; then
needs_publish=true
fi
fi

echo "has_changesets=$has_changesets" >> "$GITHUB_OUTPUT"
echo "needs_publish=$needs_publish" >> "$GITHUB_OUTPUT"

version:
name: Version PR
needs: mode
if: needs.mode.outputs.has_changesets == 'true'
runs-on: ubuntu-latest
concurrency:
group: release-version-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: write # push the changeset-release branch
pull-requests: write # open/update the Version Packages PR
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
persist-credentials: false

- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version-file: package.json

# No build needed: version-packages only runs `changeset version` and oxfmt.
- run: bun install --frozen-lockfile

# GITHUB_TOKEN can't create PRs under org policy, and PRs it opens don't
# trigger CI; the App token does both.
- name: Generate qa-wolf-ops token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ secrets.QA_WOLF_OPS_CLIENT_ID }}
private-key: ${{ secrets.QA_WOLF_OPS_PRIVATE_KEY }}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
permission-contents: write
permission-pull-requests: write

- name: Create or update Version Packages PR
uses: changesets/action@63a615b9cd06ba9a3e6d13796c7fbcb080a60a0b # v1.8.0
with:
version: bun run version-packages
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}

publish:
name: Publish
needs: mode
if: needs.mode.outputs.needs_publish == 'true'
environment: release
runs-on: ubuntu-latest
concurrency:
group: release-publish-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
id-token: write
contents: write # push the release tag and create the GitHub Release
id-token: write # OIDC for npm provenance
outputs:
published: ${{ steps.changesets.outputs.published }}
tag: ${{ steps.tag.outputs.tag }}
Expand All @@ -33,27 +112,27 @@ jobs:

- run: bun run build

# GITHUB_TOKEN can't create PRs under org policy, and PRs it opens don't
# trigger CI; the App token does both.
# The App token creates the GitHub Release, so the release:published
# trigger in release-binaries fires (default-token releases don't
# trigger workflows).
- name: Generate qa-wolf-ops token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ secrets.QA_WOLF_OPS_CLIENT_ID }}
private-key: ${{ secrets.QA_WOLF_OPS_PRIVATE_KEY }}
permission-contents: write

- name: Create Release PR or Publish
- name: Publish to npm
id: changesets
uses: changesets/action@63a615b9cd06ba9a3e6d13796c7fbcb080a60a0b # v1.8.0
with:
version: bun run version-packages
publish: bunx changeset publish
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true

# Only set when a publish actually happened; safe on version-mode runs.
- name: Resolve release tag
id: tag
if: steps.changesets.outputs.published == 'true'
Expand All @@ -65,10 +144,10 @@ jobs:

binaries:
name: Binaries
needs: release
if: needs.release.outputs.published == 'true'
needs: publish
if: needs.publish.outputs.published == 'true'
permissions:
contents: write
contents: write # upload binaries to the GitHub Release (reusable workflow)
uses: ./.github/workflows/release-binaries.yml
with:
tag: ${{ needs.release.outputs.tag }}
tag: ${{ needs.publish.outputs.tag }}
8 changes: 5 additions & 3 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,18 @@ PRs that do not affect the published package (CI config, internal tooling, docs)
## Release flow

1. One or more PRs with changeset files merge to `main`.
2. The [Release workflow](../.github/workflows/release.yml) detects the pending changesets and creates (or updates) a **"Version Packages"** PR. This PR bumps `package.json` version and updates `CHANGELOG.md`.
2. The [Release workflow](../.github/workflows/release.yml) detects the pending changesets and creates (or updates) a **"Version Packages"** PR. This PR bumps `package.json` version and updates `CHANGELOG.md`. It runs automatically, with no approval required.
3. Review the Version Packages PR and merge it when ready to ship.
4. The Release workflow runs again. It detects no pending changesets, builds the package, and runs:
4. Merging the Version Packages PR triggers the workflow's `Publish` job, which pauses for approval of the `release` environment (Actions → the run → "Review deployments"). Once approved, it builds the package and runs:

```bash
bunx changeset publish
```

`changeset publish` publishes to npm and creates a git tag for the new version. The changesets action pushes that tag and creates a GitHub Release from it, which triggers the binary build pipeline. npm [provenance](https://docs.npmjs.com/generating-provenance-statements) is enabled via the `NPM_CONFIG_PROVENANCE: true` env var on the publish step.

Merges to `main` with no pending changesets and an already-tagged version (docs/CI-only merges) skip both the version and publish jobs, so they never leave a run stuck awaiting approval.

## After publish

Verify the release:
Expand All @@ -44,4 +46,4 @@ qawolf --version # should match package.json version

## Re-running a failed publish

If the publish step fails (e.g. network issue after the version PR was merged), re-run the Release workflow from the GitHub Actions UI (Actions → Release → Re-run jobs). If the version was already partially published, npm will return an error indicating the version exists — in that case, cut a patch release with a new changeset rather than attempting to re-publish the same version.
If the publish step fails (e.g. network issue after the version PR was merged), re-run the Release workflow from the GitHub Actions UI (Actions → Release → Re-run jobs). This requests approval of the `release` environment again. "Re-run failed jobs" preserves the `mode` job's outputs, so the `Publish` job re-runs without re-evaluating whether a publish is needed. If the version was already partially published, npm will return an error indicating the version exists — in that case, cut a patch release with a new changeset rather than attempting to re-publish the same version.
Loading