Skip to main content
The CI tier is whatever the workload actually needs. Build it from marketplace actions, version-pin every dependency, and treat releases as permanent.
The policy on this page applies to every workflow under JacobPEvans/* and dryvist/*. The companion page git-signing covers identity and signed-commit attribution; this page covers everything else.

Prefer native CI/CD constructs

TaskUse thisNot this
CI/CD automationMarketplace actions, reusable workflows, composite actionsCustom shell script
GitHub Actions logicExpressions, fromJSON(), matrix strategiesPython or bash in a run: step
Scripts belong in dedicated files (.github/scripts/*.sh, scripts/*.py), not inlined into YAML — see No scripts in non-script files for the full rule.

GitHub Releases

Treat published releases as permanent. Once a release is promoted from draft to published, do not modify or delete it — ever. GitHub technically allows edits and deletions; the policy here forbids it. If a correction is needed, create a new release.
  • Open releases as drafts until fully complete (all assets uploaded, notes finalized).
  • Promote draft to published only when everything is ready.
  • Every repo uses release-please for automated version bumps:
    • Patch bumps come from fix: commits
    • Minor bumps come from feat: commits
    • Major bumps are human-initiated only — edit .release-please-manifest.json manually. Automated major bumps (including from BREAKING CHANGE: footers) are blocked by the release workflow
  • Conventional-commit style preference:
    • fix: for config tweaks, small improvements, incremental adjustments, dependency updates
    • feat: reserved for genuinely new capabilities, integrations, or significant behavioral changes
  • Reusable workflows are homed in dryvist/.github (the canonical org home) — see Reusable workflow ownership for the few still being relocated
Conventional Commits and the no-emoji rule for commit subjects live in Commit conventions.

Dependency versioning

See Dependency automation for the full trust-tier, scheduling, automerge, and version-pinning policy — including which action sources ride @main, which take SemVer tags, and which are SHA-pinned.

Scanner posture for self-references

@main on a self-owned workflow is deliberate — consumers ride upstream fixes the moment they land, with no per-repo Renovate PR. SemVer tags still exist and release-please keeps bumping them for the changelog and the per-run resolved-SHA audit trail, but callers ride @main. Every org scanner is configured to allow it for dryvist/* action sources:
  • RenovatepinDigests: false for dryvist/** overrides the global digest-pinning, so @main is never rewritten to a SHA.
  • zizmorunpinned-uses policy dryvist/*: ref-pin.
  • CodeQL — code scanning default setup on public repos only (free; private repos excluded to avoid the paid Code Security charge), managed as IaC in dryvist/tofu-github (per-repo, pending the provider resource). No committed query-filter config; same-org @main false positives (github/codeql#18316) are dismissed in the code scanning UI.
  • OSV-Scanner — reports dependency vulnerabilities, not ref pinning, so @main is never flagged.
Untrusted/external actions are unaffected and keep their SHA digests. The full per-scanner posture lives in dryvist/.github → SECURITY.md.

Reusable workflow ownership

Reusable GitHub Actions workflows are referenced by a literal owner/repo in uses: — GitHub allows no variables there and does not follow repository move or rename redirects for those references. So when a shared-CI repository changes ownership, every consumer’s uses: must be updated at the same time. dryvist is the canonical home for everything dryvist usesJacobPEvans-personal/* may depend on dryvist/*, never the reverse. Reference each workflow by its literal current owner below until a pending relocation lands:
Workflow setCurrent homeStatus
ai-workflows reusable workflowsdryvist/ai-workflowscanonical
Nix + release-please reusable workflowsdryvist/.githubcanonical
Other shared .github workflows (_markdown-lint, _ci-gate, …)JacobPEvans-personal/.githubpending relocation to dryvist/.github

Runner choice

RunsOn (AWS EC2 spot via tofu-runs-on) is the default for Linux jobs. The control plane carries a small fixed monthly cost regardless of job volume; once it is paid for, workflows that stay on ubuntu-latest spend GitHub Actions minutes that do not need to be spent. On-prem self-hosted is the documented exception for jobs that genuinely need local hardware — not a routine choice.
WorkloadRunner
Linux job (lint, validate, build, test)RunsOn — runs-on=${{ github.run_id }}/runner=2cpu-linux-x64
Nix flake check (Linux)RunsOn with more RAM — runs-on=${{ github.run_id }}/cpu=4/ram=16/family=m7+c7/extras=s3-cache
macos-latestGitHub-hosted — RunsOn EC2 Mac has a 24-hour minimum allocation; for short jobs macos-latest is cheaper despite the 10× billing multiplier
windows-latestRunsOn — supports Windows; case-by-case
*.lock.yml from gh-aw compileGitHub-hosted — lock files are regenerated; runner label must flow through the .md companion (gh-aw does not expose this yet)
Disabled-schedule workflow (manual workflow_dispatch only)GitHub-hosted — migration saves nothing
Job requiring local hardware (OrbStack cluster, Proxmox LAN access, dev-loop instrumentation that cannot be replicated on a fresh EC2 spot instance)on-prem self-hosted[self-hosted, Linux, ARM64] (orbstack-kubernetes) or [self-hosted, Linux] (ansible-proxmox-apps); see “On-prem runner requirements” below
The leading runs-on=${{ github.run_id }} segment is required so the RunsOn control plane can correlate the GitHub Actions workflow_job webhook back to the originating run — without it, the job hangs in queued. The shared reusable workflows accept a runner_label input (default ubuntu-latest); callers opt in by passing the RunsOn label string. Full RunsOn label catalog, prereqs (GitHub App allowlist), rollout playbook, and verification steps live in tofu-runs-on/docs/migration-guide.md.

On-prem runner requirements

On-prem self-hosted runners are a single point of failure when the job has E2E coverage and the runner is broken or stale. Every on-prem runner must:
  1. Authenticate via a GitHub App (APP_ID + APP_PRIVATE_KEY) — never a personal access token. App installation tokens auto-refresh; PATs silently expire and break the runner with no upstream visibility.
  2. Pin the runner image (or VM template) by digest, with Renovate tracking the digest. No floating tags.
  3. Expose a process-level healthcheck (Docker healthcheck:, systemd WatchdogSec, or equivalent) so failed state surfaces in standard inspection tools.
  4. Emit a periodic heartbeat to a dead-man’s-switch endpoint (healthchecks.io or equivalent) so silent death pages someone.
  5. Pre-flight check the secret-injection step so it fails loud when required secrets are missing.
Reference implementation: orbstack-kubernetes/docker/actions-runner/ — see that repo’s Makefile runner-* targets and TESTING.md.

Where to go next

Git signing

Identity per execution context and the App-token pattern that signs every AI-authored commit.

tofu-runs-on

The RunsOn runner pool itself — provisioning, OIDC trust, migration guide.

OpenTofu check placement

Static checks in pre-commit, credentialed operations in CI via OIDC.

CI/CD overview

Runner tiers in context, plan/apply gating, branch rulesets.