Skip to content

Latest commit

 

History

History
237 lines (170 loc) · 9.06 KB

File metadata and controls

237 lines (170 loc) · 9.06 KB

Git Development Workflow and Release Plumbing

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.

Core Principle

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.

Preferred Shape

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.

Language Choice for Orchestration

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, or security
  • 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:

  • commander for the CLI
  • execa for subprocess execution
  • zod for 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 Rules

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.

Release Phase Contract

Use these phase names consistently when a repo has release automation:

validate

Checks source truth. It may read files, inspect Git state, and validate schemas. It must not build or publish artifacts.

build

Produces artifacts from the current source and manifest. It should run validate first. It should not publish.

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.

prove

Runs end-to-end release proof against production-shaped artifacts. For apps, this means install/launch/update/check real behavior, not just parse logs.

audit

Checks published assets after publication. It should verify public URLs, checksums, appcast/update metadata, and propagation.

bless

Verifies that required evidence exists and passed. It must not perform new builds, uploads, or proof runs.

Evidence Standard

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.

Security Rules

  • Never run untrusted pull-request code on privileged self-hosted release runners.
  • Never use pull_request_target to execute untrusted code with release secrets.
  • Use protected GitHub environments for signing, notarization, deployment, and production publishing.
  • Scope GITHUB_TOKEN permissions per job.
  • Use contents: write only for publishing jobs.
  • Use id-token: write and attestations: write only 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.

Agent Working Rules

When asked to improve release or CI plumbing:

  1. Inspect the current source of truth before changing scripts or YAML.
  2. Identify what is manifest truth, runner behavior, CI harness, and proof evidence.
  3. Preserve the operator command when possible.
  4. Move policy out of GitHub Actions YAML and into the repo-owned runner.
  5. Replace script sprawl with owned helper locations, not with another pile of entrypoints.
  6. Keep local and CI commands aligned.
  7. Run both static validation and at least one production-shaped proof loop.
  8. Do not claim release readiness unless artifacts and evidence exist.
  9. For GitHub Actions changes, run actionlint when 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.

Red Flags

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

Good Minimal GitHub Action Shape

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.

References