Skip to content

Commit 910b0b7

Browse files
alexkromanclaude
andauthored
Let release.yml be cut from a manual workflow_dispatch (#160)
Add a tag job to release.yml that, on manual dispatch, resolves the version and creates+pushes the vX.Y.Z tag (reusing cut_release.sh --no-push), then builds and publishes in the same run. This lets a Claude web session — which works on a feature branch and can't push tags — cut a release via the "Run workflow" button / actions_run_trigger. Tag creation lives inside the release run on purpose: a GITHUB_TOKEN tag push does not re-trigger the on:push half, so a standalone tag-push workflow would silently never build. https://claude.ai/code/session_01D6zsmQxdvq3dUPuVrbUaNg Co-authored-by: Claude <noreply@anthropic.com>
1 parent d4581e2 commit 910b0b7

4 files changed

Lines changed: 108 additions & 27 deletions

File tree

.claude/skills/release-prep/SKILL.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,40 @@
11
---
22
name: release-prep
3-
description: Prepare an assembly CLI release — bump the version, run the full gate, then tag to trigger the bottle pipeline. Use when cutting a new release.
3+
description: Prepare an assembly CLI release — confirm main is green, then tag (locally or via the manual workflow) to trigger the bottle pipeline. Use when cutting a new release.
44
disable-model-invocation: true
55
---
66

77
# release-prep
88

99
Drive an `assembly` release to a verified, tagged state. Stop and report at the first failure — never tag on a red check.
1010

11-
## 1. Version bump
11+
## 1. Pick the version
1212

13-
- Update `version` in `pyproject.toml` (`[project]`). Confirm `aai_cli/__init__.py` `__version__` stays in sync (the `version` command reads it).
13+
- With hatch-vcs **the git tag _is_ the version** — there is no `pyproject.toml` / `aai_cli/__init__.py` string to bump. `cut_release.sh` defaults to the next patch above the latest `vX.Y.Z` tag; pass `X.Y.Z` for a minor/major bump.
1414
- Decide the bump (patch/minor/major) from what changed since the last tag; ask the user if it's ambiguous.
15-
- Land the bump via a normal PR (regular CI) before tagging.
1615

1716
## 2. Full gate
1817

1918
```sh
2019
./scripts/check.sh
2120
```
2221

23-
Must end with `All checks passed.` (ruff, mypy, markdownlint, shellcheck, pytest+coverage, build, `twine check --strict`).
22+
Must end with `All checks passed.` (ruff, mypy, markdownlint, shellcheck, pytest+coverage, build, `twine check --strict`). The release builds whatever `main` points at, so confirm `main` is green before tagging.
2423

2524
## 3. Tag to trigger the bottle pipeline
2625

26+
Two equivalent ways to cut the tag — both land on `.github/workflows/release.yml`:
27+
28+
**Local** (from a clean `main` in sync with `origin/main`):
29+
2730
```sh
28-
./scripts/cut_release.sh
31+
./scripts/cut_release.sh # next patch; --dry-run verifies without tagging, --yes skips the prompt
32+
./scripts/cut_release.sh 0.3.0 # explicit version
2933
```
3034

31-
This derives the version from `pyproject.toml`, verifies the tree is clean, on `main`, and in sync with origin, then tags `vX.Y.Z` and pushes it. (`--dry-run` verifies without tagging; `--yes` skips the confirmation prompt.)
35+
**No local checkout** (e.g. a Claude web session on a feature branch): run the **Release** workflow's manual `workflow_dispatch` — GitHub's "Run workflow" button, or the `actions_run_trigger` MCP tool — with an optional `version` input (blank = next patch). Its `tag` job resolves the version and creates+pushes the tag from `main`, then the same run builds and publishes. Set `dry_run: true` to build the bottle for an existing tag without publishing.
3236

33-
The pushed tag triggers `.github/workflows/release.yml`, which:
37+
The tag triggers `.github/workflows/release.yml`, which:
3438

3539
1. Builds the arm64 macOS bottle (`arm64_sonoma`).
3640
2. Creates the `vX.Y.Z` GitHub Release with the bottle attached.

.github/workflows/release.yml

Lines changed: 82 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,100 @@
11
name: Release
22

3-
# Cut a release by pushing a vX.Y.Z tag (after the version-bump PR merges).
4-
# Builds the arm64 macOS bottle, publishes it to the tag's GitHub Release, and
5-
# opens a formula PR (url + sha256 + bottle block) for a maintainer to merge.
3+
# Cut a release one of two ways, both landing on the same bottle pipeline below:
4+
# 1. Push a vX.Y.Z tag (what `scripts/cut_release.sh` does from a clean `main`).
5+
# 2. Run this workflow manually ("Run workflow" / the actions_run_trigger MCP
6+
# tool) — the `tag` job resolves the version, creates the tag, and pushes it,
7+
# so a Claude web session (which works on a feature branch and can't push
8+
# tags itself) can cut a release without a local checkout.
9+
# Either way the pipeline builds the arm64 macOS bottle, publishes it to the tag's
10+
# GitHub Release, and opens a formula PR (url + sha256 + bottle block) to merge.
611
on:
712
push:
813
tags: ["v*"]
9-
# Manual dry-run: build the bottle for an existing tag WITHOUT publishing.
1014
workflow_dispatch:
1115
inputs:
12-
tag:
13-
description: "Existing tag to build a bottle for (dry-run; no publish)"
14-
required: true
16+
version:
17+
description: "Release version X.Y.Z (blank = next patch above the latest vX.Y.Z tag)"
18+
required: false
19+
default: ""
20+
dry_run:
21+
description: "Build the bottle only — don't create the tag, GitHub Release, or formula PR"
22+
type: boolean
23+
default: false
1524

1625
permissions:
1726
contents: read
1827

1928
concurrency:
20-
group: ${{ github.workflow }}-${{ github.event.inputs.tag || github.ref }}
29+
group: ${{ github.workflow }}-${{ github.event.inputs.version || github.ref }}
2130
cancel-in-progress: false
2231

2332
jobs:
33+
# Resolve the tag the rest of the pipeline builds. On a tag push it already
34+
# exists (just echo it). On a manual real release it's created here from the
35+
# same logic maintainers run locally; on a manual dry run we build an existing
36+
# tag without creating anything. Always runs so `bottle` has a single source
37+
# for the tag regardless of trigger.
38+
tag:
39+
name: resolve release tag
40+
runs-on: ubuntu-latest
41+
timeout-minutes: 10
42+
permissions:
43+
contents: write # only the manual real-release path pushes a tag; unused otherwise
44+
outputs:
45+
tag: ${{ steps.resolve.outputs.tag }}
46+
steps:
47+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
48+
with:
49+
# Release from main; full history brings the vX.Y.Z tags cut_release.sh
50+
# bumps from. persist-credentials off (the real-release push below uses
51+
# an explicit tokened remote, matching the publish job).
52+
ref: main
53+
fetch-depth: 0
54+
persist-credentials: false
55+
56+
- name: Resolve the release tag
57+
id: resolve
58+
env:
59+
EVENT_NAME: ${{ github.event_name }}
60+
REF_NAME: ${{ github.ref_name }}
61+
INPUT_VERSION: ${{ github.event.inputs.version }}
62+
DRY_RUN: ${{ github.event.inputs.dry_run }}
63+
GH_TOKEN: ${{ github.token }}
64+
REPO: ${{ github.repository }}
65+
run: |
66+
set -euo pipefail
67+
if [ "$EVENT_NAME" = "push" ]; then
68+
tag="$REF_NAME"
69+
elif [ "$DRY_RUN" = "true" ]; then
70+
# Build a bottle for an already-existing tag without publishing.
71+
if [ -n "$INPUT_VERSION" ]; then
72+
tag="v${INPUT_VERSION}"
73+
else
74+
tag="$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n1)"
75+
fi
76+
[ -n "$tag" ] || { echo "no existing vX.Y.Z tag to dry-run" >&2; exit 1; }
77+
else
78+
# Real manual release: resolve + validate + create the tag locally via
79+
# the same script maintainers run, then push it with an explicit
80+
# tokened remote (persist-credentials is off above).
81+
extra=()
82+
[ -n "$INPUT_VERSION" ] && extra+=("$INPUT_VERSION")
83+
./scripts/cut_release.sh --yes --no-push "${extra[@]}"
84+
tag="$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n1)"
85+
git push "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" "refs/tags/${tag}"
86+
fi
87+
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
88+
2489
bottle:
2590
name: build arm64 bottle (macOS)
91+
needs: [tag]
2692
runs-on: macos-14
2793
timeout-minutes: 40
2894
permissions:
2995
contents: read
3096
outputs:
31-
tag: ${{ steps.meta.outputs.tag }}
97+
tag: ${{ needs.tag.outputs.tag }}
3298
steps:
3399
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
34100
with:
@@ -37,28 +103,26 @@ jobs:
37103
# commit SHA like every other action here — Dependabot keeps it current.
38104
- uses: Homebrew/actions/setup-homebrew@2ebcf16054461267868620b1414507f3ccc765c1
39105

40-
- name: Resolve tag + source sha256
106+
- name: Resolve source sha256
41107
id: meta
42108
env:
43109
# Pass via env (not inline ${{ }}) to satisfy zizmor template-injection.
44-
INPUT_TAG: ${{ github.event.inputs.tag }}
45-
REF_NAME: ${{ github.ref_name }}
110+
TAG: ${{ needs.tag.outputs.tag }}
46111
REPO: ${{ github.repository }}
47112
run: |
48113
set -euo pipefail
49-
tag="${INPUT_TAG:-$REF_NAME}"
114+
tag="$TAG"
50115
url="https://github.com/${REPO}/archive/refs/tags/${tag}.tar.gz"
51116
curl -fL "$url" -o source.tar.gz
52117
sha="$(shasum -a 256 source.tar.gz | awk '{print $1}')"
53118
{
54-
echo "tag=${tag}"
55119
echo "source_sha=${sha}"
56120
echo "root_url=https://github.com/${REPO}/releases/download/${tag}"
57121
} >> "$GITHUB_OUTPUT"
58122
59123
- name: Pin the formula to the release tag
60124
env:
61-
TAG: ${{ steps.meta.outputs.tag }}
125+
TAG: ${{ needs.tag.outputs.tag }}
62126
SOURCE_SHA: ${{ steps.meta.outputs.source_sha }}
63127
REPO: ${{ github.repository }}
64128
run: |
@@ -114,7 +178,9 @@ jobs:
114178
publish:
115179
name: publish release + open formula PR
116180
needs: [bottle]
117-
if: github.event_name == 'push'
181+
# Publish for real tag pushes and for manual real releases; skip on a manual
182+
# dry run (which only builds the bottle to prove the formula installs).
183+
if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.dry_run != 'true' }}
118184
runs-on: ubuntu-latest
119185
timeout-minutes: 10
120186
permissions:

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ structured so independent changes stay in disjoint files. Keep it that way:
7575
- The **package/module** is `aai_cli`; the **distribution** name is `aai-cli`; the **console command** is `assembly` (`[project.scripts] assembly = "aai_cli.main:run"`).
7676
- `assembly init` templates live in `aai_cli/init/templates/` and are **committed**, including renamed dotfiles (`gitignore``.gitignore`, `env.example`). The wheel force-includes them via `[tool.hatch.build.targets.wheel] artifacts`, excluding `__pycache__/*.pyc`. Editing templates needs care — see the parametrized contract tests (`tests/test_init_template_*.py`).
7777
- `audioop` left the stdlib in 3.13; `audioop-lts` backfills it (conditional dependency). Supported Pythons: 3.12–3.13.
78-
- **Releasing is tag-triggered.** The version is **derived from the git tag** by hatch-vcs and written to a gitignored `aai_cli/_version.py` at build time — there is no version string to keep in sync across `pyproject.toml` or `aai_cli/__init__.py`, and `bump_patch.sh` no longer exists. To cut a release, run `scripts/cut_release.sh` from a clean `main` in sync with `origin/main`: no argument → next patch above the latest `vX.Y.Z` tag; `cut_release.sh X.Y.Z` → explicit version. It tags + pushes, which fires `.github/workflows/release.yml` — that builds the prebuilt arm64 Homebrew bottle (`Formula/assembly.rb`), cuts the GitHub Release, and opens the formula PR. Bottling matters because the deps include Rust-backed sdists (`pydantic-core`, `jiter`, `cryptography`) that would otherwise compile from source on `brew install`. The Homebrew formula builds from a git-less GitHub source tarball, so `Formula/assembly.rb`'s `def install` sets the generic `SETUPTOOLS_SCM_PRETEND_VERSION` env var (installing resources first under a clean env, then setting the var for our package only) to feed the tag version to the build. **`cut_release.sh` only runs from a clean `main` in sync with `origin/main`** (it hard-errors on a feature branch / dirty tree), so cut releases from `main`, not your working branch. The "update available" notice users see is `aai_cli/update_check.py`.
78+
- **Releasing is tag-triggered.** The version is **derived from the git tag** by hatch-vcs and written to a gitignored `aai_cli/_version.py` at build time — there is no version string to keep in sync across `pyproject.toml` or `aai_cli/__init__.py`, and `bump_patch.sh` no longer exists. To cut a release, run `scripts/cut_release.sh` from a clean `main` in sync with `origin/main`: no argument → next patch above the latest `vX.Y.Z` tag; `cut_release.sh X.Y.Z` → explicit version. It tags + pushes, which fires `.github/workflows/release.yml` — that builds the prebuilt arm64 Homebrew bottle (`Formula/assembly.rb`), cuts the GitHub Release, and opens the formula PR. **You don't need a local checkout to release:** `release.yml` also has a manual `workflow_dispatch` (GitHub's "Run workflow" button, or `actions_run_trigger` from a Claude web session) taking an optional `version` input — its `tag` job resolves the version and creates+pushes the tag (reusing `cut_release.sh --no-push`), and the rest of the pipeline then runs in that same workflow run. Tag creation lives *inside* the release run on purpose: a `GITHUB_TOKEN` tag push wouldn't re-trigger the `on: push` half, so a separate "push the tag" workflow would silently never build. (`dry_run: true` builds the bottle for an existing tag without publishing.) Bottling matters because the deps include Rust-backed sdists (`pydantic-core`, `jiter`, `cryptography`) that would otherwise compile from source on `brew install`. The Homebrew formula builds from a git-less GitHub source tarball, so `Formula/assembly.rb`'s `def install` sets the generic `SETUPTOOLS_SCM_PRETEND_VERSION` env var (installing resources first under a clean env, then setting the var for our package only) to feed the tag version to the build. **`cut_release.sh` only runs from a clean `main` in sync with `origin/main`** (it hard-errors on a feature branch / dirty tree), so cut releases from `main`, not your working branch. The "update available" notice users see is `aai_cli/update_check.py`.
7979

8080
## Manual QA / running the CLI in sandboxed sessions
8181

scripts/cut_release.sh

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
#!/bin/sh
22
# Cut an AssemblyAI CLI release: tag the version and push the tag, which triggers
33
# .github/workflows/release.yml (builds the arm64 bottle, creates the GitHub
4-
# Release, opens the formula PR).
4+
# Release, opens the formula PR). release.yml's manual "Run workflow" dispatch
5+
# runs these same steps in CI — so a Claude web session can cut a release with no
6+
# local checkout — by calling this script with --no-push and pushing the tag itself.
57
#
68
# With hatch-vcs the git tag IS the version — there is no version file to bump
79
# or version-bump PR to merge first. By default the script tags the next patch
@@ -11,16 +13,19 @@
1113
# ./scripts/cut_release.sh 0.2.0 # tag an explicit version instead
1214
# ./scripts/cut_release.sh --yes # skip the interactive confirmation
1315
# ./scripts/cut_release.sh -n # dry run: verify only, don't tag or push
16+
# ./scripts/cut_release.sh --no-push # create the tag locally, don't push it
1417
set -eu
1518

1619
ASSUME_YES=0
1720
DRY_RUN=0
21+
NO_PUSH=0
1822
for arg in "$@"; do
1923
case "$arg" in
2024
-y | --yes) ASSUME_YES=1 ;;
2125
-n | --dry-run) DRY_RUN=1 ;;
26+
--no-push) NO_PUSH=1 ;;
2227
-h | --help)
23-
sed -n '2,13p' "$0" | sed 's/^# \{0,1\}//'
28+
sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'
2429
exit 0
2530
;;
2631
[0-9]*.[0-9]*.[0-9]*) EXPLICIT_VERSION="$arg" ;;
@@ -107,6 +112,12 @@ if [ "$ASSUME_YES" -ne 1 ]; then
107112
fi
108113

109114
git tag -a "$tag" -m "Release ${tag}"
115+
116+
if [ "$NO_PUSH" -eq 1 ]; then
117+
info "Created tag ${tag} locally; --no-push set, leaving the push to the caller."
118+
exit 0
119+
fi
120+
110121
info "Created tag ${tag}. Pushing..."
111122
git push origin "$tag"
112123

0 commit comments

Comments
 (0)