Skip to content

Commit ecf25cf

Browse files
authored
ci: cut releases from a workflow that runs release.sh (#336)
Co-authored-by: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com>
1 parent fc2274e commit ecf25cf

3 files changed

Lines changed: 186 additions & 10 deletions

File tree

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

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# Cut Release — run scripts/release.sh from CI.
2+
#
3+
# Three steps, one workflow:
4+
#
5+
# 1. Dispatch with an explicit version:
6+
# gh workflow run cut-release.yml -f version=0.40.0
7+
# The `prepare` job runs `scripts/release.sh prepare <version>`: skill
8+
# check, release/<version> branch, cargo-release bump (no publish, no
9+
# tag), changelog validation, pushes the branch and opens the release PR.
10+
# The job then enables auto-merge (squash) on that PR.
11+
# 2. The release PR merges on its own once CI and the review gate pass.
12+
# 3. The merge fires this workflow again (pull_request closed, head
13+
# release/*). The `publish` job runs `scripts/release.sh finish` on main:
14+
# skill check with --require-version, annotated v<version> tag, push.
15+
# The tag push triggers release.yml (cargo-dist), which builds, publishes
16+
# the GitHub Release and the Homebrew formula.
17+
#
18+
# Both jobs act as the hotdata-automation GitHub App rather than GITHUB_TOKEN:
19+
# a PR opened or a tag pushed with GITHUB_TOKEN triggers no other workflows,
20+
# so CI would never run on the release PR and release.yml would never see the
21+
# tag.
22+
#
23+
# The skill-drift check inside `prepare` is not bypassed here (no
24+
# SKIP_SKILL_DRIFT). If the job fails on it, update skills/ on main first and
25+
# dispatch again.
26+
27+
name: Cut Release
28+
29+
on:
30+
workflow_dispatch:
31+
inputs:
32+
version:
33+
description: 'Version to release, e.g. 0.40.0'
34+
required: true
35+
type: string
36+
pull_request:
37+
types: [closed]
38+
branches: [main]
39+
40+
permissions: {}
41+
42+
concurrency:
43+
group: cut-release
44+
cancel-in-progress: false
45+
46+
jobs:
47+
prepare:
48+
name: Open release PR
49+
if: github.event_name == 'workflow_dispatch'
50+
runs-on: ubuntu-latest
51+
steps:
52+
- name: Generate GitHub App token
53+
id: app-token
54+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
55+
with:
56+
app-id: ${{ secrets.HOTDATA_AUTOMATION_APP_ID }}
57+
private-key: ${{ secrets.HOTDATA_AUTOMATION_PRIVATE_KEY }}
58+
owner: hotdata-dev
59+
60+
# Full history and tags: check-skills.sh diffs against the last v* tag,
61+
# and cargo-release pushes the release branch with the app token.
62+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
63+
with:
64+
token: ${{ steps.app-token.outputs.token }}
65+
persist-credentials: true
66+
fetch-depth: 0
67+
fetch-tags: true
68+
69+
- name: Install Rust
70+
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
71+
72+
- name: Cache cargo
73+
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
74+
with:
75+
path: |
76+
~/.cargo/registry
77+
~/.cargo/git
78+
target
79+
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
80+
restore-keys: |
81+
${{ runner.os }}-cargo-
82+
83+
# cargo-release does the bump; git-cliff is its pre-release-hook
84+
# (Cargo.toml [package.metadata.release]) and writes CHANGELOG.md.
85+
- name: Install cargo-release and git-cliff
86+
uses: taiki-e/install-action@9afc45b9c8ff2049b2e4e34df3db97754711d5c1 # cargo-llvm-cov tag, same pin as ci.yml
87+
with:
88+
tool: cargo-release,git-cliff
89+
90+
- name: Configure git identity
91+
env:
92+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
93+
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
94+
run: |
95+
slug="${APP_SLUG}[bot]"
96+
id="$(gh api "/users/${slug}" --jq .id)"
97+
git config user.name "$slug"
98+
git config user.email "${id}+${slug}@users.noreply.github.com"
99+
100+
- name: Prepare release
101+
env:
102+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
103+
VERSION: ${{ inputs.version }}
104+
run: scripts/release.sh prepare "$VERSION"
105+
106+
- name: Enable auto-merge on the release PR
107+
env:
108+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
109+
run: |
110+
branch="$(git rev-parse --abbrev-ref HEAD)"
111+
number="$(gh pr list --head "$branch" --state open --json number --jq '.[0].number')"
112+
[[ -n "$number" ]] || { echo "::error::no open PR for $branch"; exit 1; }
113+
gh pr merge "$number" --squash --auto
114+
115+
publish:
116+
name: Tag release
117+
if: >-
118+
github.event_name == 'pull_request'
119+
&& github.event.pull_request.merged == true
120+
&& startsWith(github.event.pull_request.head.ref, 'release/')
121+
runs-on: ubuntu-latest
122+
steps:
123+
- name: Generate GitHub App token
124+
id: app-token
125+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
126+
with:
127+
app-id: ${{ secrets.HOTDATA_AUTOMATION_APP_ID }}
128+
private-key: ${{ secrets.HOTDATA_AUTOMATION_PRIVATE_KEY }}
129+
owner: hotdata-dev
130+
131+
# finish expects to be on main and pushes the tag with the app token.
132+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
133+
with:
134+
ref: main
135+
token: ${{ steps.app-token.outputs.token }}
136+
persist-credentials: true
137+
fetch-depth: 0
138+
fetch-tags: true
139+
140+
- name: Install Rust
141+
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
142+
143+
- name: Cache cargo
144+
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
145+
with:
146+
path: |
147+
~/.cargo/registry
148+
~/.cargo/git
149+
target
150+
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
151+
restore-keys: |
152+
${{ runner.os }}-cargo-
153+
154+
- name: Configure git identity
155+
env:
156+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
157+
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
158+
run: |
159+
slug="${APP_SLUG}[bot]"
160+
id="$(gh api "/users/${slug}" --jq .id)"
161+
git config user.name "$slug"
162+
git config user.email "${id}+${slug}@users.noreply.github.com"
163+
164+
- name: Tag and push
165+
env:
166+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
167+
run: scripts/release.sh finish

‎docs/RELEASING.md‎

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,38 @@
11
# Releasing
22

3-
Releases use a two-phase workflow wrapping [`cargo-release`](https://github.com/crate-ci/cargo-release).
3+
Releases run in CI. The `Cut Release` workflow (`.github/workflows/cut-release.yml`) calls the same two-phase script, `scripts/release.sh`, that wraps [`cargo-release`](https://github.com/crate-ci/cargo-release).
44

5-
**Phase 1 — prepare**
5+
## Normal path
66

77
```sh
8-
scripts/release.sh prepare <version>
8+
gh workflow run cut-release.yml -f version=0.40.0
99
```
1010

11-
Runs the skill check (below), then creates a `release/<version>` branch, bumps the version (including the `version:` line in every `SKILL.md`), updates `CHANGELOG.md`, pushes the branch, and opens a pull request.
11+
1. **prepare** — the workflow runs `scripts/release.sh prepare 0.40.0`: skill check (below), `release/0.40.0` branch, version bump (including the `version:` line in every `SKILL.md`), `CHANGELOG.md` update, push, and a pull request. The job then enables auto-merge on the PR.
12+
2. **merge** — the PR squash-merges on its own once CI and the review gate pass.
13+
3. **finish** — the merge triggers the workflow again, which runs `scripts/release.sh finish` on `main`: skill check with `--require-version`, annotated `v0.40.0` tag, push. The tag triggers the dist workflow (`release.yml`), which builds the binaries, publishes the GitHub Release, and updates the Homebrew formula.
1214

13-
**Phase 2 — finish**
15+
The workflow acts as the `hotdata-automation` GitHub App. A PR opened or a tag pushed with the default `GITHUB_TOKEN` would trigger no other workflows.
16+
17+
If the `prepare` job fails on the skill check, the skills lag the code: update `skills/` on `main` first (see below), then dispatch again. CI does not set `SKIP_SKILL_DRIFT=1`.
18+
19+
## Manual path
20+
21+
The script still works locally with the same two phases:
1422

1523
```sh
16-
scripts/release.sh finish
24+
scripts/release.sh prepare <version> # branch, bump, changelog, PR
25+
scripts/release.sh finish # on main after the PR merges: tag and push
1726
```
1827

19-
Switches to `main`, pulls latest, runs the skill check with `--require-version`, tags the release, and triggers the dist workflow.
28+
If you run `prepare` locally, the merge of its `release/<version>` PR still triggers the workflow's `finish` job, so do not run `finish` by hand as well.
2029

2130
## Skill check
2231

2332
Both phases run `scripts/check-skills.sh`. The agent skills under `skills/` ship in every release (`skills.tar.gz`, `SKILL.md`), so the script refuses to release when they lag the code:
2433

25-
- **Drift** — if `src/` or `README.md` changed since the last `v*` tag and `skills/` did not, the run fails and lists the commits to review. Update the skills and commit, or set `SKIP_SKILL_DRIFT=1` when the release is verified to be skill-neutral.
34+
- **Drift** — if `src/` or `README.md` changed since the last `v*` tag and `skills/` did not, the run fails and lists the commits to review. Update the skills and commit, or set `SKIP_SKILL_DRIFT=1` (local runs only) when the release is verified to be skill-neutral.
2635
- **Coverage** — every subcommand the built binary exposes must appear as `hotdata <group> <sub>` somewhere in `skills/**/*.md`, and every long flag in its `--help` must be mentioned too. CI runs this check on every pull request (with the drift check skipped), so a new command or flag cannot merge undocumented.
2736
- **Version** (`finish` only) — every `SKILL.md` frontmatter `version:` must equal the crate version.
2837

29-
The drift check is a reminder, not a judge of prose. Before `prepare`, read the commits since the last tag and update `skills/hotdata/SKILL.md` and the subskills to match the current `--help` output and behavior.
38+
The drift check is a reminder, not a judge of prose. Before cutting a release, read the commits since the last tag and update `skills/hotdata/SKILL.md` and the subskills to match the current `--help` output and behavior.

‎scripts/release.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ case "$COMMAND" in
8181
echo ""
8282
echo "Next steps:"
8383
echo " 1. Review and merge the PR (use 'Squash and merge')"
84-
echo " 2. Run: scripts/release.sh finish"
84+
echo " 2. On merge, CI (cut-release.yml) runs 'scripts/release.sh finish' to tag"
8585
;;
8686

8787
finish)

0 commit comments

Comments
 (0)