Skip to content

Commit d405e7f

Browse files
authored
Merge pull request #2 from crup/feature-final-release-pipeline
ci(release): add stable release pipeline
2 parents b29681d + 33a67dc commit d405e7f

4 files changed

Lines changed: 222 additions & 26 deletions

File tree

.github/workflows/release.yml

Lines changed: 171 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,65 +4,213 @@ on:
44
workflow_dispatch:
55
inputs:
66
confirm_stable_release:
7-
description: Type "publish-stable" to publish the latest dist-tag.
7+
description: Type "publish-stable" to publish to npm latest.
88
required: true
99
default: alpha-only
10+
stable_version:
11+
description: Stable version to publish. First stable should be 0.0.1.
12+
required: true
13+
default: 0.0.1
1014

1115
permissions:
1216
contents: write
13-
pull-requests: write
1417
id-token: write
1518

1619
env:
1720
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
1821

1922
concurrency:
20-
group: release
23+
group: stable-release
2124
cancel-in-progress: false
2225

2326
jobs:
24-
release:
25-
if: github.ref_name == 'main' && inputs.confirm_stable_release == 'publish-stable'
27+
guard:
28+
name: 1. Release guard
29+
runs-on: ubuntu-latest
30+
outputs:
31+
stable_version: ${{ steps.guard.outputs.stable_version }}
32+
steps:
33+
- name: Validate release inputs
34+
id: guard
35+
run: |
36+
if [ "${{ github.ref_name }}" != "main" ]; then
37+
echo "Stable releases must run from main. Current ref: ${{ github.ref_name }}" >&2
38+
exit 1
39+
fi
40+
41+
if [ "${{ inputs.confirm_stable_release }}" != "publish-stable" ]; then
42+
echo "Stable release is locked. Use confirm_stable_release=publish-stable to publish." >&2
43+
exit 1
44+
fi
45+
46+
if ! printf '%s' "${{ inputs.stable_version }}" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
47+
echo "stable_version must be a stable semver version like 0.0.1." >&2
48+
exit 1
49+
fi
50+
51+
echo "stable_version=${{ inputs.stable_version }}" >> "$GITHUB_OUTPUT"
52+
53+
verify:
54+
name: 2. Verify source
55+
needs: guard
2656
runs-on: ubuntu-latest
2757
steps:
28-
- name: Checkout
58+
- name: Checkout main
2959
uses: actions/checkout@v4
3060

3161
- name: Setup pnpm
3262
uses: pnpm/action-setup@v4
3363
with:
3464
version: 10.30.2
3565

36-
- name: Setup Node
66+
- name: Setup Node 24
3767
uses: actions/setup-node@v4
3868
with:
3969
node-version: 24
4070
cache: pnpm
41-
registry-url: https://registry.npmjs.org
4271

43-
- name: Install
72+
- name: Install dependencies
73+
id: install
4474
run: pnpm install --frozen-lockfile
4575

46-
- name: Verify
76+
- name: Typecheck
77+
id: typecheck
78+
run: pnpm typecheck
79+
80+
- name: Tests
81+
id: test
82+
run: pnpm test
83+
84+
- name: Build package
85+
id: build
86+
run: pnpm build
87+
88+
- name: Build docs
89+
id: docs
90+
run: pnpm docs:build
91+
92+
- name: README check
93+
id: readme
94+
run: pnpm readme:check
95+
96+
- name: Size report
97+
id: size
98+
run: pnpm size
99+
100+
- name: Pack dry run
101+
id: pack
102+
run: pnpm pack --dry-run
103+
104+
- name: Verify summary
105+
if: always()
47106
run: |
48-
pnpm typecheck
49-
pnpm test
50-
pnpm build
107+
{
108+
echo "## Stable release verify"
109+
echo
110+
echo "| Stage | Result |"
111+
echo "| --- | --- |"
112+
echo "| Install | ${{ steps.install.outcome }} |"
113+
echo "| Typecheck | ${{ steps.typecheck.outcome }} |"
114+
echo "| Tests | ${{ steps.test.outcome }} |"
115+
echo "| Package build | ${{ steps.build.outcome }} |"
116+
echo "| Docs build | ${{ steps.docs.outcome }} |"
117+
echo "| README check | ${{ steps.readme.outcome }} |"
118+
echo "| Size report | ${{ steps.size.outcome }} |"
119+
echo "| Pack dry run | ${{ steps.pack.outcome }} |"
120+
} >> "$GITHUB_STEP_SUMMARY"
121+
122+
publish:
123+
name: 3. Publish npm latest
124+
needs: [guard, verify]
125+
runs-on: ubuntu-latest
126+
outputs:
127+
package_version: ${{ steps.version.outputs.package_version }}
128+
steps:
129+
- name: Checkout main
130+
uses: actions/checkout@v4
131+
132+
- name: Setup pnpm
133+
uses: pnpm/action-setup@v4
134+
with:
135+
version: 10.30.2
51136

52-
- name: Create release PR or publish
53-
uses: changesets/action@v1
137+
- name: Setup Node 24
138+
uses: actions/setup-node@v4
54139
with:
55-
publish: pnpm release
140+
node-version: 24
141+
cache: pnpm
142+
registry-url: https://registry.npmjs.org
143+
144+
- name: Install dependencies
145+
run: pnpm install --frozen-lockfile
146+
147+
- name: Set stable package version
148+
id: version
149+
env:
150+
STABLE_VERSION: ${{ needs.guard.outputs.stable_version }}
151+
run: |
152+
node -e "const fs = require('node:fs'); const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); pkg.version = process.env.STABLE_VERSION; pkg.publishConfig = { access: 'public' }; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');"
153+
PACKAGE_NAME=$(node -p "require('./package.json').name")
154+
PACKAGE_VERSION=$(node -p "require('./package.json').version")
155+
echo "package_version=$PACKAGE_NAME@$PACKAGE_VERSION" >> "$GITHUB_OUTPUT"
156+
157+
- name: Fail if version already exists
158+
env:
159+
PACKAGE_NAME: ${{ steps.version.outputs.package_version }}
160+
run: |
161+
if npm view "$PACKAGE_NAME" version >/dev/null 2>&1; then
162+
echo "$PACKAGE_NAME is already published." >&2
163+
exit 1
164+
fi
165+
166+
- name: Build final package
167+
run: pnpm build
168+
169+
- name: Publish latest
170+
run: pnpm publish --tag latest --access public --no-git-checks
56171
env:
57-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
58172
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
173+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
59174

60-
stable-release-locked:
61-
if: inputs.confirm_stable_release != 'publish-stable'
175+
github-release:
176+
name: 4. Create GitHub release
177+
needs: [guard, publish]
62178
runs-on: ubuntu-latest
63179
steps:
64-
- name: Stable release is locked
180+
- name: Checkout main
181+
uses: actions/checkout@v4
182+
183+
- name: Create GitHub release
184+
env:
185+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
186+
STABLE_VERSION: ${{ needs.guard.outputs.stable_version }}
187+
PACKAGE_VERSION: ${{ needs.publish.outputs.package_version }}
188+
run: |
189+
cat > release-notes.md <<EOF
190+
Stable npm release for $PACKAGE_VERSION.
191+
192+
Install:
193+
194+
\`\`\`sh
195+
npm install @crup/react-timer-hook@$STABLE_VERSION
196+
\`\`\`
197+
198+
Docs: https://crup.github.io/react-timer-hook/
199+
EOF
200+
201+
gh release create "v$STABLE_VERSION" \
202+
--title "v$STABLE_VERSION" \
203+
--notes-file release-notes.md \
204+
--target "${{ github.sha }}"
205+
206+
- name: Release summary
65207
run: |
66-
echo "Stable npm publishing is disabled by default."
67-
echo "Use the Prerelease workflow for alpha releases."
68-
echo "To intentionally publish latest, rerun this workflow with confirm_stable_release=publish-stable on main."
208+
{
209+
echo "## Stable release published"
210+
echo
211+
echo "| Field | Value |"
212+
echo "| --- | --- |"
213+
echo "| Package | ${{ needs.publish.outputs.package_version }} |"
214+
echo "| Dist tag | latest |"
215+
echo "| GitHub release | v${{ needs.guard.outputs.stable_version }} |"
216+
} >> "$GITHUB_STEP_SUMMARY"

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,5 +297,6 @@ Issues, recipes, docs improvements, and focused bug reports are welcome.
297297
- Read the docs: https://crup.github.io/react-timer-hook/
298298
- Open an issue: https://github.com/crup/react-timer-hook/issues
299299
- See the contributing guide: ./CONTRIBUTING.md
300+
- Release policy: https://crup.github.io/react-timer-hook/project/release-channels/
300301

301302
The package targets Node 18+ and React 18+.
Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
---
22
title: Release channels
3-
description: Alpha-only release policy before the first stable version.
3+
description: Alpha and stable release pipeline policy.
44
---
55

66
# Release channels
77

8-
The package is alpha-only until stable publishing is explicitly unlocked.
8+
The package publishes alpha builds from `next` and stable builds from `main`.
99

1010
```sh
1111
npm install @crup/react-timer-hook@alpha
@@ -14,6 +14,31 @@ npm install @crup/react-timer-hook@alpha
1414
- `Prerelease` publishes an `0.0.1-alpha.x` version from `next`.
1515
- `Prerelease` updates the `alpha` dist-tag.
1616
- Npm requires a `latest` dist-tag, so the workflow keeps `latest` pointing at the current alpha until stable publishing is unlocked.
17+
- `Release` only runs from `main`.
1718
- `Release` is manually gated and requires `confirm_stable_release=publish-stable`.
19+
- The first stable version should be `0.0.1` to match the existing `0.0.1-alpha.x` prerelease line.
1820

19-
Consumers should use `@alpha` until the release policy changes.
21+
## Stable release stages
22+
23+
The stable release workflow is intentionally split into visible jobs:
24+
25+
| Stage | What it gates |
26+
| --- | --- |
27+
| Release guard | Confirms the workflow is running on `main`, the stable confirmation was typed, and the version is stable semver. |
28+
| Verify source | Runs typecheck, tests, package build, docs build, README check, size report, and pack dry run. |
29+
| Publish npm latest | Sets the stable version, blocks duplicate publishes, builds the final package, and publishes to npm `latest`. |
30+
| Create GitHub release | Creates a `vX.Y.Z` GitHub release for the exact `main` commit. |
31+
32+
Recommended flow:
33+
34+
1. Merge feature work into `next`.
35+
2. Test alpha from `next`.
36+
3. Open and merge `next` into `main`.
37+
4. Run `Release` manually on `main` with:
38+
39+
```txt
40+
confirm_stable_release=publish-stable
41+
stable_version=0.0.1
42+
```
43+
44+
Consumers should use `@alpha` until the stable workflow has published `latest`.

package.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
"name": "@crup/react-timer-hook",
33
"version": "0.0.0",
44
"description": "A lightweight React hooks library for building timers, stopwatches, and real-time clocks with minimal boilerplate.",
5+
"homepage": "https://crup.github.io/react-timer-hook/",
6+
"repository": {
7+
"type": "git",
8+
"url": "git+https://github.com/crup/react-timer-hook.git"
9+
},
10+
"bugs": {
11+
"url": "https://github.com/crup/react-timer-hook/issues"
12+
},
513
"main": "./dist/index.cjs",
614
"module": "./dist/index.js",
715
"types": "./dist/index.d.ts",
@@ -65,11 +73,25 @@
6573
"keywords": [
6674
"react",
6775
"hook",
76+
"hooks",
6877
"timer",
78+
"react-timer",
79+
"react-timer-hook",
80+
"timer-hook",
6981
"stopwatch",
82+
"react-stopwatch",
83+
"stopwatch-hook",
7084
"time",
7185
"countdown",
86+
"countdown-timer",
87+
"react-countdown",
88+
"clock",
89+
"real-time",
7290
"scheduler",
91+
"polling",
92+
"duration",
93+
"timer-group",
94+
"typescript",
7395
"react-hooks"
7496
],
7597
"author": "Rajender Joshi <connect@rajender.pro>",

0 commit comments

Comments
 (0)