| name | cut-a-release |
| description | The documentation work that must land before a release is tagged — capture a capabilities deck pinned to the release commit, publish it to Google Slides, link it, and re-verify the docs gates. Use when every release gate is green and the next step is tagging a version and creating the GitHub release. |
Cut a release
The gates prove the code works. They prove nothing about whether the
documentation still describes it, and that is the part that rots silently: a
README sentence naming a deleted tree stays legible for weeks, and a slide
claiming a capability outlives the capability by longer. This skill is the
documentation pass that runs after the gates are green and before
anything is tagged.
Its centrepiece is a capabilities deck pinned to the exact commit being
released. Pinning is the point: a deck that is edited forever answers "what can
mac do?" with "it depends when you looked", while a deck bound to a SHA can be
checked against that SHA a year later.
Scope. This covers documentation and the tag/release mechanics. Fleet
cutover and image qualification are separate and are not in here — see
docs/synchronized-fleet-cutover.md and
docs/image-publication-and-qualification.md.
0. Do not start until the gates are actually green
Not "were green this morning". Run them, on the commit you intend to release:
make lint
make test
make docs-check
and confirm CI is green on main for that commit:
gh run list --branch main --limit 5
If main is red for an unrelated reason, say so and stop. Releasing on top of a
known-red trunk turns one person's broken test into everyone's release.
1. Pin the directory to the commit
git rev-parse --short=8 HEAD
date -u +%Y%m%dT%H%M%SZ
mkdir -p docs/presentation/<timestamp>-<sha>/images
The timestamp sorts, so ls is chronological, and it disambiguates two decks
cut from one commit for different audiences. The SHA is what makes a claim
checkable later. Neither alone is enough; together they cannot collide. The
convention is written up in docs/presentation/README.md.
Never revise a previous deck. Make a new directory.
2. Audit from source, never from memory
This is the step that carries the value, and the one it is tempting to skip
because you think you know what changed. It is not satisfied by the generated
CLI/OpenAPI references or the capabilities deck alone: before every release,
every current documentation file must be audited against the candidate tree and
its deployed behaviour, and source code is the source of truth — not memory,
issue text, a prior deck, or planned behaviour. Walk the whole current-doc set
from the index, and for each file record a changed / not-changed decision with
its source anchor. docs/reference/documentation-inventory.md enumerates every
current doc for exactly this pass; no active file is left unchecked. Read:
grep -c "^## mac" docs/reference/cli.md
grep -cE "^\| .(GET|POST|PUT|PATCH|DELETE)." docs/reference/openapi.md
git log --oneline $(git describe --tags --abbrev=0)..HEAD
grep -l "Status: \*\*Proposed\*\*" docs/adr/*.md
Three rules that decide whether the deck is worth anything:
- The generated references are authoritative for counts. CI fails when they
drift from the parser and the OpenAPI schema, so they are true by
construction. Prose in
README.md is not.
- Where the README and the code disagree, follow the code, and record the
discrepancy in
AUDIT.md. That is how the deck stays honest about a repo
that is moving faster than its prose.
- An ADR marked
Proposed has not shipped. Say so on the slide. A
capabilities deck that presents proposals as capabilities is marketing, and
the first engineer to read the code will find it.
Date every measured figure. Ledger and token-routing numbers are true for a
window, not forever.
3. Diagrams are SVG, and only the SVG is committed
Author or refresh images/*.svg by hand, then render to PNG for the deck:
CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
cd docs/presentation/<timestamp>-<sha>/images
"$CHROME" --headless --disable-gpu --hide-scrollbars \
--force-device-scale-factor=2 --window-size=1520,900 \
--default-background-color=FFFFFF \
--screenshot=01-object-model.png "file://$PWD/01-object-model.svg"
Render at 2× so the diagrams survive a projector. Look at every PNG before
building the deck — SVG text does not wrap, so an overlong line silently
overflows its box and nothing warns you.
docs/ stays text-only: the PNGs and the .pptx are gitignored. See
docs/presentation/README.md for why that is a hard rule and not tidiness.
4. AUDIT.md, or the deck is unverifiable
Every figure and claim traced to a file, commit, or generated reference. Without
it, next year nobody can tell which slides are still true, and the deck becomes
folklore with a logo. It is also where the README/code discrepancies from step 2
are recorded.
5. Build and publish
python3 -m venv /tmp/deckvenv && /tmp/deckvenv/bin/pip install python-pptx
/tmp/deckvenv/bin/python docs/presentation/<timestamp>-<sha>/build_deck.py
scripts/publish-deck-to-slides.py \
docs/presentation/<timestamp>-<sha>/mac-capabilities-<sha>.pptx \
--title "MAC — <subject> (<sha>)" \
--expect-slides <n>
python-pptx is deliberately not a repository dependency; the deck is a
documentation artifact, not part of the shipped runtime.
Pass --expect-slides. The script exports the published deck back out of Google
and counts its pages, so a conversion that dropped slides fails here instead of
in front of an audience.
The published deck is private to the uploading account. If the release notes
will link it for anyone else, share it explicitly.
6. Link it in all three places
A deck nobody can find was not published.
- The deck's own
README.md — the Slides URL, the slide list, how to rebuild.
docs/presentation/README.md — add a row to the existing-decks table.
- The root
README.md — the entry under ## Documentation.
7. Re-verify, in this order
The order matters and is the trap that cost the most time:
git add docs/presentation <other changed files>
MAC_TEST_PG_URL=... uv run --extra dev --extra postgres pytest \
tests/test_docs_no_operator_identity.py tests/test_guide_docs_are_true.py \
tests/test_docs_graph.py -q
make docs-check
Stage before running. tests/test_docs_no_operator_identity.py enumerates
git ls-files, so it only scans tracked files. Run it while the new directory
is untracked and it scans nothing you wrote and reports a confident pass over an
empty set. The same is true of scripts/check-docs-graph.py, which enumerates
tracked docs: stage new docs before it can see them.
Every current doc must be reachable from README.md. make docs-check
runs scripts/check-docs-graph.py, which traverses the internal-link graph out
of the root README.md and fails on any current doc that is orphaned, any
broken internal link, or any current doc missing from
docs/reference/documentation-inventory.md. Leaf docs need not be linked
directly from README.md: the complete documentation index
(docs/reference/documentation-inventory.md) is README-reachable and links
every current doc, and historical material stays behind the explicitly linked,
visibly-labelled archive index (docs/archive/index.md). If you added or moved
a doc, regenerate the index with python scripts/generate-docs-reference.py --write and commit it, or the gate reports the doc as orphaned or missing from
the inventory. Only pinned decks under docs/presentation/ are allowlisted, and
that list is the single place an exemption may be argued.
8. Then, and only then, cut the release
The version is single-sourced. pyproject.toml declares dynamic = ["version"]
pointing at mac.__version__, and mac.api / mac.a2a.card import it, so one
line is the whole bump:
$EDITOR src/mac/__init__.py
git commit -am "Release vX.Y.Z"
Land it the way all work lands here — through a pull request, not a push to
main (skills/mac-cli/SKILL.md). Once it is on main:
git tag vX.Y.Z && git push origin vX.Y.Z
gh release create vX.Y.Z --title "mac vX.Y.Z" --notes-file notes.md
Write the notes from the deck's AUDIT.md, not from the commit log. The log
says what changed; the audit says what is now true, which is what a reader
wants, and it is already sourced.
The traps, all of which have happened
gcloud cannot publish anything. There is no gcloud slides or
gcloud drive — the Cloud CLI has no Workspace surface. It mints a token; the
Drive API does the upload. The default credential has no Drive scope, so
gcloud auth login --enable-gdrive-access is a prerequisite.
- Never commit the PNGs or the
.pptx. Beyond the repository weight,
tests/test_docs_no_operator_identity.py greps every tracked file under docs/ for
fleet-identity tokens, and compressed image data matches one by coincidence
eventually. The first PNG committed here did.
- Do not name a forbidden identity token, even to explain one. That gate
does not care why the token is present, and it is right not to — a gate that
accepts "I am only quoting it" accepts anything. Only the test file itself is
exempt.
- No
bash/sh/shell fences anywhere under docs/ outside the
executable book; scripts/test-docs.py rejects them. Use console for
transcripts. (Skills like this one are outside docs/, so they may use
bash.)
- Do not quote a dead link.
scripts/check-docs-accessibility.py resolves relative
links under docs/, so pasting a broken link into an audit as evidence
reproduces the defect in a checked file. Describe the target instead.
- A backticked repository path asserts that the path exists
(
tests/test_guide_docs_are_true.py). Writing about something deliberately
removed? Leave it unbackticked.