This playbook is for coding agents working in repos where GitHub Actions, release scripts, signing, packaging, deployment, or artifact publication matter. The goal is professional release plumbing without turning every repo into a ceremony machine.
Treat release infrastructure as a control system:
- the manifest is the schematic
- the runner is the controller
- CI is the fixture bench
- artifacts are the outputs
- evidence is the measurement
- bless is signoff
Do not let release behavior live half in YAML, half in shell snippets, half in docs, and half in local machine memory. Pick one source of truth and make every other layer read from it.
Use this layered model unless the repo clearly has a better established pattern:
release manifest
Declares version, channel, signing, publishing, proof, budgets, and policy.
release runner
Implements validate, build, publish, prove, audit, and bless.
thin scripts
Provide stable human entrypoints and compatibility shims.
GitHub Actions
Choose trigger, runner, environment, permissions, credentials, and artifacts.
proof/evidence
Machine-readable JSON plus logs/screenshots only where they prove real behavior.
Prefer a real programming language for release orchestration once the workflow is larger than a few shell commands.
Good default choices:
- TypeScript for repos that already use Node, need structured CLI behavior, and interact with JSON/GitHub/web tooling.
- Python for repos that already use Python heavily and need filesystem/process orchestration.
- Go or Swift only when the release tool itself needs to be distributed as a compiled binary or share product libraries.
Use Bash only for thin entrypoints and truly shell-native platform helpers. Bash is fine for:
exec node release/runner/dist/cli.js "$@"- invoking
codesign,xcrun notarytool,hdiutil, orsecurity - tiny compatibility wrappers
Bash is a poor fit for:
- typed manifest validation
- complex branching release policy
- JSON evidence generation
- GitHub API response parsing
- retry state machines
- cross-phase orchestration
For TypeScript release runners, prefer:
commanderfor the CLIexecafor subprocess executionzodfor runtime schema validation- a TOML parser for manifest files
- explicit modules for phases, evidence, GitHub, appcast/update metadata, and errors
Be cautious with zx: it is useful for small scripts, but release systems should prefer explicit subprocess wrappers and typed result objects.
Do not perform a heroic rewrite from Bash to TypeScript just because Bash feels ugly. Migrate one phase at a time, keep the public command stable, and avoid two manifest parsers. If a Python manifest tool already owns release truth, either keep it as the manifest authority during the transition or replace it deliberately with one tested TypeScript implementation.
GitHub Actions workflows should be thin and boring.
Good workflow responsibilities:
- checkout source
- select runner and environment
- set minimal permissions
- install toolchains
- expose secrets/vars
- call one repo-owned command
- upload evidence artifacts
Bad workflow responsibilities:
- computing release versions
- deciding update class
- constructing appcast URLs
- hand-writing JSON
- duplicating local release commands
- branching product policy in YAML
When a workflow grows large, first ask whether the repo-owned release runner should own that logic. Use reusable workflows only after multiple repos share the same job shape. Use composite actions for reusable step bundles inside one job, not for multi-job release control.
Use these phase names consistently when a repo has release automation:
Checks source truth. It may read files, inspect Git state, and validate schemas. It must not build or publish artifacts.
Produces artifacts from the current source and manifest. It should run validate first. It should not publish.
Uploads already-built artifacts. It must not rebuild. It records exact URLs, asset names, hashes, and sizes.
Where GitHub supports it, publish artifact attestations or provenance for binaries, appcasts, checksums, and asset manifests. Grant attestation permissions only to the job that emits them.
Runs end-to-end release proof against production-shaped artifacts. For apps, this means install/launch/update/check real behavior, not just parse logs.
Checks published assets after publication. It should verify public URLs, checksums, appcast/update metadata, and propagation.
Verifies that required evidence exists and passed. It must not perform new builds, uploads, or proof runs.
Every non-trivial release phase should write machine-readable evidence with:
- schema version
- phase
- channel
- version or git SHA
- started and ended timestamps
- status
- command inputs
- artifact paths or URLs
- hashes and sizes where relevant
- proof result paths
- failure reason, if any
Logs are supporting evidence, not the evidence model. Prefer JSON first, logs second, screenshots only for GUI proof or user-facing behavior.
- Never run untrusted pull-request code on privileged self-hosted release runners.
- Never use
pull_request_targetto execute untrusted code with release secrets. - Use protected GitHub environments for signing, notarization, deployment, and production publishing.
- Scope
GITHUB_TOKENpermissions per job. - Use
contents: writeonly for publishing jobs. - Use
id-token: writeandattestations: writeonly where the job actually emits provenance. - Prefer OIDC or environment-scoped secrets over broad long-lived tokens when the hosting provider supports it.
- Keep signing/notary credentials outside repo files and outside generic dev jobs.
- On self-hosted runners, record runner identity, OS version, Xcode/toolchain version, workspace path, and artifact paths.
- Redact evidence before publishing or attaching it to public releases.
When asked to improve release or CI plumbing:
- Inspect the current source of truth before changing scripts or YAML.
- Identify what is manifest truth, runner behavior, CI harness, and proof evidence.
- Preserve the operator command when possible.
- Move policy out of GitHub Actions YAML and into the repo-owned runner.
- Replace script sprawl with owned helper locations, not with another pile of entrypoints.
- Keep local and CI commands aligned.
- Run both static validation and at least one production-shaped proof loop.
- Do not claim release readiness unless artifacts and evidence exist.
- For GitHub Actions changes, run
actionlintwhen available.
When deleting release bloat:
- Delete obsolete wrappers first.
- Merge one-off proof scripts into one named proof command when behavior belongs together.
- Move low-level helpers under the subsystem that owns them.
- Keep release commands discoverable: one release runner, one test command, one package command if needed.
- Update workflow references and docs in the same change.
- A repo has several package/release scripts that differ only by environment variables.
- GitHub Actions YAML computes release policy.
- A release step rebuilds during publish.
- A bless step performs new work instead of checking existing proof.
- Evidence exists only as prose in a doc.
- A shell script contains hundreds of lines of JSON writing or API parsing.
- Release facts can be overridden by random environment variables even though a manifest exists.
- Self-hosted runner proof relies on a manually launched app while claiming startup/update behavior.
- CI passes, but no artifact was installed, launched, updated, or queried through the real product surface.
name: Release
on:
workflow_dispatch:
permissions:
contents: write
id-token: write
attestations: write
jobs:
release:
runs-on: [self-hosted, macOS, ARM64, release-runner]
environment: release
steps:
- uses: actions/checkout@v6
- name: Prepare credentials
run: ./scripts/prepare-release-keychain.sh
- name: Build release
run: ./scripts/release-train.sh build --channel official
- name: Publish release
run: ./scripts/release-train.sh publish --channel official
- name: Upload evidence
uses: actions/upload-artifact@v4
with:
name: release-evidence
path: dist/release-evidence/**The exact commands will vary by repo. The shape should not: workflow owns execution context; the release runner owns release meaning.
- GitHub Actions reusable workflows and permissions: https://docs.github.com/actions/reference/workflows-and-actions/reusable-workflows
- GitHub Actions Toolkit: https://github.com/actions/toolkit
- Zod runtime schema validation: https://zod.dev/api
- Execa subprocess execution: https://github.com/sindresorhus/execa
- Commander CLI framework: https://www.npmjs.com/package/commander
- zx scripting tool: https://google.github.io/zx/