Skip to content

Commit 3dc4151

Browse files
committed
ci: add release
1 parent f92a782 commit 3dc4151

2 files changed

Lines changed: 349 additions & 1 deletion

File tree

.github/workflows/release.yml

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
name: Release
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
package:
7+
description: Language package to release
8+
required: true
9+
type: choice
10+
options:
11+
- js
12+
- python
13+
- go
14+
version:
15+
description: Stable SemVer version without a leading v, for example 0.3.1
16+
required: true
17+
type: string
18+
dry_run:
19+
description: Validate release without publishing or creating a tag
20+
required: true
21+
type: boolean
22+
default: true
23+
24+
permissions:
25+
contents: read
26+
27+
concurrency:
28+
group: release-${{ inputs.package }}-${{ inputs.version }}
29+
cancel-in-progress: false
30+
31+
jobs:
32+
validate:
33+
runs-on: ubuntu-latest
34+
outputs:
35+
tag: ${{ steps.meta.outputs.tag }}
36+
title: ${{ steps.meta.outputs.title }}
37+
steps:
38+
- uses: actions/checkout@v4
39+
with:
40+
fetch-depth: 0
41+
42+
- name: Validate release inputs
43+
id: meta
44+
env:
45+
PACKAGE: ${{ inputs.package }}
46+
VERSION: ${{ inputs.version }}
47+
DRY_RUN: ${{ inputs.dry_run }}
48+
run: |
49+
set -euo pipefail
50+
51+
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
52+
echo "Version must be a stable SemVer value like 0.3.1"
53+
exit 1
54+
fi
55+
56+
if [[ "$DRY_RUN" != "true" && "$GITHUB_REF" != "refs/heads/main" && "$GITHUB_REF" != "refs/heads/master" ]]; then
57+
echo "Real releases must be run from main or master"
58+
exit 1
59+
fi
60+
61+
case "$PACKAGE" in
62+
js) TAG="js/v$VERSION" ;;
63+
python) TAG="python/v$VERSION" ;;
64+
go) TAG="go/v$VERSION" ;;
65+
*)
66+
echo "Unknown package: $PACKAGE"
67+
exit 1
68+
;;
69+
esac
70+
71+
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
72+
echo "Tag already exists: $TAG"
73+
exit 1
74+
fi
75+
76+
{
77+
echo "tag=$TAG"
78+
echo "title=$PACKAGE v$VERSION"
79+
} >> "$GITHUB_OUTPUT"
80+
81+
js-check:
82+
needs: validate
83+
if: ${{ inputs.package == 'js' }}
84+
runs-on: ubuntu-latest
85+
steps:
86+
- uses: actions/checkout@v4
87+
with:
88+
fetch-depth: 0
89+
90+
- uses: pnpm/action-setup@v4
91+
with:
92+
version: 8
93+
94+
- uses: actions/setup-node@v4
95+
with:
96+
node-version: 24
97+
cache: pnpm
98+
99+
- run: pnpm install --frozen-lockfile
100+
101+
- name: Check JS package version
102+
env:
103+
VERSION: ${{ inputs.version }}
104+
run: |
105+
node -e "const pkg = require('./js/package.json'); if (pkg.version !== process.env.VERSION) { throw new Error(`js/package.json version ${pkg.version} does not match ${process.env.VERSION}`); }"
106+
107+
- run: pnpm run verify:fixtures
108+
- run: pnpm -C js test
109+
- run: pnpm -C js build
110+
- run: npm pack --dry-run
111+
working-directory: js
112+
113+
js-publish:
114+
needs:
115+
- validate
116+
- js-check
117+
if: ${{ inputs.package == 'js' && inputs.dry_run == false }}
118+
runs-on: ubuntu-latest
119+
environment: npm
120+
permissions:
121+
contents: write
122+
id-token: write
123+
steps:
124+
- uses: actions/checkout@v4
125+
with:
126+
fetch-depth: 0
127+
128+
- uses: pnpm/action-setup@v4
129+
with:
130+
version: 8
131+
132+
- uses: actions/setup-node@v4
133+
with:
134+
node-version: 24
135+
registry-url: https://registry.npmjs.org
136+
137+
- run: pnpm install --frozen-lockfile
138+
- run: pnpm -C js build
139+
- run: npm publish --access public
140+
working-directory: js
141+
142+
- name: Create GitHub Release
143+
env:
144+
GH_TOKEN: ${{ github.token }}
145+
TAG: ${{ needs.validate.outputs.tag }}
146+
TITLE: ${{ needs.validate.outputs.title }}
147+
run: |
148+
gh release create "$TAG" --target "$GITHUB_SHA" --title "$TITLE" --notes "Published $TITLE to npm."
149+
150+
python-check:
151+
needs: validate
152+
if: ${{ inputs.package == 'python' }}
153+
runs-on: ubuntu-latest
154+
steps:
155+
- uses: actions/checkout@v4
156+
with:
157+
fetch-depth: 0
158+
159+
- uses: pnpm/action-setup@v4
160+
with:
161+
version: 8
162+
163+
- uses: actions/setup-node@v4
164+
with:
165+
node-version: 24
166+
cache: pnpm
167+
168+
- uses: actions/setup-python@v5
169+
with:
170+
python-version: "3.12"
171+
172+
- run: pnpm install --frozen-lockfile
173+
- run: pnpm run verify:fixtures
174+
175+
- name: Check Python package version
176+
env:
177+
VERSION: ${{ inputs.version }}
178+
run: |
179+
python - <<'PY'
180+
import os
181+
import pathlib
182+
import tomllib
183+
184+
pyproject = tomllib.loads(pathlib.Path("python/pyproject.toml").read_text())
185+
actual = pyproject["project"]["version"]
186+
expected = os.environ["VERSION"]
187+
if actual != expected:
188+
raise SystemExit(f"python/pyproject.toml version {actual} does not match {expected}")
189+
PY
190+
191+
- run: python -m pip install -e 'python[dev]'
192+
- run: python -m pytest python/tests
193+
- run: python -m ruff check python/src python/tests
194+
- run: python -m build python
195+
- run: python -m twine check python/dist/*
196+
197+
- uses: actions/upload-artifact@v4
198+
with:
199+
name: python-dist
200+
path: python/dist/*
201+
if-no-files-found: error
202+
203+
python-publish:
204+
needs:
205+
- validate
206+
- python-check
207+
if: ${{ inputs.package == 'python' && inputs.dry_run == false }}
208+
runs-on: ubuntu-latest
209+
environment: pypi
210+
permissions:
211+
contents: write
212+
id-token: write
213+
steps:
214+
- uses: actions/checkout@v4
215+
with:
216+
fetch-depth: 0
217+
218+
- uses: actions/download-artifact@v4
219+
with:
220+
name: python-dist
221+
path: python/dist
222+
223+
- name: Publish package distributions to PyPI
224+
uses: pypa/gh-action-pypi-publish@release/v1
225+
with:
226+
packages-dir: python/dist
227+
228+
- name: Create GitHub Release
229+
env:
230+
GH_TOKEN: ${{ github.token }}
231+
TAG: ${{ needs.validate.outputs.tag }}
232+
TITLE: ${{ needs.validate.outputs.title }}
233+
run: |
234+
gh release create "$TAG" --target "$GITHUB_SHA" --title "$TITLE" --notes "Published $TITLE to PyPI."
235+
236+
go-check:
237+
needs: validate
238+
if: ${{ inputs.package == 'go' }}
239+
runs-on: ubuntu-latest
240+
steps:
241+
- uses: actions/checkout@v4
242+
with:
243+
fetch-depth: 0
244+
245+
- uses: pnpm/action-setup@v4
246+
with:
247+
version: 8
248+
249+
- uses: actions/setup-node@v4
250+
with:
251+
node-version: 24
252+
cache: pnpm
253+
254+
- uses: actions/setup-go@v5
255+
with:
256+
go-version: "1.22"
257+
cache-dependency-path: go/go.mod
258+
259+
- run: pnpm install --frozen-lockfile
260+
- run: pnpm run verify:fixtures
261+
262+
- run: go test ./...
263+
working-directory: go
264+
- run: go vet ./...
265+
working-directory: go
266+
- run: go mod tidy
267+
working-directory: go
268+
- run: git diff --exit-code -- go/go.mod go/go.sum
269+
- run: test -z "$(git status --porcelain -- go/go.mod go/go.sum)"
270+
271+
go-release:
272+
needs:
273+
- validate
274+
- go-check
275+
if: ${{ inputs.package == 'go' && inputs.dry_run == false }}
276+
runs-on: ubuntu-latest
277+
environment: go-release
278+
permissions:
279+
contents: write
280+
steps:
281+
- uses: actions/checkout@v4
282+
with:
283+
fetch-depth: 0
284+
285+
- uses: actions/setup-go@v5
286+
with:
287+
go-version: "1.22"
288+
289+
- name: Push Go module tag
290+
env:
291+
TAG: ${{ needs.validate.outputs.tag }}
292+
run: |
293+
git tag "$TAG" "$GITHUB_SHA"
294+
git push origin "$TAG"
295+
296+
- name: Request Go module indexing
297+
env:
298+
VERSION: ${{ inputs.version }}
299+
run: |
300+
set -euo pipefail
301+
for attempt in {1..6}; do
302+
if GOPROXY=proxy.golang.org go list -m "github.com/algoux/standard-ranklist-utils/go@v$VERSION"; then
303+
exit 0
304+
fi
305+
echo "Go proxy has not indexed the module yet; retry $attempt/6"
306+
sleep 10
307+
done
308+
GOPROXY=proxy.golang.org go list -m "github.com/algoux/standard-ranklist-utils/go@v$VERSION"
309+
310+
- name: Create GitHub Release
311+
env:
312+
GH_TOKEN: ${{ github.token }}
313+
TAG: ${{ needs.validate.outputs.tag }}
314+
TITLE: ${{ needs.validate.outputs.title }}
315+
run: |
316+
gh release create "$TAG" --title "$TITLE" --notes "Published $TITLE as a Go module tag."

README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ pnpm run verify:fixtures
5757

5858
## Release Checks
5959

60+
The `Test` GitHub Actions workflow is CI only: it runs tests, builds, fixture checks, and packaging dry runs. Releases are
61+
handled by the manual `Release` workflow so each language package can publish independently.
62+
6063
```shell
6164
pnpm -C js build
6265
(cd js && npm pack --dry-run)
@@ -66,4 +69,33 @@ go -C go test ./...
6669
go -C go vet ./...
6770
```
6871

69-
For Go subdirectory releases, tag with the module prefix, for example `go/v0.3.0`.
72+
## Publishing
73+
74+
Run **Actions > Release** manually with:
75+
76+
- `package`: `js`, `python`, or `go`.
77+
- `version`: stable SemVer without `v`, for example `0.3.1`.
78+
- `dry_run`: keep `true` to validate only; set `false` to publish, tag, and create a GitHub Release.
79+
80+
Real releases are restricted to `main` or `master`. The workflow validates that the target tag does not already exist,
81+
runs the target package checks, and then publishes only the selected package.
82+
83+
Version sources and tags are independent:
84+
85+
- JS: update `js/package.json`; release tag `js/vX.Y.Z`.
86+
- Python: update `python/pyproject.toml`; release tag `python/vX.Y.Z`.
87+
- Go: no version in `go.mod`; release tag `go/vX.Y.Z`.
88+
89+
Configure registry publishing before setting `dry_run=false`:
90+
91+
- npm: configure Trusted Publishing for `@algoux/standard-ranklist-utils` with workflow `release.yml` and environment
92+
`npm`.
93+
- PyPI: configure Trusted Publisher for `algoux-standard-ranklist-utils` with workflow `release.yml` and environment
94+
`pypi`.
95+
- GitHub: create `npm`, `pypi`, and `go-release` environments, preferably with required reviewers.
96+
97+
The workflow uses OIDC / Trusted Publishing and does not require `NPM_TOKEN` or `PYPI_API_TOKEN`.
98+
99+
Registry setup references: [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers),
100+
[PyPI Trusted Publishers](https://docs.pypi.org/trusted-publishers/using-a-publisher/), and
101+
[Go modules](https://go.dev/ref/mod).

0 commit comments

Comments
 (0)