GitHub Actions workflow architecture

This guide owns CI and automation procedures. Repository paths and architectural ownership remain defined by Repository Layout and Architecture.

Perastage uses separate workflows for validation, automatic test artifacts, compatibility packages, and formal releases. The separation keeps Debug test coverage out of Release package builders and prevents release publication before every package and asset check has succeeded.

Issue maintenance

Awaiting Feedback Triage runs daily and can be dispatched manually in a safe dry-run mode. It processes only open issues explicitly labeled status: awaiting-feedback; see the issue triage policy for its conservative warning and closure rules. The workflow has read-only repository access plus issue write access and does not process pull requests or apply the label itself. Every write follows a fresh decision check. Versioned issue-revision markers on warnings restart inactivity after later edits, while the closing marker lets an interrupted close resume without duplicating comments or overriding later human changes.

Pull requests

Pull requests targeting main run CI Debug Tests from .github/workflows/ci-tests.yml.

  • The workflow resolves the requested ref to one exact commit SHA and every Debug job checks out that SHA.
  • Linux, Windows, and macOS Debug each build the complete registered test target set and run the complete, unfiltered CTest suite. Linux also runs repository policy tests.
  • Windows Debug initializes Visual Studio Hostx64/x64 explicitly, uses Ninja with MSVC and the x64-windows vcpkg triplet, and rejects MinGW, MSYS, or Strawberry toolchain selection.
  • macOS Debug remains on macos-26 with arm64 AppleClang, Ninja, Debug configuration, and the arm64-osx vcpkg triplet. The shared Homebrew prerequisite path installs ripgrep for fail-closed policy tests. Its test-result artifact retains the complete CTest inventory, canonical CTest log, JUnit report, failure CSV, compact result summary, optional failed/disabled-test records, and sccache diagnostics.
  • Debug jobs configure with BUILD_TESTING=ON and do not define NDEBUG, because several tests rely on assert().
  • The workflow uploads diagnostics only on failure. Diagnostics include command logs, CMake configure logs, CMakeCache.txt, CTest logs, vcpkg failure logs, and a concise environment summary.

Each Debug platform job also writes compact performance telemetry to the GitHub Step Summary and retains ci-telemetry-<platform>-debug.json in its normal test results artifact. The standard-library-only collector records comparable phase durations, cache outcomes, runner resources, disk evolution, relevant directory sizes and best-effort vcpkg and sccache figures. Cache restore and save phases implemented by GitHub Actions are measured by monotonic checkpoints immediately before and after the action; this is the safest available approximation because an action cannot be wrapped by the repository's command runner.

The Windows compiled vcpkg cache key includes the resolved MSVC toolset and Windows SDK versions from the same Hostx64/x64 environment used for dependency installation and compilation. A legacy restore prefix remains available only as a partial fallback, so rebuilding packages for a new hosted-runner toolchain can be saved under its new exact identity. The macOS key already incorporates both the Xcode version and resolved SDK identity.

The Debug CI workflow does not upload installers, AppImages, DMGs, or platform packages.

Merges into main

Main Patch Release Artifacts in .github/workflows/main-patch-test-build.yml runs after accepted non-bot pushes to main and can also be started manually.

The workflow serializes version mutation, increments only the patch component in VERSION, commits the bump with [skip version-bump], and passes the exact generated commit SHA to the three primary Release builders:

  • Windows Release installer.
  • Ubuntu Release AppImage.
  • Current macOS Release DMG.

It does not rerun Debug CI, and it does not build macOS 15 or Arch Linux compatibility packages. These artifacts are intended for manual testing and continuous verification of current hosted packaging runners. Automatic patch artifacts use GitHub Actions artifact retention and are not permanent release assets.

Release publication authentication

Dedicated-App mode is active. The patch bump-version and minor-release publish-release jobs mint a current-repository token for the Perastage Release Automation GitHub App using actions/create-github-app-token@v3. The App is installed only on PeramatoG/Perastage, requests only contents: write, and is the sole Integration with always bypass in the active Protect main ruleset.

Token creation is fail-closed: enabled App mode never falls back to GITHUB_TOKEN. Only the two protected-main publishers can mint the token; package builders, temporary-reference staging and cleanup, and recovery cannot access it. The manual validation workflow verifies the installation and permission scope without a Git write. See the main branch protection contract for the verified live ruleset, security rationale, and rollback procedure.

Weekly and manual compatibility packages

Weekly Compatibility Packages in .github/workflows/compatibility-builds.yml runs weekly at 03:27 UTC on Tuesday and supports workflow_dispatch with an optional source_ref.

Scheduled executions test the default branch. For a manual execution, an explicit source_ref overrides the ref selected in the GitHub Run workflow interface; when source_ref is empty, the workflow tests that selected ref. The chosen source is resolved once to an exact SHA before either secondary compatibility builder is invoked, and the workflow does not modify VERSION:

  • macOS 15 arm64 Release DMG.
  • Arch Linux x64 Release package.

A newer compatibility run for the same explicit or selected ref cancels an older in-progress compatibility run. The individual macOS 15 and Arch builder workflows remain manually runnable when a maintainer needs to reproduce one package.

Minor draft releases

Minor Draft Release in .github/workflows/minor-draft-release.yml is manual. It keeps the existing release title, prerelease, and dry-run inputs.

The release pipeline is staged:

  1. Resolve current main to an exact base SHA, validate VERSION, compute the next MAJOR.MINOR.0 version and tag, verify the tag does not already exist, and validate the artifact contract.
  2. Create a temporary automation ref based on the resolved base SHA and commit only the VERSION update there. Debug CI is diagnostic while the historical suite is being repaired, so the minor release package builders do not wait for or depend on Debug test success.
  3. Build all five Release packages from the staged release commit SHA: Windows, Ubuntu AppImage, macOS 15, current macOS, and Arch Linux. Package builder failures remain blocking.
  4. Download and validate all package and symbol artifacts, reject duplicates or stale filenames, assemble the unified developer-only debug-symbol archive from the actual platform symbol files, and create the final validated release-assets artifact. Asset validation failures remain blocking.
  5. Publish only after validation succeeds by verifying origin/main still equals the base SHA, fast-forwarding main to the staged release commit, creating the annotated tag at that commit, and creating a draft GitHub Release with the validated assets.

The release remains transactional: no tag, main update, or draft GitHub Release is created until every required package and asset validation succeeds. If main changes while packages are building, publication fails clearly before the tag or draft release is created. The maintainer should rerun the workflow from the updated main. Temporary automation refs are deleted after success and by the cleanup job after failure.

Dry runs are inexpensive: they compute and report the version, tag, title, and artifact contract status without pushing a branch, committing, tagging, building packages, or creating a release.

Stable artifact contract

The package filename patterns are centralized in .github/release-artifact-contract.json and validated by tests/check_ci_workflow_architecture.py.

Stable package patterns:

  • Perastage_*_Setup.exe
  • Perastage-*-x86_64.AppImage
  • Perastage-*-macOS15-arm64.dmg
  • Perastage-*-macOS26-arm64.dmg
  • Perastage-*-arch-x86_64.pkg.tar.zst

Stable artifact names:

  • Perastage-windows-installer
  • Perastage-linux-appimage
  • Perastage-macos15-dmg
  • Perastage-macos26-dmg
  • Perastage-arch-package
  • Perastage-windows-symbols
  • Perastage-linux-symbols
  • Perastage-macos15-symbols
  • Perastage-macos26-symbols
  • Perastage-archlinux-symbols

Builder workflows must preserve the package filenames and symbol artifact structures unless the producer and collector contract is updated and tested in the same change.

Minor draft release publication and recovery

The Minor Draft Release workflow publishes Git state only after all installer builders and final asset validation have succeeded. The publisher configures the repository-local Git identity as github-actions[bot] before creating annotated tags so fresh GitHub-hosted checkouts can run git tag -a without depending on machine-global configuration.

Normal publication is intentionally transactional on the first publish attempt. It fetches main and tags, verifies that origin/main still equals the resolved base SHA, creates the annotated release tag locally at the validated release SHA, and pushes main plus the tag with one git push --atomic command. There is no non-atomic fallback; if the server rejects the atomic update, the workflow fails without intentionally publishing only half of the Git state.

The normal publisher is retry-safe for the known safe states:

  • origin/main is still the base SHA and the tag is absent: create the annotated tag and atomically push main and the tag.
  • origin/main is already the release SHA and the tag peels to that same release SHA: treat Git publication as complete and continue to draft release creation.
  • origin/main is already the release SHA and the tag is absent: treat this as the legacy partial state from the v1.5.0 publication failure, create the exact annotated tag, and push only the tag.

Any other main SHA, any tag that peels to a different commit, or any situation requiring a force push is rejected. Tag checks use the peeled commit target (tag^{commit}), not the tag object's SHA, because release tags are annotated.

Draft release creation is also idempotent. The publisher checks gh release view before creating a release. If no release exists, it creates a draft and attaches only the public final assets. If a matching draft exists, it updates the title and prerelease state and reuploads assets with --clobber, retaining the existing notes. The recovery workflow also refreshes notes from the validated release commit. If a non-draft release exists, publication stops rather than editing an already-published release or creating a duplicate.

Recovering a validated but unpublished minor release

Use Recover Validated Minor Release only when a Minor Draft Release run completed all builders and validate-release-assets but failed during publication. The recovery workflow is manual-only, never calculates a new version from current main, never updates main, and never rebuilds installers in the normal recovery path. It downloads only the Perastage-validated-release-assets artifact from the supplied source run and places it under release-assets/final.

For the v1.5.0 incident, rerunning the normal minor workflow after merge would read VERSION as 1.5.0 on main and calculate 1.6.0, so recovery must be used instead. Run the recovery workflow with:

  • source_run_id=<SOURCE_RUN_ID> from the failed workflow URL.
  • release_sha=c857665b99aacf9f466edd4416584dfb56ac1a1f.
  • release_version=1.5.0.
  • dry_run=true first.

After the dry run validates the source run, commit identity, asset names, checksums, tag state, release state, and release notes source, run the same workflow inputs again with dry_run=false. Confirm afterwards that v1.5.0 points to c857665b99aacf9f466edd4416584dfb56ac1a1f, the GitHub Release exists as a draft, all six final public assets are attached, and main was not moved by recovery.

Validated artifact provenance

Perastage-validated-release-assets workflow artifacts include an internal release-provenance.json file. This file records the repository, workflow run ID and attempt, base SHA, release SHA, release version, tag, timestamp, final asset filenames, and SHA-256 checksum for each final asset. The provenance file is retained inside the workflow artifact to support safe recovery, but it is excluded from public GitHub Release uploads unless the artifact contract is explicitly changed to publish it.

The v1.5.0 artifact predates provenance. The validator permits exactly one legacy missing-provenance exception for release SHA c857665b99aacf9f466edd4416584dfb56ac1a1f and release version 1.5.0; other SHA/version pairs must include matching provenance.

CI Debug sccache policy

ci-tests.yml is the sole sccache owner. Its Linux, Hostx64/x64 Windows, and ARM64 macOS Debug jobs install dependencies first and then use the pinned Mozilla action v0.0.10 commit 9e7fa8a12102821edf02ca5dbea1acd0f89a2696 to install sccache v0.15.0. Installer, packaging, release, and vcpkg Binary Cache workflows do not enable it. The native GHA backend uses perastage-ci-debug-v1 plus stable runner, architecture, compiler/toolset, Debug, and macOS SDK boundaries; SCCACHE_BASEDIRS normalizes the absolute checkout root.

Pull requests use GitHub's native merge-ref cache scope, so their writes support PR reruns but cannot be consumed by main or sibling pull requests; compatible default-branch entries remain restorable by PR jobs. Every push to main starts a trusted cross-platform cache warmer because PR merge-ref writes do not reliably seed later PRs. The warmer can publish to gha-main only when its checked-out SHA exactly matches freshly fetched origin/main and no source override is present. A trusted manual current-main dispatch retains the same default-branch scope. Workflow calls, arbitrary manual sources, and ambiguous combinations disable GHA and use an unpersisted disk-ephemeral cache. The unsupported SCCACHE_GHA_RW_MODE variable is not used. Summaries and artifacts retain credential-safe statistics and warning-level error logs, never runtime tokens. Remote I/O is fail-open to compilation, while a missing launcher, wrong compiler, malformed JSON, or zero requests after a successful build fails the job. A generated bracket-quoted CMake initial cache transports the complete launcher path, including Windows sccache.exe; macOS also exports its compile database. An earlier configure/build failure remains authoritative while the always-run statistics step records diagnostics. Windows Debug alone selects CMP0141 NEW and embedded Z7 object debug information for cache compatibility.

The trusted push path performs checkout and exact-source validation, restores dependencies, validates the toolchain and compile commands, configures with BUILD_TESTING=ON, builds the complete Debug all target, and records sccache statistics. It skips only CTest execution and test-result artifact upload; test inventory generation, repository policy checks, and failure diagnostics remain active. Pull requests, manual dispatches, and reusable workflow calls continue to execute and upload the same CTest results as before. The step summary identifies whether a run is cache warming or a normal test run.

To verify reuse, inspect each platform's sccache step summary and compare requests, hits, misses, writes, errors, compatibility identity, and build-step duration against a trusted main run. Establish performance from current GitHub Actions measurements rather than a stored point-in-time snapshot. CTest results are never cached; compare build duration separately from test duration.

Back to top