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
61 changes: 40 additions & 21 deletions .agents/skills/record-tui-gif/actop-demo.tape
Original file line number Diff line number Diff line change
@@ -1,48 +1,67 @@
# actop hero capture — the README/hero GIF.
# One continuous, gapless take in the stack layout that tells the whole story:
# off-camera: start actop -> l (stack layout)
# on-camera: stack + braille dots -> g (block charts) -> p (process table)
# -> t (cycle theme, several times) -> t back to dark
# Layout switch is off-camera (grid would leave a gap at this height); the glyph,
# process, and theme toggles are on-camera and stay full because stack fills the height.
# Render from the repo root: vhs .claude/skills/record-tui-gif/actop-demo.tape
# One continuous, gapless take in the DEFAULT grid layout that tells the whole
# v1.9.x story:
# off-camera: start actop (grid is the default — no layout switch needed)
# on-camera: grid + braille dots -> g (block charts) -> c (per-core panels)
# -> p (process table + watt attribution) -> t (theme cycle)
#
# Why grid rather than stack: since v1.9.0 the dashboard has seven sections, and
# grid is the only layout that shows the mirrored Network / Disk pair side by
# side without a ~57-row column. Stack would need Height ~1240 here, which
# leaves a large gap under Power once the process table is on.
#
# Width is load-bearing: `p` splits the dashboard against the process table, and
# below ~96 cols the grid preset auto-degrades to stack. At 1400px (~165 cols)
# the dashboard drops to ~80 cols and silently degrades mid-take; 1800px
# (~212 cols) leaves it ~122 cols, so grid survives the whole recording.
#
# Render from the repo root: vhs .agents/skills/record-tui-gif/actop-demo.tape
# (or via record.sh, which drives a GPU workload so the gauges move).

Output images/actop-demo.gif

Require actop
# Record the REPO build, not whatever `actop` happens to be on PATH. Typing a
# bare `actop` picks up the Homebrew/pipx install, which silently recorded a
# stale TUI (a v1.9.x hero was captured off a v1.8.0 binary before this was
# caught — no Network/Disk sections at all). Require the venv entry point.
Require .venv/bin/actop

Set Shell "zsh"
Set FontSize 16
# 1400px @ FontSize 16 ≈ 165 cols — well above the ~96-col grid-degrade floor.
Set Width 1400
# Sized to the stack layout's natural column height so there's no bottom gap.
Set Height 1050
# 1800px @ FontSize 16 ≈ 212 cols — keeps the dashboard above the 96-col
# grid-degrade floor even after `p` splits it against the process table.
Set Width 1800
# Sized to the tallest on-camera state (grid + per-core panels + process table
# ≈ 42 dashboard rows + chrome), so nothing clips and no gap opens under Power.
Set Height 930
Set Theme "Dracula"
Set Padding 20
Set Framerate 24

# --- Warm up + switch to stack layout OFF-camera ---
# --- Warm up OFF-camera so the first visible frame already has live gauges ---
Hide
Type "actop"
Type ".venv/bin/actop"
Enter
Sleep 6s
Type "l"
Sleep 1s
Sleep 7s
Show

# --- 1) stack layout, classic braille-dot charts ---
Sleep 5s
# --- 1) default grid layout, classic braille-dot charts ---
# Seven sections: P-CPU | E-CPU, GPU·ANE | Memory, Network | Disk, Power.
Sleep 6s

# --- 2) toggle chart glyph dots -> modern solid blocks ---
Type "g"
Sleep 5s

# --- 3) turn on the process table (needs a poll cycle to populate) ---
# --- 3) reveal the per-core panels inside both cluster boxes ---
Type "c"
Sleep 5s

# --- 4) turn on the process table (needs a poll cycle to populate) ---
Type "p"
Sleep 8s

# --- 4) cycle through themes live ---
# --- 5) cycle through themes live ---
Type "t"
Sleep 2s
Type "t"
Expand Down
28 changes: 23 additions & 5 deletions .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
name: publish-pypi
run-name: "publish-pypi | ${{ github.event_name == 'workflow_dispatch' && inputs.tag_name || github.ref_name }} | ${{ github.sha }}"

# Publishes the actop package to PyPI on a version tag push using PyPI
# Trusted Publishing (OIDC) — no API token is stored in the repo.
# Publishes the actop package to PyPI using PyPI Trusted Publishing (OIDC) —
# no API token is stored in the repo.
#
# MANUAL ONLY — this workflow does NOT run on tag push. Publishing is a
# deliberate step taken AFTER the tag and its CI have landed, because the
# automatic form was self-defeating: a tag push started the publish at the same
# moment the tag appeared, so the run raced the release it was meant to ship and
# never published against a settled tag. Making it a dispatch that takes the tag
# as an input removes the ordering problem entirely — the tag already exists and
# is named explicitly.
#
# Publish with (after `scripts/tag_release.sh` and green CI):
# gh workflow run publish-pypi.yml --ref vX.Y.Z -f tag_name=vX.Y.Z
#
# `--ref` must be the TAG, not a branch: the `pypi` environment's deployment
# policy only admits refs matching `v*`, so a dispatch from main is rejected
# before the job starts. Pointing it at the tag satisfies the policy and keeps
# the "only a tagged commit ever reaches PyPI" guarantee that the old automatic
# trigger provided.
#
# Local fallback, from a clean checkout of the tag (needs a PyPI API token —
# OIDC is only available to the workflow above):
# python -m build && python -m twine upload --skip-existing dist/*
#
# One-time PyPI-side setup (do this once, signed in as the project owner):
# 1. https://pypi.org/manage/account/publishing/ -> "Add a new pending publisher"
Expand All @@ -15,9 +36,6 @@ run-name: "publish-pypi | ${{ github.event_name == 'workflow_dispatch' && inputs
# trusted publisher for the project.

on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag_name:
Expand Down
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,31 @@ This project follows a Keep a Changelog-style format and uses version tags for r

## [Unreleased]

## [1.9.2] - 2026-08-12

### Changed

- **PyPI publishing left the tag flow — it is now a manual, post-tag step.**
`publish-pypi.yml` no longer triggers on `push: tags`; it is
`workflow_dispatch` only, taking the tag as an input:
`gh workflow run publish-pypi.yml --ref vX.Y.Z -f tag_name=vX.Y.Z`.
The automatic form was self-defeating — the tag push started the publish at
the moment the tag appeared, so the run raced the release it was meant to
ship rather than publishing a settled one. Dispatching against a tag that
already exists removes the ordering problem entirely, and because the `pypi`
environment admits only `v*` refs, the "only a tagged commit reaches PyPI"
guarantee is preserved rather than weakened. OIDC trusted publishing is
unchanged; no token is introduced.
- **Hero GIF re-recorded for the v1.9.x dashboard** (`images/actop-demo.gif`),
and the recording tape now pins the binary it records. The tape typed a bare
`actop`, which resolved to whatever was installed on `PATH` — it had been
capturing a stale Homebrew build with no Network/Disk sections at all. It now
requires `.venv/bin/actop`. The capture moved to the default `grid` layout at
1800x930 (stack pushed the two new I/O boxes below the fold) and covers the
mirrored Network/Disk pair, per-core panels, both glyph modes, the process
table with watt attribution, and the theme cycle.
- Cover page: net/disk feature card now describes the mirrored charts.

## [1.9.1] - 2026-08-12

### Fixed
Expand Down
17 changes: 11 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,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`, `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.
`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. **PyPI publishing is NOT part of the tag flow** — it is a manual step taken after the tag and its CI have landed (`gh workflow run publish-pypi.yml --ref vX.Y.Z -f tag_name=vX.Y.Z`). The old tag-triggered publish raced the tag that started it and shipped against an unsettled ref; a dispatch naming the tag explicitly has no such ordering problem. 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 All @@ -74,10 +74,15 @@ git checkout main && git pull --ff-only
# 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
# 5. Monitor CI — wait for release-formula and guard-release to pass
gh run list -R binlecode/actop --limit 10

# 6. Verify
# 6. Publish to PyPI — MANUAL, only after the tag exists and CI is green.
# --ref must be the tag: the `pypi` environment admits only `v*` refs.
gh workflow run publish-pypi.yml --ref "vX.Y.Z" -f tag_name="vX.Y.Z"
gh run watch -R binlecode/actop "$(gh run list -R binlecode/actop --workflow publish-pypi.yml --limit 1 --json databaseId --jq '.[0].databaseId')"

# 7. Verify
pipx install actop && actop --version # PyPI
brew update && brew upgrade binlecode/actop/actop # Homebrew (tap repo synced)
```
Expand All @@ -96,12 +101,12 @@ brew update && brew upgrade binlecode/actop/actop # Homebrew (tap repo synced
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.
- **`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 dispatch used `--ref vX.Y.Z` (the `pypi` environment's deploy policy is tag-only, so a dispatch from `main` is rejected before the job starts). `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.
- **Emergency rerun without resource refresh:** `workflow_dispatch` on `release-formula` with `refresh_resources=false` as a temporary workaround; follow up with a normal run (`refresh_resources=true`).

### PyPI publishing — two flows

OIDC Trusted Publishing is the default (no stored secret; the `pypi` environment on `binlecode/actop` is tag-only, and the workflow file documents the one-time publisher registration). The token-driven flow is the fallback / bootstrap and the only place a long-lived PyPI credential appears:
OIDC Trusted Publishing is the default (no stored secret; the `pypi` environment on `binlecode/actop` is tag-only, and the workflow file documents the one-time publisher registration). It is **dispatched manually against the tag**, never fired by the tag push. The token-driven flow is the fallback / bootstrap and the only place a long-lived PyPI credential appears:

```bash
python -m build # produces dist/*.tar.gz and dist/*.whl
Expand Down Expand Up @@ -229,7 +234,7 @@ a mount point, not a fake); faking the data or the logic under test is not.

## Commit & Pull Request Guidelines
- **Branch from `main`; PR strictly into `main`.** Every branch forks from `main` and targets `main`. **Never fork a feature branch off another feature branch** (no stacked PRs): if you need work that is still on an unmerged branch, wait for it to merge and re-branch from `main`. This holds especially for CI/CD and release changes — they land via a single PR into `main`, never a chained branch.
- **Bump the version in every PR.** Each PR updates `pyproject.toml` version + moves `CHANGELOG.md` `[Unreleased]` into a new dated section, in the same PR: **patch bump by default, minor only for a milestone PR** (major reserved for breaking API/CLI changes). Tagging (`scripts/tag_release.sh`) is a separate step after merge and is what publishes — see the Release Process section above.
- **Bump the version in every PR.** Each PR updates `pyproject.toml` version + moves `CHANGELOG.md` `[Unreleased]` into a new dated section, in the same PR: **patch bump by default, minor only for a milestone PR** (major reserved for breaking API/CLI changes). Tagging (`scripts/tag_release.sh`) is a separate step after merge; publishing to PyPI is a third, manual step after that — see the Release Process section above.
- Use concise, imperative commit subjects (as seen in history), e.g. `Add support for M1 Ultra` or `actop/utils.py: add bandwidth of M2`.
- Keep commits scoped to one logical change.
- Before every commit and before every push, always run:
Expand Down
Binary file modified cover/actop-demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion cover/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ <h3><span class="ic">▮</span>Memory bandwidth</h3>
</div>
<div class="feature">
<h3><span class="ic">▮</span>Net / disk I/O</h3>
<p>Live network ↓/↑ and disk read/write throughput in bytes/s, sourced from native getifaddrs and IOKit counters — no sudo, no subprocesses.</p>
<p>Network and disk throughput as mirrored in-over-out charts sharing a zero axis, sourced from native getifaddrs and IOKit counters — no sudo, no subprocesses.</p>
</div>
<div class="feature">
<h3><span class="ic">▮</span>Thermals & throttling</h3>
Expand Down
Binary file modified images/actop-demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "actop"
version = "1.9.1"
version = "1.9.2"
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" }
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.