Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@
# branch + pull request. This mirrors the server-side branch protection on
# binlecode/actop and fails fast locally before a push is rejected by GitHub.
#
# Also nudges when a v* tag is being pushed directly (bare `git push origin
# vX.Y.Z`) — use scripts/tag_release.sh instead; it creates the required
# GitHub Release object alongside the tag.
#
# Enabled via: git config core.hooksPath .githooks
set -euo pipefail

protected_branch="main"
remote="${1:-}"
warn_tag=0

while read -r local_ref local_sha remote_ref remote_sha; do
if [[ "$remote_ref" == "refs/heads/${protected_branch}" ]]; then
Expand All @@ -19,6 +24,19 @@ while read -r local_ref local_sha remote_ref remote_sha; do
echo "" >&2
exit 1
fi
if [[ "$remote_ref" =~ ^refs/tags/v ]]; then
warn_tag=1
fi
done

if [[ $warn_tag -eq 1 ]]; then
echo "" >&2
echo "WARNING: you are pushing a v* tag directly." >&2
echo "Use scripts/tag_release.sh instead — it creates the required GitHub Release" >&2
echo "object alongside the tag. A bare tag push creates no release and leaves the" >&2
echo "project's /releases page incomplete." >&2
echo "" >&2
echo "Push anyway? Proceeding…" >&2
fi

exit 0
54 changes: 54 additions & 0 deletions .github/workflows/guard-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: guard-release
run-name: "guard-release | ${{ github.ref_name }}"

# Post-push integrity guard: every tag push triggers a scan of all v* tags.
# If any tag lacks a GitHub Release object the job fails — catching the
# "silent link breakage" pattern where tags are pushed but release creation
# never happens (e.g. bare `git tag` + push without `tag_release.sh`).

on:
push:
tags:
- "v*"

permissions:
contents: read

jobs:
guard:
runs-on: ubuntu-latest
steps:
- name: Check all v* tags have a release object
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail

TAGS="$(gh api "repos/${GH_REPO}/git/refs/tags" --jq '.[].ref | ltrimstr("refs/tags/")' | grep '^v' | sort -V)"
RELEASES="$(gh api "repos/${GH_REPO}/releases" --jq '.[].tag_name' | sort -V | uniq)"

missing=()
while IFS= read -r tag; do
[[ -z "$tag" ]] && continue
# Normalise: strip ^{} deref suffix and skip non-annotated deref lines.
tag="${tag%^\{\}}"
[[ "$tag" =~ \^\{ ]] && continue
if ! printf '%s\n' "$RELEASES" | grep -qxF "$tag"; then
missing+=("$tag")
fi
done <<< "$TAGS"

if [[ ${#missing[@]} -gt 0 ]]; then
echo "" >&2
echo "GUARD FAILED: ${#missing[@]} tag(s) have no GitHub Release object:" >&2
printf ' - %s\n' "${missing[@]}" >&2
echo "" >&2
echo "Every v* tag must have a corresponding release. Create them with:" >&2
echo " scripts/tag_release.sh" >&2
echo "Or backfill an existing tag:" >&2
echo " gh release create <tag> --title \"actop <ver>\" --notes-from-tag" >&2
exit 1
fi

echo "All $(echo "$TAGS" | wc -l | tr -d ' ') v* tags have a release object."
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ This file is the single source of truth for repository guidelines, used by Claud
- Do not run `python`, `pip`, or `pytest` from the global environment for this repo.

## Build, Test, and Development Commands
- `git config core.hooksPath .githooks`: **run once per clone** to activate the local hooks (`pre-commit` secret redaction + `pre-push` `main` guard). Fresh clones have no hooks until this is set.
- `git config core.hooksPath .githooks`: **run once per clone** to activate the local hooks (`pre-commit` secret redaction + `pre-push` `main` guard + `pre-push` bare-tag nudge). Fresh clones have no hooks until this is set.
- `.venv/bin/python -m pip install -e ".[dev]"`: install editable + dev deps (ruff).
- `.venv/bin/python -m actop.actop --help`: validate CLI parsing and flags.
- `.venv/bin/python -m actop.actop --interval 2 --avg 30`: run the tool locally.
Expand Down Expand Up @@ -47,7 +47,7 @@ This file is the single source of truth for repository guidelines, used by Claud

## Release Process

`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.
`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`, `guard-release.yml`) — each carries its one-time setup inline. `guard-release.yml` is the post-push integrity gate: every tag push triggers a scan that verifies every `v*` tag has a corresponding GitHub Release object, catching the "silent link breakage" that caused the v0.8.7–v1.6.6 release-object gap.

**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.

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[![Python versions](https://img.shields.io/pypi/pyversions/actop.svg)](https://pypi.org/project/actop/)
[![License: MIT](https://img.shields.io/github/license/binlecode/actop.svg)](LICENSE)
[![CI](https://github.com/binlecode/actop/actions/workflows/main-ci.yml/badge.svg)](https://github.com/binlecode/actop/actions/workflows/main-ci.yml)
[![Website](https://img.shields.io/badge/website-actop.pages.dev-58a6ff)](https://actop.pages.dev)

**Watch your Apple Silicon Mac the way it actually works — and profile your own workloads from Python.**

Expand Down