โš ๏ธ Work in Progress
You are viewing development documentation built from the latest commit on main. APIs and features are subject to change.

GitHub Workflows

GitHub Workflows Documentation

This document provides an overview of all GitHub Actions workflows used in the kure project.

Last Updated: 2026-08-21


Workflow Summary

WorkflowFileTriggersPurpose
CIci.ymlpush, PR, schedule, manualComprehensive testing, linting, building, security
Deploy Docsdeploy-docs.ymlpush to main (docs paths), workflow_dispatchMulti-version docs deployment
Manage Docsmanage-docs.ymlworkflow_dispatchRemove, rebuild, or re-point doc versions
Release / Createrelease-create.ymlmanualAuto-infer release type from VERSION, create tag
Release / Promoterelease-promote.ymlmanualPromote to explicit release type (beta/rc/stable)
Release / Bumprelease-bump.ymlmanualAdvance version cycle (minor/major/prerelease), no tag
Release / Publishrelease-publish.ymltag pushGoReleaser, SBOM, cosign signing, docs deploy, proxy refresh
PR Reviewpr-review.ymlpull_request, merge_groupTwo-pass AI code review via claude-max-proxy

CI Workflow

File: .github/workflows/ci.yml Name: CI

Triggers

  • Push to: main, develop, release/*
  • Pull requests to: main, develop, on GitHub’s default types opened, synchronize, reopened
  • Merge group (merge queue’s temporary branch โ€” required checks must report here)
  • Schedule: 4am UTC daily (catch external changes)
  • Manual dispatch

Every job runs on draft PRs the same as ready ones (2026-08-19, GitLab mr-review parity โ€” see Draft PRs ), so ready_for_review is not declared: it would only re-trigger a suite that already ran.

Concurrency

Uses github.ref to cancel superseded runs on the same branch or PR:

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

Job Dependency Graph

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   lint          โ”‚  โ† Fast checks: go-version, fmt, tidy, vet, lint
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚
    โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”
    โ–ผ         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ test  โ”‚ โ”‚ security  โ”‚  โ† Tests + govulncheck (parallel)
โ””โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    โ”‚
    โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ coverage-check    โ”‚   โ”‚ forbidden-terms   โ”‚  โ† Unconditional full-tree policy guard
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                       โ–ผ
                 โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                 โ”‚ build โ”‚  โ† Aggregation gate
                 โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

PR-only jobs (parallel, no blocking):
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ analyze-changes โ”‚  โ”‚ docs-check โ”‚  โ”‚ pin-impact  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

pin-impact is PR-only like docs-check in the diagram above, but โ€” like action-pins/ forbidden-terms/doc-gate โ€” it does feed the build aggregation gate (see the Jobs Detail table); the diagram groups it with the other PR-only jobs for layout only, not to say it’s unblocking.

On merge_group events (merge queue), lint/test/build run against the queue’s temporary branch โ€” the merged result โ€” before the PR is allowed to land.

Jobs Detail

JobCheck NameTimeoutDependenciesPurpose
validatelint15 minchangesGo fmt, tidy, vet, lint, tool-version parity (golangci-lint pin across Makefile/ci.yml/docs), govulncheck doc parity; sync-versions.sh check; scripts/gen-builders.sh check (fails when the generated constructor wrappers under pkg/kubernetes are stale against the registered scheme); syntax-check on the version-sync scripts, sh -n or bash -n per each script’s own shebang; scripts/test/run-tests.sh (hermetic mutation-matrix guard tests for sync-versions.sh’s own six guards, no network); caches goimports + yq binaries
action-pinsaction-pins2 minโ€”Fails if any third-party uses: ref is not pinned to a 40-char commit SHA (go-kure/.github canonical checker)
forbidden-termsforbidden-terms2 minโ€”Runs the canonical full-tree downstream-reference guard on every workflow event and verifies the vendored release guard
testtest20 minchangesUnit tests with race detection and coverage; -race compilation takes ~5 min on the in-cluster runner, so 20 min allows compilation + 15 min for test execution
securitySecurity15 minchangesgovulncheck (-scan symbol, v1.7.0), gated on reachable advisories via the canonical govulncheck-gate action from go-kure/.github โ€” blocking, not informational
coverage-checkCoverage Check5 mintestTwo separate gates โ€” 90% total coverage, and 90% on each individual package โ€” plus Codecov upload and PR comment
buildbuild1 minvalidate, test, docs-build, coverage-check, doc-gate, action-pins, forbidden-terms, security, pin-impactAggregation gate โ€” fails if any required job failed; forbidden-terms must report success and may not be skipped
analyze-changesAnalyze Changes5 min-Changed files analysis, breaking change warnings (PR only)
docs-builddocs-build15 minchangesHugo build; separate Go + Hugo caches; validates the docs map and rendered internal links via the canonical check-doc-sync/check-links actions from go-kure/.github
docs-checkDocs Check5 minchangesAPI changes need docs check (PR only); runs the canonical check-doc-gate action from go-kure/.github (job id: doc-gate)
pin-impactpin-impact3 minโ€”PR only; resolves every go-kure/.github action kure’s workflows reference to the scripts/*.sh (and one transitive source) each runs, compares base vs. head, and fails if the pin bump touched a path kure actually executes โ€” vendored scripts/check-pin-impact.sh (not a canonical action: it must run at the SHA it’s vetting, not the SHA a bump would move it to)

Configuration

  • Go Version: read from go.mod (go-version-file: go.mod)
  • Golangci-lint Version: v2.13.2
  • govulncheck Version: v1.7.0 (pinned, cached binary, -scan symbol mode)
  • Coverage Threshold (total): 90% โ€” the overall figure from go tool cover -func
  • Coverage Threshold (per-package): 90% โ€” checked separately for every package, and a single package below it fails the job even when the total passes. Packages whose import path contains /examples/ are exempt.

Features

  • gotestfmt - Nice formatted test output
  • Fail fast - Jobs depend on validate, so lint failure stops everything
  • Artifact sharing - Coverage is uploaded as an artifact and reused by coverage-check; upload, download, missing-file, and invalid-profile failures are blocking so the coverage gates cannot pass without valid data
  • PR comments - Coverage report comment on PRs
  • Runs on draft PRs - no draft gate on any job (see below )
  • Sensitive file check - Print at most ten potential matches and emit one warning only when matches exist; this check remains informational and does not block CI
  • goimports - Installed as a tool dependency for the formatting check (goimports -l)
  • Matrix fail-fast: false - Cross-platform builds continue if one fails
  • Doc-sync checks - docs-build and docs-check (doc-gate job) run the canonical check-doc-sync, check-links and check-doc-gate actions from go-kure/.github; kure no longer vendors its own copies under site/scripts/
  • Downstream-reference guard - the unconditional forbidden-terms job scans the complete tracked tree and keeps the release script’s vendored guard byte-identical to the pinned canonical action
  • Pin-impact gate - pin-impact renders a go-kure/.github pin bump’s real effect (which scripts/*.sh a referenced action actually runs, whether the compare touches any of them) into the job summary and fails on a match, so a bump touching consumed code cannot merge unreviewed (go-kure/kure#719, 2026-08-30). A maintainer who has reviewed a real hit and judged it safe adds the pin-impact-ack label to merge anyway โ€” same convention as check-doc-gate’s docs-skip label; there is no other override. Rerun gotcha: the strip-ack step only runs when the triggering event’s action was synchronize or reopened; re-running a stale/failed run of one of those (gh run rerun, or the Actions UI) replays that same original action and silently strips a freshly-added pin-impact-ack again before the gate re-checks it, even though nothing was pushed. A rerun of an opened/labeled/unlabeled-triggered run is unaffected โ€” strip-ack skips it either way. Scoped to same-repo PRs: strip-ack’s if: also requires the PR’s head repo to equal this repo, so it never runs at all on a fork PR โ€” but that’s moot, since the gate separately forces PIN_IMPACT_ACK=false unconditionally on forks; a fork PR has no acknowledgment path regardless of labels. Add (or re-add) the label rather than rerunning, on a same-repo PR; full writeup in go-kure/.github’s docs/standards.md ยง “Pin-impact-ack”

Draft PRs

No job carries a draft == false condition (removed 2026-08-19 for parity with the downstream GitLab CI template this workflow was ported from, which reviews/tests every merge-request pipeline regardless of draft status). A draft PR gets the identical lint/test/Security/coverage-check/build run as a ready one; draft blocks merge only, via branch protection โ€” it does not change what CI runs.

One retargeting case is still not covered, because GitHub sends neither synchronize nor any type in this workflow’s list for it:

SituationEvent GitHub sendsRemedy
PR retargeted to another base branchedited (with changes.base)close and reopen the PR, which sends reopened
PR title or body editededitednone needed โ€” no code changed

edited is deliberately not in the type list: it fires on every title and body edit, which would run the full suite for text-only changes. A retarget is rare enough to handle by hand.


Release / Create Workflow

File: .github/workflows/release-create.yml Name: Release / Create

Triggers

  • Manual dispatch with input: dry_run (boolean)

How It Works

The release type is auto-inferred from the VERSION file by release.sh. No type or scope inputs are needed. The regression guard in release.sh blocks invalid transitions automatically.

Pre-release Test Gate

The workflow runs a full test suite (with race detection) before creating the tag. This prevents tags from being pushed when tests fail.

workflow_dispatch
  โ†’ test job (go test -race ./...)
    โ†’ release job (needs: test)
      โ†’ release.sh (auto-infer type from VERSION) โ†’ creates tag + pushes
        โ†’ triggers release-publish.yml (tag push)

If the pre-release test fails, the release job never runs and no tag is created.

Jobs

  1. test โ€” Full test run with race detection and CGO enabled (build-essential + CGO_ENABLED=1)
  2. release โ€” Runs scripts/release.sh to generate changelog, commit, create tag, and push

Authentication

Uses a GitHub App token (RELEASE_APP_ID + RELEASE_APP_PRIVATE_KEY) so that the tag push triggers subsequent workflows (tag-triggered release-publish.yml).

Usage

# Preview release (auto-infer from VERSION)
./scripts/release-trigger.sh

# Create release via CI:
#   Actions > "Release / Create" > Run workflow (type is auto-inferred)

Release / Promote Workflow

File: .github/workflows/release-promote.yml Name: Release / Promote

Triggers

  • Manual dispatch with inputs: to (beta/rc/stable) and dry_run

Purpose

Explicit type transition (e.g., beta โ†’ rc). The regression guard in release.sh blocks invalid downgrade transitions (e.g., rc โ†’ beta will fail with an error).

Usage

# Preview promotion to rc
./scripts/release-trigger.sh promote rc

# Execute via CI:
#   Actions > "Release / Promote" > to=rc > Run workflow
./scripts/release-trigger.sh promote rc --do-it

Release / Bump Workflow

File: .github/workflows/release-bump.yml Name: Release / Bump

Triggers

  • Manual dispatch with inputs: scope (minor/major/prerelease) and dry_run

Purpose

Advance the version cycle without creating a tag. Use before starting a new prerelease cycle (e.g., after a stable release, to begin the next minor version’s alpha).

Usage

# Preview minor version bump
./scripts/release-trigger.sh bump minor

# Execute via CI:
#   Actions > "Release / Bump" > scope=minor > Run workflow
./scripts/release-trigger.sh bump minor --do-it

Release / Publish Workflow

File: .github/workflows/release-publish.yml Name: Release / Publish

Triggers

  • Push tags: v* (e.g., v1.0.0, v0.1.0-beta.2)

Jobs

  1. test - Full test run with race detection
  2. validate - Strict tag format, changelog, and version progression validation
  3. goreleaser - Build release artifacts, generate SBOM, sign with cosign
  4. deploy-docs - Trigger versioned docs deployment (stable tags only)
  5. post-release - Go proxy refresh

Configuration

  • Go Version: read from mise.toml
  • Tag Format: ^v[0-9]+\.[0-9]+\.[0-9]+(-alpha\.[0-9]+|-beta\.[0-9]+|-rc\.[0-9]+)?$
  • Changelog: Required (must have ## [0.1.0] section โ€” version without v prefix, in square brackets)

Release Management

# Preview release (auto-infer from VERSION)
./scripts/release-trigger.sh

# Preview type promotion
./scripts/release-trigger.sh promote rc

# Preview version bump
./scripts/release-trigger.sh bump minor

# Execute via CI (add --do-it to any of the above)
./scripts/release-trigger.sh --do-it
./scripts/release-trigger.sh promote rc --do-it
./scripts/release-trigger.sh bump minor --do-it

Merge Queue

kure merges through GitHub’s native merge queue (configured in the main-protection ruleset, not a workflow file). This replaced the former rebase-check job and auto-rebase.yml workflow โ€” it is the native equivalent of GitLab’s merged-results pipelines.

How It Works

  1. A reviewed PR is added to the queue (“Merge when ready”).
  2. The queue creates a temporary branch combining main + the PR and fires a merge_group event; lint/test/build run against that merged result.
  3. If green, the PR lands on main with the rebase merge method (linear history preserved). If the merged result fails, the PR is dropped from the queue and main stays green.

Why

  • Tests the actual merged result, which rebase-check (ancestry-only) could not.
  • No force-pushing contributor branches and no per-merge auto-rebase storm โ€” the queue rebases once, at merge time.

Configuration (ruleset merge_queue rule)

  • Merge method: REBASE (linear history)
  • Grouping: ALLGREEN (a failing entry is dropped from the group)
  • Batch size: 1 (conservative; tune after observing runner load)
  • Required checks on the queue: lint, test, build (must also trigger on merge_group)

Auto-merge is not enabled โ€” every PR is reviewed and queued manually. The merge queue rule is managed centrally in go-kure/.github (governance/repository-settings-policy.yaml).


PR Review Workflow

File: .github/workflows/pr-review.yml Name: PR Review

Triggers

  • Pull requests: opened, synchronize, reopened, on GitHub’s default types
  • merge_group (no filters): required so this check reports on the merge queue’s temporary ref once it becomes a required status check โ€” the queue payload has no pull_request field, so the existing fork skip below evaluates false and the job reports skipped/success as a no-op
  • Runs on draft PRs the same as ready ones (2026-08-19, GitLab mr-review parity โ€” see Draft PRs ); skips fork PRs (self-hosted runner security)
  • ready_for_review is not declared, same reasoning as ci.yml: it was kept as a rollout-window safety net while the callee (pr-review.yml@main, in go-kure/.github) still gated on draft == false (its own parity fix landed 2026-08-19, 46dfc88) and dropped once that window closed.

How It Works

Uses a two-pass AI review system via the in-cluster claude-max-proxy (ported from the GitLab mr-review.yml template):

  1. Pass 1 โ€” Review: Sends the PR diff + project context (AGENTS.md, .claude/CLAUDE.md) to the review model (default: claude-opus-4). Anti-hallucination rules prevent the model from inventing standards or referencing code not in the diff. The model returns up to 3 findings ranked by severity in a structured table. Posted as a PR comment.

  2. Pass 2 โ€” Assessment: If the review found issues (not LGTM), sends the review + diff to an assessment model (default: claude-sonnet-4-6) which fact-checks each finding against the actual diff and project context. Includes standards verification โ€” claims about “standards violations” are checked against actually-provided standards. Catches hallucinations and false positives. Posted as a second PR comment.

Requirements

  • Self-hosted runner: Runs on autops-kube label (ARC runner with in-cluster access)
  • claude-max-proxy: Reachable at http://openclaw-claude-proxy.openclaw.svc:3456 from the runner pod
  • No API keys needed: the proxy handles model authentication

Configuration

Configurable via repository variables or workflow env defaults:

VariableDefaultPurpose
PR_REVIEW_MODELclaude-opus-4Model for code review pass (cosmetic label; backend ignores the model field)
PR_REVIEW_MAX_DIFF_CHARS50000Truncation threshold for large diffs
PR_REVIEW_MAX_TOKENS1500Max response tokens for review
PR_REVIEW_CONTEXTkure project descriptionAdditional system prompt context
PR_REVIEW_ASSESS_ENABLEDtrueEnable/disable assessment pass
PR_REVIEW_ASSESS_MODELclaude-sonnet-4-6Model for hallucination checking
PR_REVIEW_ASSESS_MAX_TOKENS4096Max response tokens for assessment
PR_REVIEW_AGENTS_FILEAGENTS.mdProject context file path

Non-Blocking

The workflow uses continue-on-error: true so review failures never block PR merges.


Deploy Docs Workflow

File: .github/workflows/deploy-docs.yml Name: Deploy Docs

Triggers

  • Push to main (paths: site/**, docs/**, pkg/**/*.md, examples/**/*.md, README.md, CHANGELOG.md, DEVELOPMENT.md)
  • Manual dispatch with inputs: version_slot, version_label, set_latest

How It Works

  1. Runs scripts/gen-versions-toml.sh to generate a versioned Hugo config overlay
  2. Builds the Hugo site with --config hugo.toml,versions.toml
  3. Deploys the built site to a subdirectory of go-kure.github.io

Trigger Matrix

EventWhat DeploysPathBaseURL
Push to main (docs paths)Dev docs/dev/www.gokure.dev/dev/
workflow_dispatchVersioned/vX.Y/www.gokure.dev/vX.Y/
workflow_dispatch + set_latest=trueVersioned + root/vX.Y/ + /Both

Concurrency

Per-slot concurrency group (deploy-docs-<slot>) prevents race conditions when deploying different versions simultaneously.

Preservation

During deployment, existing version subdirectories (dev/, v*/), CNAME, and .nojekyll are preserved. Only the target slot is replaced.


Manage Docs Workflow

File: .github/workflows/manage-docs.yml Name: Manage Docs

Triggers

  • Manual dispatch only with inputs: action, version_slot

Actions

ActionDescriptionImplementation
remove-versionDelete a version’s docsRemoves /vX.Y/ directory from deploy target
set-latestChange root / to a specific versionTriggers deploy-docs.yml with set_latest=true
rebuild-versionRe-trigger a docs buildTriggers deploy-docs.yml for the specified version

Common Scenarios

# Roll back latest to an older version:
#   Actions > "Manage Docs" > action=set-latest > version_slot=v0.1

# Remove a yanked version:
#   Actions > "Manage Docs" > action=remove-version > version_slot=v0.2

# Rebuild after theme or script changes:
#   Actions > "Manage Docs" > action=rebuild-version > version_slot=dev

Versioned Documentation

The docs site supports multiple documentation versions at different URL paths.

URL Structure

PathContentUpdated By
/Latest stable releaseRelease workflow (set_latest=true)
/vX.Y/Specific stable versionRelease workflow or manual dispatch
/dev/Development (from main)Every push to main that touches docs

Version Switcher

The Relearn theme provides a native version dropdown in the sidebar. It is configured via params.versions entries in versions.toml, which gen-versions-toml.sh generates from git tags.

How gen-versions-toml.sh Works

# Generate config overlay for a dev build:
./scripts/gen-versions-toml.sh --version dev

# Generate for a stable release:
./scripts/gen-versions-toml.sh --version v0.1.0 --latest v0.1.0

The script:

  1. Reads all stable tags (vX.Y.Z without pre-release suffix) from git
  2. Deduplicates to minor level (keeps highest patch per vX.Y)
  3. Generates site/versions.toml with [params] section and [[params.versions]] entries
  4. Marks the latest version with isLatest = true and root baseURL
  5. Always includes a “Development” entry pointing to /dev/

WIP Banner

The development version shows a warning banner linking to the latest stable version (if one exists). Stable versions show no banner.


Test Jobs in CI

JobMatrixCommandUses Makefile?
test-go test -json -v ./...โœ… (deps)
test-make test-raceโœ…
test-make test-coverageโœ…

Test Targets in Makefile

TargetCommandUsed in CI?In precommit?
testgo test -timeout 30s ./...โœ…โœ…
test-racego test -race -timeout 30s ./...โœ…-
test-coveragego test -coverprofile=... ./...โœ…-
test-integrationgo test -tags=integration -timeout 5m ./...--
vulngovulncheck ./...โœ…-
versions-testbash scripts/test/run-tests.sh- (CI runs the script directly, not via make)โœ…

CI vs Pre-commit

TargetTasksUse Case
precommitfmt, tidy, lint, test, check-tool-versions, check-govulncheck-docs, versions-testFast local checks (~10s)
cideps, fmt, tidy, lint, vet, test, test-race, test-coverage, test-integration, vuln, check-tool-versions, check-govulncheck-docs, versions-testComprehensive CI pipeline (~2min)

Configuration Standards

Go Version

All jobs use go-version-file: go.mod โ€” the go directive in go.mod is the single source of truth (kept in sync with mise.toml via make check-go-version).

yq and lychee Versions

Both are read from mise.toml at run time by a dedicated step, the same shape as the Hugo version read in deploy-docs.yml:

- name: Read yq version from mise.toml
  id: yq-version
  run: |
    YQ_VER=$(grep '^yq = ' mise.toml | sed 's/yq = "\(.*\)"/\1/')
    if [ -z "$YQ_VER" ]; then
      echo "::error::Failed to parse yq version from mise.toml"
      exit 1
    fi
    echo "version=$YQ_VER" >> $GITHUB_OUTPUT

${{ steps.yq-version.outputs.version }} (or steps.lychee-version... for lychee) feeds both the cache key and the download URL everywhere the version used to be hardcoded โ€” three yq sites plus one lychee site in ci.yml, one yq site in deploy-docs.yml. Bumping mise.toml’s yq/lychee pin is sufficient on its own; no other file needs a manual update, and no tool-version-parity checker needs to know about either (there is nothing left in ci.yml/deploy-docs.yml for one to compare against). lychee previously had no mise.toml entry at all โ€” its CI-pinned version was the only copy anywhere; it now has one, matching the parity yq already had before this section was written.

Caching

CI jobs use explicit actions/cache steps with cache: false on setup-go to control cache keys precisely. Two Go caches are maintained.

Module cache โ€” dependency-only, one combined step per Go job:

- name: Cache Go modules
  uses: actions/cache@v6
  with:
    path: ~/go/pkg/mod
    key: ${{ runner.os }}-gomod-${{ hashFiles('**/go.sum') }}
    restore-keys: |
      ${{ runner.os }}-gomod-

Go build cache (~/.cache/go-build) โ€” split actions/cache/restore + actions/cache/save so the log shows exact vs fallback restore (cache-matched-key). The key is source-aware (was go.sum-only, which froze the cache at an old snapshot and never refreshed as source changed) and split by job purpose so validate (non-race) and test (race+coverage) never overwrite each other’s entry:

- name: Restore Go build cache
  id: gocache
  uses: actions/cache/restore@v6
  with:
    path: ~/.cache/go-build
    key: ${{ runner.os }}-${{ runner.arch }}-go-<GOVER>-gocache-<purpose>-deps-<go.sum hash>-src-<source hash>
    restore-keys: |
      ${{ runner.os }}-${{ runner.arch }}-go-<GOVER>-gocache-<purpose>-deps-<go.sum hash>-src-
      ${{ runner.os }}-${{ runner.arch }}-go-<GOVER>-gocache-<purpose>-
# ... compile / test ...
- name: Save Go build cache
  if: success() && steps.gocache.outputs.cache-hit != 'true'
  uses: actions/cache/save@v6
  with:
    path: ~/.cache/go-build
    key: ${{ steps.gocache.outputs.cache-primary-key }}

Purpose prefixes: gocache-validate-, gocache-test-race-cover-, gocache-security-. <GOVER> comes from a Read Go version from go.mod step. The source hash covers **/*.go, go.mod, go.sum, Makefile, **/testdata/**. Save runs only on a non-exact (fallback/miss) restore and only when the run succeeded, so a broken build never publishes a cache.

Cross-ref scoping. GitHub caches are ref-scoped: a merge_group (queue) run cannot restore a pull_request run’s cache โ€” only the default branch (main) is shared. So these caches are warmed by push-to-main and restored by PR + queue via restore-key fallback. This lowers both runs' absolute cost but does not deduplicate the PRโ†”queue build (inherent to the merge queue). Measured in launcher: warmed cycles cut test ~50%, build/lint ~30%.

Tool binaries are also cached to avoid reinstalling on every run:

  • goimports โ€” keyed by go.sum hash (tied to golang.org/x/tools version)
  • yq, lychee โ€” keyed by the version a “Read <tool> version from mise.toml” step reads at run time (see yq and lychee Versions below), never a hardcoded literal
  • govulncheck โ€” keyed by pinned version (v1.7.0)

Cache and artifact traffic is routed through an in-cluster falcondev cache server backed by Garage S3. Two layers work together:

  1. Binary patch (containers/actions-runner/Dockerfile in opsmaster): Runner.Worker.dll is patched to read CUSTOM_ACTIONS_RESULTS_URL instead of ACTIONS_RESULTS_URL for its own internal connection. CUSTOM_ACTIONS_RESULTS_URL is set as a pod env var in the runner’s HelmRelease. This ensures the Worker process itself connects through the cache server.

  2. Workflow env (ACTIONS_RESULTS_URL): The binary patch replaces all UTF-16LE occurrences of ACTIONS_RESULTS_URL in the DLL โ€” including the name the Worker injects into step process environments (renamed to ACTIONS_RESULTS_ORL as a side effect). Setting ACTIONS_RESULTS_URL in the workflow env: block overrides this so step processes (upload-artifact, download-artifact, actions/cache v2) see the correct URL.

ACTIONS_CACHE_URL / ACTIONS_CACHE_SERVICE_V2 are not needed โ€” cache actions use the v2 Results API path through ACTIONS_RESULTS_URL.

docs-build Caching

The docs-build job uses two separate caches:

  • gomod โ€” Go module cache
  • hugo โ€” Hugo module cache ($HUGO_CACHEDIR only, not ~/go/pkg/mod)

Path Filters

The changes job uses dorny/paths-filter to skip jobs when unrelated files change:

  • go: filter โ€” triggers lint/test/security/build jobs. Includes **.go, go.mod, go.sum, Makefile, and .github/workflows/** so that workflow-only PRs are also validated, plus versions.yaml, docs/compatibility.md, scripts/sync-versions.sh, scripts/test/** and scripts/sync-eso-pin.sh. Those last five are here because the only sync-versions.sh check invocation lives in the validate job: without them a PR touching just version metadata, sync-versions.sh’s own guard-test harness (scripts/test/** โ€” a case file or the harness itself, the exact changes it exists to enforce CI coverage of), or the release-pinning script skipped the supported-range guard, the compatibility-matrix drift guard, and/or the “Run sync-versions.sh guard tests” step (or, for sync-eso-pin.sh, all of validate/test) and still reported success โ€” the build gate accepts a skipped dependency as passing. Also includes mise.toml, scripts/check-tool-versions.sh, scripts/sync-tool-versions.sh and this file, for the same reason: check-tool-versions also runs only in the validate job, and a PR touching only one of those would otherwise skip the golangci-lint pin-parity guard. Same reasoning covers scripts/check-govulncheck-docs.sh and scripts/sync-govulncheck-docs.sh โ€” check-govulncheck-docs also runs only in validate. Same reasoning covers scripts/sync-go-version.sh too โ€” check-go-version (Go-version parity between mise.toml and go.mod) also runs only in validate. And scripts/gen-builders.sh: the generated-builders check (scripts/gen-builders.sh check) also runs only in validate, and Renovate invokes the same script after Go module bumps. pkg/**/testdata/** is there for the same reason: Go testdata is test input, and pkg/** alone matches only the docs filter, so a PR editing just the admission exclusion list would skip the tests that check it.
  • docs: filter โ€” triggers docs-build/docs-check jobs. Includes site/**, docs/**, *.md, scripts/**, and .github/workflows/ci.yml (only ci.yml, since other workflows don’t affect the docs build).

Branch Patterns

  • Release branches: release/* (note: not releases/*)
  • Development branches: main, develop

Estimated CI Time

ScenarioBefore (4 workflows)After (2 workflows)
PR opened~8 min (duplicate work)~4 min
Push to main~5 min~4 min
PR merge~5 min (full re-run)~0 min (same SHA, skipped)

Self-Hosted Runner Requirements

All jobs run on the autops-kube-kure GitHub ARC scale-set, which uses a custom runner image (ghcr.io/ginsys/opsmaster/actions-runner:latest) โ€” a minimal Ubuntu image that includes curl and git but not make or wget.

To account for this:

  • Every job that calls make includes an explicit install step: sudo apt-get install -y --no-install-recommends make
  • All wget calls have been replaced with curl -fsSL -o

Maintenance Notes

  • When adding/modifying workflows: Update this document with changes
  • Version updates: Run make sync-go-version to update Go version in all files
  • Version check: Run make check-go-version to verify consistency
  • Action versions: Keep GitHub Actions up to date (currently using v3-v6)
  • New jobs using make: Add the Install build tools step (see above) if the job runs on autops-kube

See Also