Skip to content

Commit 4e00a9a

Browse files
authored
Merge pull request #25 from stackbox-dev/ci/auto-release
ci: make merging a version bump the whole release
2 parents 162aa13 + c74406f commit 4e00a9a

5 files changed

Lines changed: 172 additions & 103 deletions

File tree

‎.claude/skills/release/SKILL.md‎

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,39 @@ Standardized release workflow for @stackbox-dev/fp-plugins.
1010

1111
## Workflow
1212

13-
1. **Pre-flight checks**
14-
- Ensure working tree is clean (`git status`)
15-
- Ensure on `main` branch
16-
- Run `pnpm test` to verify all tests pass
17-
- Run `pnpm run build` to verify build succeeds
18-
19-
2. **Version bump**
20-
- Ask user for bump type: patch, minor, or major
21-
- Update `version` in `package.json`
22-
- Create a commit with message: `X.Y.Z` (version number only, matching existing convention)
23-
24-
3. **Verify**
25-
- Show the user the version diff and commit
26-
- Remind the user that publishing is triggered by **creating a GitHub Release**,
27-
not by pushing. `npm-publish-github-packages.yml` runs `on: release: [created]`;
28-
a push to `main` alone publishes nothing.
13+
1. **Pre-flight**
14+
- Working tree clean, on `main`, up to date
15+
- `pnpm test` and `pnpm run build` pass
16+
17+
2. **Cut the version on a branch** — `main` requires a PR, so this cannot be pushed
18+
directly:
19+
20+
```bash
21+
git checkout -b release/X.Y.Z
22+
pnpm version <patch|minor|major> # commit "X.Y.Z" + local tag vX.Y.Z
23+
git push -u origin release/X.Y.Z # branch only, not the tag
24+
```
25+
26+
Ask the user for the bump type. `pnpm version` writes a commit whose message is
27+
just the version, matching existing history.
28+
29+
3. **Open the PR**, titled with the version (see `release/2.14.0`, PR #4).
30+
31+
4. **Merging is the release.** `.github/workflows/release.yml` runs on every push to
32+
`main`: if `package.json`'s version has no matching `vX.Y.Z` tag, it runs the full
33+
gate (lint, build, unit tests, MinIO integration), then tags, creates the GitHub
34+
Release and publishes to GitHub Packages. Nothing else is required.
2935

3036
## Notes
3137

32-
- The version bump belongs on `main` and nowhere else. Never put one in a feature or
33-
fix PR — those PRs describe changes, releases decide versions. A bump riding along
34-
in a feature branch also makes the PR harder to review and forces a rebuild if the
35-
release is deferred.
36-
- Do NOT push automatically — let the user decide when to push
37-
- Do NOT create git tags by hand. Creating the GitHub Release creates the tag.
38-
- Follow existing commit message convention (see `git log` — version bumps use just the version number like `2.12.0`)
39-
- `main` is protected by a ruleset requiring one approving review, code-owner review,
40-
and `require_last_push_approval` — so any push after an approval dismisses it. Get
41-
the branch final before asking for review.
38+
- **Do not push the tag by hand.** The workflow creates it. `pnpm version` makes one
39+
locally; delete it (`git tag -d vX.Y.Z`) or just leave it unpushed.
40+
- **Do not create the GitHub Release by hand.** The workflow does that too, with
41+
generated notes.
42+
- A version bump belongs on `main` and nowhere else — never in a feature or fix PR.
43+
- Pushes to `main` whose version is already tagged are a no-op, so ordinary merges do
44+
not publish.
45+
- A failed run can be retried with `workflow_dispatch` without bumping the version
46+
again; every step is idempotent.
47+
- `main` requires one approving review and sets `require_last_push_approval`, so any
48+
push after an approval dismisses it. Get the branch final before requesting review.

‎.github/workflows/npm-publish-github-packages.yml‎

Lines changed: 0 additions & 66 deletions
This file was deleted.

‎.github/workflows/release.yml‎

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Merging a version bump to main is the whole release. This workflow tags, creates the
2+
# GitHub Release and publishes to GitHub Packages.
3+
#
4+
# Why one workflow instead of "create a release, let release:published publish it":
5+
# a release created with GITHUB_TOKEN does not trigger further workflow runs — GitHub
6+
# blocks that to avoid recursion — so the publish would silently never happen.
7+
#
8+
# Every step is idempotent, so a failed run can be retried with workflow_dispatch
9+
# without bumping the version again.
10+
11+
name: Release
12+
13+
on:
14+
push:
15+
branches: [main]
16+
workflow_dispatch:
17+
18+
concurrency:
19+
group: release
20+
cancel-in-progress: false
21+
22+
env:
23+
NODE_VERSION: 24
24+
25+
jobs:
26+
check:
27+
runs-on: ubuntu-latest
28+
outputs:
29+
version: ${{ steps.check.outputs.version }}
30+
release: ${{ steps.check.outputs.release }}
31+
steps:
32+
- uses: actions/checkout@v4
33+
with:
34+
fetch-depth: 0
35+
- id: check
36+
run: |
37+
VERSION="$(node -p 'require("./package.json").version')"
38+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
39+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
40+
echo "release=true" >> "$GITHUB_OUTPUT"
41+
echo "manual run: releasing v$VERSION"
42+
elif git rev-parse -q --verify "refs/tags/v$VERSION" >/dev/null; then
43+
echo "release=false" >> "$GITHUB_OUTPUT"
44+
echo "v$VERSION is already tagged — nothing to release"
45+
else
46+
echo "release=true" >> "$GITHUB_OUTPUT"
47+
echo "v$VERSION is new — releasing"
48+
fi
49+
50+
release:
51+
needs: check
52+
if: needs.check.outputs.release == 'true'
53+
runs-on: ubuntu-latest
54+
permissions:
55+
contents: write # create the tag and the release
56+
packages: write # publish
57+
env:
58+
VERSION: ${{ needs.check.outputs.version }}
59+
steps:
60+
- uses: actions/checkout@v4
61+
with:
62+
fetch-depth: 0
63+
- uses: pnpm/action-setup@v4
64+
- uses: actions/setup-node@v4
65+
with:
66+
node-version: ${{ env.NODE_VERSION }}
67+
cache: pnpm
68+
registry-url: https://npm.pkg.github.com/
69+
- run: pnpm install --frozen-lockfile
70+
71+
# Publishing is irreversible, so the full gate runs first.
72+
- run: pnpm run lint
73+
- run: pnpm run build
74+
- run: pnpm test
75+
76+
- name: Start MinIO
77+
run: |
78+
docker run -d --name release-minio -p 9000:9000 \
79+
-e MINIO_ROOT_USER=minioadmin \
80+
-e MINIO_ROOT_PASSWORD=minioadmin \
81+
minio/minio:latest server /data
82+
for i in $(seq 1 30); do
83+
if curl -sf http://127.0.0.1:9000/minio/health/live; then
84+
echo "minio ready"; exit 0
85+
fi
86+
sleep 2
87+
done
88+
echo "minio did not become healthy" >&2
89+
exit 1
90+
- run: pnpm run test:integration
91+
env:
92+
MINIO_TEST_ENDPOINT: http://127.0.0.1:9000
93+
- name: Stop MinIO
94+
if: always()
95+
run: docker rm -f release-minio || true
96+
97+
- name: Tag
98+
run: |
99+
if git rev-parse -q --verify "refs/tags/v$VERSION" >/dev/null; then
100+
echo "tag v$VERSION already exists"
101+
else
102+
git tag "v$VERSION"
103+
git push origin "v$VERSION"
104+
fi
105+
106+
- name: GitHub Release
107+
env:
108+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
109+
run: |
110+
if gh release view "v$VERSION" >/dev/null 2>&1; then
111+
echo "release v$VERSION already exists"
112+
else
113+
gh release create "v$VERSION" --title "v$VERSION" --generate-notes
114+
fi
115+
116+
# npm publish, not pnpm publish: pnpm adds git-state checks this flow does not
117+
# need. prepublishOnly rebuilds. No --provenance: GitHub Packages rejects
118+
# attestations. A duplicate version 409s, which is the desired failure.
119+
- name: Publish
120+
env:
121+
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
122+
run: npm publish

‎CLAUDE.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ the issue thread or the PR that resolves it.
117117
- **`tsconfig.build.json` sets `skipLibCheck: true`** — still required. Turning it off
118118
fails on `thread-stream@4.2.0` (a pino dependency), whose `.d.ts` references
119119
`TransferListItem`, a name its `worker_threads` types do not export.
120-
- **Releasing**: version bumps happen on `main`, in their own commit named just the
121-
version (`2.16.0`). Do not put a version bump in a feature PR and do not create
122-
tags by hand — publishing is triggered by creating a GitHub Release. See
120+
- **Releasing**: bump the version with `pnpm version` on a `release/X.Y.Z` branch and
121+
merge it. `.github/workflows/release.yml` does the rest — tag, GitHub Release,
122+
publish — on any push to `main` whose version has no matching tag. Do not push tags
123+
or create releases by hand, and never put a version bump in a feature PR. See
123124
`.claude/skills/release/SKILL.md`.

‎CONTRIBUTING.md‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,19 @@ updated lockfile.
3535

3636
## Releases
3737

38-
**Do not put a version bump in a feature or fix PR**, and do not create tags by hand.
38+
Cut the version on a branch — `main` requires a PR:
3939

40-
Releasing is a separate act on `main`:
40+
```bash
41+
git checkout -b release/X.Y.Z
42+
pnpm version <patch|minor|major>
43+
git push -u origin release/X.Y.Z # branch only; do not push the tag
44+
```
45+
46+
Open a PR titled with the version. **Merging it is the release**: the `Release`
47+
workflow tags, creates the GitHub Release and publishes to GitHub Packages, after
48+
running lint, build, unit tests and the MinIO integration suite.
4149

42-
1. Bump `version` in `package.json` in its own commit, whose message is just the
43-
version number (`2.16.0`), matching existing history.
44-
2. Create a **GitHub Release**. That is what creates the tag and triggers
45-
`npm-publish-github-packages.yml` to publish. Pushing to `main` alone publishes
46-
nothing.
50+
Do not push tags or create releases by hand. Merges whose version is already tagged
51+
are a no-op, so normal PRs do not publish.
4752

48-
See `.claude/skills/release/SKILL.md` for the full checklist.
53+
See `.claude/skills/release/SKILL.md`.

0 commit comments

Comments
 (0)