diff --git a/CHANGELOG.md b/CHANGELOG.md index ec930af..8754545 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,17 @@ This project follows a Keep a Changelog-style format and uses version tags for r ## [Unreleased] +## [1.6.8] - 2026-08-09 + ### Changed +- **Cutting a release REQUIRES a GitHub Release (`gh release create`), not just + a git tag.** `scripts/tag_release.sh` now creates both — it pushes the tag and + then creates the GitHub Release with the notes from the matching + `CHANGELOG.md` section (falling back to generated notes if the section is + missing). A bare `git tag` + push never surfaces on `/releases` or as `Latest`, + so a hand-tag-only release cut is now an explicit rule violation — codified in + `CLAUDE.md` → Release Process, with a manual-recovery playbook for when the + `gh release create` step fails. - **% readouts and the fan spinner tint by utilization color.** The headline % in each metric row (and the fan-glyph) now wears the same severity color as the sparkline that traces it — a 90% reading reads red, an idle 5% stays cool. diff --git a/CLAUDE.md b/CLAUDE.md index 5eb32c5..1c9c9cf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,6 +49,8 @@ This file is the single source of truth for repository guidelines, used by Claud `main` is **PR-only** (branch protection + `.githooks/pre-commit` redaction check and `.githooks/pre-push` guard; run `git config core.hooksPath .githooks` once). The Homebrew formula lives in the separate tap repo `binlecode/homebrew-actop` (not this repo); CI syncs it on tag and publishes to PyPI via OIDC. CI/CD mechanics are owned by the workflow files themselves (`.github/workflows/main-ci.yml`, `release-formula.yml`, `publish-pypi.yml`) — each carries its one-time setup inline. +**RULE — cutting a release REQUIRES a GitHub Release, not just a tag.** A release is cut with `scripts/tag_release.sh`, which pushes the git tag **and** creates the GitHub Release for it via `gh release create` (release notes pulled from the matching `CHANGELOG.md` section). A bare `git tag` + push is **not** a release: it never surfaces on the `/releases` page or as `Latest`, and leaves the Releases history silently behind the code. Never cut a release by hand-tagging alone — if the `gh release create` step fails, finish it manually (see playbooks below) before calling the release cut done. + ### Release steps ```bash @@ -68,7 +70,7 @@ gh pr create --base main --fill gh pr merge --merge --delete-branch git checkout main && git pull --ff-only -# 4. Tag the release (after the bump PR merges) +# 4. Cut the release — tag + GitHub Release (RULE: tag alone is not a release) scripts/tag_release.sh "X.Y.Z" # 5. Monitor CI — wait for release-formula and publish-pypi to pass @@ -86,6 +88,11 @@ brew update && brew upgrade binlecode/actop/actop # Homebrew (tap repo synced git fetch origin && git checkout main && git pull --ff-only origin main scripts/tag_release.sh "X.Y.Z" ``` +- **`gh release create` fails or is skipped (auth/permissions):** the tag may be pushed but **no GitHub Release exists** — the release cut is not done until it does. Confirm `gh auth status` and re-run just the release step against the existing tag: + ```bash + gh release create "vX.Y.Z" --title "actop X.Y.Z" --notes "$(awk '/^## \['"X.Y.Z"'\]/ {f=1; next} f && /^## \[/ {exit} f' CHANGELOG.md)" + ``` + Never leave a release cut as a bare tag — verify it appears on `/releases` before moving on. - **`release-formula` fails with tag/version mismatch:** tag `vX.Y.Z` doesn't match `pyproject.toml` in the tag commit. Fix in a new PR, merge, then create a **new** tag (never reuse the old tag). - **`release-formula` fails at tap checkout/push:** missing or under-scoped `HOMEBREW_TAP_TOKEN`. Confirm the secret exists on `binlecode/actop` and the PAT has **Contents: Read/write** on `binlecode/homebrew-actop`. Re-run the failed job. - **`publish-pypi` fails with OIDC / trusted-publisher error:** verify the trusted publisher on pypi.org matches repo `binlecode/actop`, workflow `publish-pypi.yml`, environment `pypi`; confirm the run is from a `v*` tag (the `pypi` environment's deploy policy is tag-only). `skip-existing` makes re-runs safe. **Break-glass:** publish that version manually with the token-driven Flow B (`twine upload --skip-existing`), then restore OIDC for subsequent releases. diff --git a/pyproject.toml b/pyproject.toml index eac6c58..ceffb77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "actop" -version = "1.6.7" +version = "1.6.8" description = "Apple Silicon (M1–M4) power, GPU, ANE & memory-bandwidth monitor — sudoless TUI + Python API for profiling local LLM / MLX / CoreML inference" readme = "README.md" license = { text = "MIT" } diff --git a/scripts/tag_release.sh b/scripts/tag_release.sh index c848fcc..7c45f93 100755 --- a/scripts/tag_release.sh +++ b/scripts/tag_release.sh @@ -24,6 +24,13 @@ Behavior: - Verifies tag does not already exist locally/remotely - Verifies local main is in sync with origin/main (no-op push) - Creates and pushes tag vX.Y.Z + - Creates the GitHub Release for vX.Y.Z (gh release create) with the notes + from the matching CHANGELOG.md section + +RULE: a release cut REQUIRES the GitHub Release, not just the git tag. This +script always creates both; a bare `git tag` + push is NOT a release (it never +surfaces on /releases or as Latest). If the gh step fails, fix auth and finish +the release manually before moving on. This tag push triggers release-formula.yml (syncs the formula in the tap repo binlecode/homebrew-actop) and publish-pypi.yml (publishes to PyPI via OIDC). @@ -122,5 +129,26 @@ git tag "${TAG}" echo "Pushing tag ${TAG}..." git push origin "${TAG}" -echo "Done. Tag ${TAG} pushed." +echo "Creating GitHub Release ${TAG}..." +if ! command -v gh >/dev/null 2>&1; then + echo "Error: gh CLI not found — the GitHub Release (required for a release cut) was not created." >&2 + echo "The tag was pushed, but the release is incomplete. Install gh and run:" >&2 + echo " gh release create \"${TAG}\" --title \"actop ${VERSION}\" --notes \"\$(awk '/^## \\[${VERSION}\\]/ {f=1; next} f && /^## \\[/ {exit} f' CHANGELOG.md)\"" >&2 + exit 1 +fi + +RELEASE_NOTES="$(awk -v ver="${VERSION}" ' + $0 ~ "^## \\[" ver "\\]( -.*)?$" { f=1; next } + f && /^## \[/ { exit } + f { print } +' CHANGELOG.md)" + +if [[ -z "$RELEASE_NOTES" ]]; then + echo "No CHANGELOG.md section found for ${VERSION}; using generated notes." + gh release create "${TAG}" --generate-notes +else + gh release create "${TAG}" --title "actop ${VERSION}" --notes "${RELEASE_NOTES}" +fi + +echo "Done. Tag ${TAG} pushed and GitHub Release created." echo "Release formula workflow should start from this tag push."