From 111dd10d01d3c2036e53b1dac4bb5212f6ae43e6 Mon Sep 17 00:00:00 2001 From: Eddie A Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:34:13 -0700 Subject: [PATCH] ci: cut releases from a workflow that runs release.sh --- .github/workflows/cut-release.yml | 167 ++++++++++++++++++++++++++++++ docs/RELEASING.md | 27 +++-- scripts/release.sh | 2 +- 3 files changed, 186 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/cut-release.yml diff --git a/.github/workflows/cut-release.yml b/.github/workflows/cut-release.yml new file mode 100644 index 0000000..1a185e8 --- /dev/null +++ b/.github/workflows/cut-release.yml @@ -0,0 +1,167 @@ +# Cut Release — run scripts/release.sh from CI. +# +# Three steps, one workflow: +# +# 1. Dispatch with an explicit version: +# gh workflow run cut-release.yml -f version=0.40.0 +# The `prepare` job runs `scripts/release.sh prepare `: skill +# check, release/ branch, cargo-release bump (no publish, no +# tag), changelog validation, pushes the branch and opens the release PR. +# The job then enables auto-merge (squash) on that PR. +# 2. The release PR merges on its own once CI and the review gate pass. +# 3. The merge fires this workflow again (pull_request closed, head +# release/*). The `publish` job runs `scripts/release.sh finish` on main: +# skill check with --require-version, annotated v tag, push. +# The tag push triggers release.yml (cargo-dist), which builds, publishes +# the GitHub Release and the Homebrew formula. +# +# Both jobs act as the hotdata-automation GitHub App rather than GITHUB_TOKEN: +# a PR opened or a tag pushed with GITHUB_TOKEN triggers no other workflows, +# so CI would never run on the release PR and release.yml would never see the +# tag. +# +# The skill-drift check inside `prepare` is not bypassed here (no +# SKIP_SKILL_DRIFT). If the job fails on it, update skills/ on main first and +# dispatch again. + +name: Cut Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to release, e.g. 0.40.0' + required: true + type: string + pull_request: + types: [closed] + branches: [main] + +permissions: {} + +concurrency: + group: cut-release + cancel-in-progress: false + +jobs: + prepare: + name: Open release PR + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: ${{ secrets.HOTDATA_AUTOMATION_APP_ID }} + private-key: ${{ secrets.HOTDATA_AUTOMATION_PRIVATE_KEY }} + owner: hotdata-dev + + # Full history and tags: check-skills.sh diffs against the last v* tag, + # and cargo-release pushes the release branch with the app token. + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ steps.app-token.outputs.token }} + persist-credentials: true + fetch-depth: 0 + fetch-tags: true + + - name: Install Rust + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + + - name: Cache cargo + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + # cargo-release does the bump; git-cliff is its pre-release-hook + # (Cargo.toml [package.metadata.release]) and writes CHANGELOG.md. + - name: Install cargo-release and git-cliff + uses: taiki-e/install-action@9afc45b9c8ff2049b2e4e34df3db97754711d5c1 # cargo-llvm-cov tag, same pin as ci.yml + with: + tool: cargo-release,git-cliff + + - name: Configure git identity + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + run: | + slug="${APP_SLUG}[bot]" + id="$(gh api "/users/${slug}" --jq .id)" + git config user.name "$slug" + git config user.email "${id}+${slug}@users.noreply.github.com" + + - name: Prepare release + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + VERSION: ${{ inputs.version }} + run: scripts/release.sh prepare "$VERSION" + + - name: Enable auto-merge on the release PR + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + branch="$(git rev-parse --abbrev-ref HEAD)" + number="$(gh pr list --head "$branch" --state open --json number --jq '.[0].number')" + [[ -n "$number" ]] || { echo "::error::no open PR for $branch"; exit 1; } + gh pr merge "$number" --squash --auto + + publish: + name: Tag release + if: >- + github.event_name == 'pull_request' + && github.event.pull_request.merged == true + && startsWith(github.event.pull_request.head.ref, 'release/') + runs-on: ubuntu-latest + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: ${{ secrets.HOTDATA_AUTOMATION_APP_ID }} + private-key: ${{ secrets.HOTDATA_AUTOMATION_PRIVATE_KEY }} + owner: hotdata-dev + + # finish expects to be on main and pushes the tag with the app token. + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + ref: main + token: ${{ steps.app-token.outputs.token }} + persist-credentials: true + fetch-depth: 0 + fetch-tags: true + + - name: Install Rust + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + + - name: Cache cargo + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Configure git identity + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + run: | + slug="${APP_SLUG}[bot]" + id="$(gh api "/users/${slug}" --jq .id)" + git config user.name "$slug" + git config user.email "${id}+${slug}@users.noreply.github.com" + + - name: Tag and push + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: scripts/release.sh finish diff --git a/docs/RELEASING.md b/docs/RELEASING.md index c6f2d03..3f0a34f 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -1,29 +1,38 @@ # Releasing -Releases use a two-phase workflow wrapping [`cargo-release`](https://github.com/crate-ci/cargo-release). +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). -**Phase 1 — prepare** +## Normal path ```sh -scripts/release.sh prepare +gh workflow run cut-release.yml -f version=0.40.0 ``` -Runs the skill check (below), then creates a `release/` branch, bumps the version (including the `version:` line in every `SKILL.md`), updates `CHANGELOG.md`, pushes the branch, and opens a pull request. +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. +2. **merge** — the PR squash-merges on its own once CI and the review gate pass. +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. -**Phase 2 — finish** +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. + +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`. + +## Manual path + +The script still works locally with the same two phases: ```sh -scripts/release.sh finish +scripts/release.sh prepare # branch, bump, changelog, PR +scripts/release.sh finish # on main after the PR merges: tag and push ``` -Switches to `main`, pulls latest, runs the skill check with `--require-version`, tags the release, and triggers the dist workflow. +If you run `prepare` locally, the merge of its `release/` PR still triggers the workflow's `finish` job, so do not run `finish` by hand as well. ## Skill check 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: -- **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. +- **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. - **Coverage** — every subcommand the built binary exposes must appear as `hotdata ` 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. - **Version** (`finish` only) — every `SKILL.md` frontmatter `version:` must equal the crate version. -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. +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. diff --git a/scripts/release.sh b/scripts/release.sh index cd90c5f..56aeeb3 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -81,7 +81,7 @@ case "$COMMAND" in echo "" echo "Next steps:" echo " 1. Review and merge the PR (use 'Squash and merge')" - echo " 2. Run: scripts/release.sh finish" + echo " 2. On merge, CI (cut-release.yml) runs 'scripts/release.sh finish' to tag" ;; finish)