Skip to content

Commit f1fd013

Browse files
authored
ci: cut releases from a workflow that runs release.sh (#217)
Trigger Cut Release with a bump kind; the prepare job runs release.sh prepare and enables auto-merge on the release PR, and merging that PR runs release.sh publish, whose tag push feeds the existing PyPI and GitHub Release workflows. Both jobs act as the automation app so the PR gets checks and the tag triggers publishing. Co-authored-by: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com>
1 parent e6f00ff commit f1fd013

3 files changed

Lines changed: 151 additions & 16 deletions

File tree

‎.github/workflows/cut-release.yml‎

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
name: Cut Release
2+
3+
# The whole release runs here; nothing is cut from a laptop.
4+
#
5+
# 1. Trigger this workflow (Actions tab, or
6+
# `gh workflow run cut-release.yml -f bump=minor`). The `prepare` job runs
7+
# the existing `scripts/release.sh prepare`, which bumps the version, dates
8+
# the [Unreleased] changelog section, pushes `release/vX.Y.Z` and opens the
9+
# release PR. Auto-merge is enabled, so the PR merges once branch
10+
# protection is satisfied (checks green, review approved).
11+
# 2. When that PR merges, the `publish` job runs `scripts/release.sh publish`,
12+
# which tags main `vX.Y.Z` and pushes the tag.
13+
# 3. The tag push triggers publish.yml (PyPI) and release.yml (GitHub Release),
14+
# exactly as before.
15+
#
16+
# Both jobs act as the Hotdata automation GitHub App rather than GITHUB_TOKEN:
17+
# a PR opened or a tag pushed with GITHUB_TOKEN triggers no other workflows, so
18+
# the release PR would run no checks and the tag would publish nothing.
19+
20+
on:
21+
workflow_dispatch:
22+
inputs:
23+
bump:
24+
description: 'patch | minor | major | explicit X.Y.Z'
25+
required: true
26+
default: patch
27+
type: string
28+
pull_request:
29+
types: [closed]
30+
branches: [main]
31+
32+
permissions: {}
33+
34+
concurrency:
35+
group: cut-release
36+
cancel-in-progress: false
37+
38+
jobs:
39+
prepare:
40+
name: Open release PR
41+
if: github.event_name == 'workflow_dispatch'
42+
runs-on: ubuntu-latest
43+
steps:
44+
- name: Generate GitHub App token
45+
id: app-token
46+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
47+
with:
48+
client-id: Iv23liKBX2RYMoZIYuKa
49+
private-key: ${{ secrets.HOTDATA_AUTOMATION_PRIVATE_KEY }}
50+
owner: hotdata-dev
51+
52+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
53+
with:
54+
token: ${{ steps.app-token.outputs.token }}
55+
fetch-depth: 0
56+
fetch-tags: true
57+
58+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
59+
with:
60+
python-version: '3.12'
61+
62+
# `release.sh prepare` refreshes uv.lock when uv is on PATH, so the lock
63+
# the PR carries matches the bumped version.
64+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
65+
66+
- name: Configure git identity
67+
env:
68+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
69+
run: |
70+
slug="${{ steps.app-token.outputs.app-slug }}[bot]"
71+
id="$(gh api "/users/${slug}" --jq .id)"
72+
git config user.name "$slug"
73+
git config user.email "${id}+${slug}@users.noreply.github.com"
74+
75+
- name: Prepare release
76+
env:
77+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
78+
run: ./scripts/release.sh prepare "${{ inputs.bump }}"
79+
80+
# Branch protection on main gates the merge on the checks and the
81+
# approving review, so this only merges once everything is green.
82+
- name: Enable auto-merge on the release PR
83+
env:
84+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
85+
run: |
86+
branch="$(git rev-parse --abbrev-ref HEAD)"
87+
number="$(gh pr list --head "$branch" --state open --json number --jq '.[0].number')"
88+
[[ -n "$number" ]] || { echo "::error::no open PR for $branch"; exit 1; }
89+
gh pr merge "$number" --squash --auto
90+
echo "Release PR #$number opened from $branch with auto-merge enabled."
91+
92+
publish:
93+
name: Tag release
94+
if: >-
95+
github.event_name == 'pull_request'
96+
&& github.event.pull_request.merged == true
97+
&& startsWith(github.event.pull_request.head.ref, 'release/v')
98+
runs-on: ubuntu-latest
99+
steps:
100+
- name: Generate GitHub App token
101+
id: app-token
102+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
103+
with:
104+
client-id: Iv23liKBX2RYMoZIYuKa
105+
private-key: ${{ secrets.HOTDATA_AUTOMATION_PRIVATE_KEY }}
106+
owner: hotdata-dev
107+
108+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
109+
with:
110+
ref: main
111+
token: ${{ steps.app-token.outputs.token }}
112+
fetch-depth: 0
113+
fetch-tags: true
114+
115+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
116+
with:
117+
python-version: '3.12'
118+
119+
- name: Configure git identity
120+
env:
121+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
122+
run: |
123+
slug="${{ steps.app-token.outputs.app-slug }}[bot]"
124+
id="$(gh api "/users/${slug}" --jq .id)"
125+
git config user.name "$slug"
126+
git config user.email "${id}+${slug}@users.noreply.github.com"
127+
128+
- name: Tag and push
129+
env:
130+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
131+
run: ./scripts/release.sh publish

‎RELEASING.md‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,28 @@ Every release uses `./scripts/release.sh`. Do not bump versions, tag, or create
99

1010
## Release steps
1111

12-
1. Add user-facing notes under `## [Unreleased]` in `CHANGELOG.md`.
13-
2. Prepare the release PR:
12+
Releases are cut in CI by the `Cut Release` workflow (`cut-release.yml`). It runs
13+
the same `./scripts/release.sh` a person would, as the Hotdata automation app.
14+
15+
1. Make sure `## [Unreleased]` in `CHANGELOG.md` has user-facing notes.
16+
2. Trigger the workflow with the bump kind or an explicit version:
1417

1518
```bash
16-
./scripts/release.sh prepare patch # or minor | major | 1.2.3
19+
gh workflow run cut-release.yml -f bump=minor # or patch | major | 1.2.3
1720
```
1821

19-
3. Merge the PR after CI passes (including the changelog check).
20-
4. Publish from a clean default branch checkout:
22+
The `prepare` job runs `release.sh prepare`, which opens the
23+
`release/vX.Y.Z` PR with auto-merge enabled.
24+
3. The PR merges on its own once CI and the review gate pass. Merging it runs
25+
the `publish` job, which runs `release.sh publish` and pushes the `vX.Y.Z`
26+
tag.
2127

22-
```bash
23-
git checkout main # or master for hotdata-marimo
24-
git pull
25-
./scripts/release.sh publish
26-
```
28+
That is the whole process. Running the script locally still works and does the
29+
same thing, but is not the normal path.
2730

2831
## What happens automatically
2932

30-
Pushing a `vX.Y.Z` tag triggers two workflows:
33+
Pushing the `vX.Y.Z` tag triggers two workflows:
3134

3235
| Workflow | Purpose |
3336
|----------|---------|

‎scripts/release.sh‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ need() { command -v "$1" >/dev/null 2>&1 || die "$1 is required"; }
1616
#
1717
# `python3` carries no version guarantee, and this package's own requires-python
1818
# is >=3.10 — so hardcoding it meant the release script could not run on the
19-
# oldest Python the package claims to support. No workflow runs this script; only
20-
# a human cutting a release does, which is why it went unnoticed for so long. The
19+
# oldest Python the package claims to support. Until cut-release.yml, no workflow
20+
# ran this script; only a human cutting a release did, which is why it went
21+
# unnoticed for so long. The
2122
# symptom was a bare ModuleNotFoundError, which reads as a broken checkout rather
2223
# than a too-old interpreter.
2324
#
2425
# `uv` is the fallback because `prepare` below already shells out to `uv lock`
2526
# when a uv.lock is present, so a machine able to cut a release here already has
26-
# it. (CI installs with pip, so this is about the local release path only.)
27+
# it. (cut-release.yml installs uv for the same reason.)
2728
resolve_python() {
2829
if command -v python3 >/dev/null 2>&1 && python3 -c "import tomllib" >/dev/null 2>&1; then
2930
echo python3
@@ -192,9 +193,9 @@ Release **${pkg} v${new}**.
192193
- [x] \`CHANGELOG.md\` updated
193194
- [ ] CI green
194195
195-
After merge, run \`./scripts/release.sh publish\` from a clean \`${base}\` checkout."
196+
Merging this PR tags the release in CI (cut-release.yml), which publishes to PyPI."
196197

197-
echo "Prepared ${pkg} v${new}. Merge the PR, then run: ./scripts/release.sh publish"
198+
echo "Prepared ${pkg} v${new}. Once the PR merges, CI tags and publishes it."
198199
}
199200

200201
cmd_publish() {

0 commit comments

Comments
 (0)