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
| Workflow | File | Triggers | Purpose |
|---|---|---|---|
| CI | ci.yml | push, PR, schedule, manual | Comprehensive testing, linting, building, security |
| Deploy Docs | deploy-docs.yml | push to main (docs paths), workflow_dispatch | Multi-version docs deployment |
| Manage Docs | manage-docs.yml | workflow_dispatch | Remove, rebuild, or re-point doc versions |
| Release / Create | release-create.yml | manual | Auto-infer release type from VERSION, create tag |
| Release / Promote | release-promote.yml | manual | Promote to explicit release type (beta/rc/stable) |
| Release / Bump | release-bump.yml | manual | Advance version cycle (minor/major/prerelease), no tag |
| Release / Publish | release-publish.yml | tag push | GoReleaser, SBOM, cosign signing, docs deploy, proxy refresh |
| PR Review | pr-review.yml | pull_request, merge_group | Two-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 typesopened,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: trueJob 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
| Job | Check Name | Timeout | Dependencies | Purpose |
|---|---|---|---|---|
validate | lint | 15 min | changes | Go 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-pins | action-pins | 2 min | โ | Fails if any third-party uses: ref is not pinned to a 40-char commit SHA (go-kure/.github canonical checker) |
forbidden-terms | forbidden-terms | 2 min | โ | Runs the canonical full-tree downstream-reference guard on every workflow event and verifies the vendored release guard |
test | test | 20 min | changes | Unit 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 |
security | Security | 15 min | changes | govulncheck (-scan symbol, v1.7.0), gated on reachable advisories via the canonical govulncheck-gate action from go-kure/.github โ blocking, not informational |
coverage-check | Coverage Check | 5 min | test | Two separate gates โ 90% total coverage, and 90% on each individual package โ plus Codecov upload and PR comment |
build | build | 1 min | validate, test, docs-build, coverage-check, doc-gate, action-pins, forbidden-terms, security, pin-impact | Aggregation gate โ fails if any required job failed; forbidden-terms must report success and may not be skipped |
analyze-changes | Analyze Changes | 5 min | - | Changed files analysis, breaking change warnings (PR only) |
docs-build | docs-build | 15 min | changes | Hugo 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-check | Docs Check | 5 min | changes | API changes need docs check (PR only); runs the canonical check-doc-gate action from go-kure/.github (job id: doc-gate) |
pin-impact | pin-impact | 3 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 symbolmode) - Coverage Threshold (total):
90%โ the overall figure fromgo 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-buildanddocs-check(doc-gatejob) run the canonicalcheck-doc-sync,check-linksandcheck-doc-gateactions fromgo-kure/.github; kure no longer vendors its own copies undersite/scripts/ - Downstream-reference guard - the unconditional
forbidden-termsjob scans the complete tracked tree and keeps the release script’s vendored guard byte-identical to the pinned canonical action - Pin-impact gate -
pin-impactrenders ago-kure/.githubpin bump’s real effect (whichscripts/*.sha 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 thepin-impact-acklabel to merge anyway โ same convention ascheck-doc-gate’sdocs-skiplabel; there is no other override. Rerun gotcha: thestrip-ackstep only runs when the triggering event’s action wassynchronizeorreopened; 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-addedpin-impact-ackagain before the gate re-checks it, even though nothing was pushed. A rerun of anopened/labeled/unlabeled-triggered run is unaffected โstrip-ackskips it either way. Scoped to same-repo PRs:strip-ack’sif: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 forcesPIN_IMPACT_ACK=falseunconditionally 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 ingo-kure/.github’sdocs/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:
| Situation | Event GitHub sends | Remedy |
|---|---|---|
| PR retargeted to another base branch | edited (with changes.base) | close and reopen the PR, which sends reopened |
| PR title or body edited | edited | none 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
- test โ Full test run with race detection and CGO enabled (
build-essential+CGO_ENABLED=1) - release โ Runs
scripts/release.shto 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) anddry_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-itRelease / Bump Workflow
File: .github/workflows/release-bump.yml
Name: Release / Bump
Triggers
- Manual dispatch with inputs:
scope(minor/major/prerelease) anddry_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-itRelease / 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
- test - Full test run with race detection
- validate - Strict tag format, changelog, and version progression validation
- goreleaser - Build release artifacts, generate SBOM, sign with cosign
- deploy-docs - Trigger versioned docs deployment (stable tags only)
- 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 withoutvprefix, 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-itMerge 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
- A reviewed PR is added to the queue (“Merge when ready”).
- The queue creates a temporary branch combining
main+ the PR and fires amerge_groupevent;lint/test/buildrun against that merged result. - If green, the PR lands on
mainwith the rebase merge method (linear history preserved). If the merged result fails, the PR is dropped from the queue andmainstays 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 onmerge_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 nopull_requestfield, so the existing fork skip below evaluates false and the job reportsskipped/success as a no-op- Runs on draft PRs the same as ready ones (2026-08-19, GitLab
mr-reviewparity โ see Draft PRs ); skips fork PRs (self-hosted runner security) ready_for_reviewis not declared, same reasoning asci.yml: it was kept as a rollout-window safety net while the callee (pr-review.yml@main, ingo-kure/.github) still gated ondraft == 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):
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.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-kubelabel (ARC runner with in-cluster access) - claude-max-proxy: Reachable at
http://openclaw-claude-proxy.openclaw.svc:3456from the runner pod - No API keys needed: the proxy handles model authentication
Configuration
Configurable via repository variables or workflow env defaults:
| Variable | Default | Purpose |
|---|---|---|
PR_REVIEW_MODEL | claude-opus-4 | Model for code review pass (cosmetic label; backend ignores the model field) |
PR_REVIEW_MAX_DIFF_CHARS | 50000 | Truncation threshold for large diffs |
PR_REVIEW_MAX_TOKENS | 1500 | Max response tokens for review |
PR_REVIEW_CONTEXT | kure project description | Additional system prompt context |
PR_REVIEW_ASSESS_ENABLED | true | Enable/disable assessment pass |
PR_REVIEW_ASSESS_MODEL | claude-sonnet-4-6 | Model for hallucination checking |
PR_REVIEW_ASSESS_MAX_TOKENS | 4096 | Max response tokens for assessment |
PR_REVIEW_AGENTS_FILE | AGENTS.md | Project 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
- Runs
scripts/gen-versions-toml.shto generate a versioned Hugo config overlay - Builds the Hugo site with
--config hugo.toml,versions.toml - Deploys the built site to a subdirectory of
go-kure.github.io
Trigger Matrix
| Event | What Deploys | Path | BaseURL |
|---|---|---|---|
Push to main (docs paths) | Dev docs | /dev/ | www.gokure.dev/dev/ |
workflow_dispatch | Versioned | /vX.Y/ | www.gokure.dev/vX.Y/ |
workflow_dispatch + set_latest=true | Versioned + 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
| Action | Description | Implementation |
|---|---|---|
remove-version | Delete a version’s docs | Removes /vX.Y/ directory from deploy target |
set-latest | Change root / to a specific version | Triggers deploy-docs.yml with set_latest=true |
rebuild-version | Re-trigger a docs build | Triggers 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=devVersioned Documentation
The docs site supports multiple documentation versions at different URL paths.
URL Structure
| Path | Content | Updated By |
|---|---|---|
/ | Latest stable release | Release workflow (set_latest=true) |
/vX.Y/ | Specific stable version | Release 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.0The script:
- Reads all stable tags (
vX.Y.Zwithout pre-release suffix) from git - Deduplicates to minor level (keeps highest patch per
vX.Y) - Generates
site/versions.tomlwith[params]section and[[params.versions]]entries - Marks the latest version with
isLatest = trueand rootbaseURL - 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
| Job | Matrix | Command | Uses Makefile? |
|---|---|---|---|
test | - | go test -json -v ./... | โ (deps) |
test | - | make test-race | โ |
test | - | make test-coverage | โ |
Test Targets in Makefile
| Target | Command | Used in CI? | In precommit? |
|---|---|---|---|
test | go test -timeout 30s ./... | โ | โ |
test-race | go test -race -timeout 30s ./... | โ | - |
test-coverage | go test -coverprofile=... ./... | โ | - |
test-integration | go test -tags=integration -timeout 5m ./... | - | - |
vuln | govulncheck ./... | โ | - |
versions-test | bash scripts/test/run-tests.sh | - (CI runs the script directly, not via make) | โ |
CI vs Pre-commit
| Target | Tasks | Use Case |
|---|---|---|
precommit | fmt, tidy, lint, test, check-tool-versions, check-govulncheck-docs, versions-test | Fast local checks (~10s) |
ci | deps, fmt, tidy, lint, vet, test, test-race, test-coverage, test-integration, vuln, check-tool-versions, check-govulncheck-docs, versions-test | Comprehensive 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 bygo.sumhash (tied togolang.org/x/toolsversion)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 literalgovulncheckโ 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:
Binary patch (
containers/actions-runner/Dockerfilein opsmaster):Runner.Worker.dllis patched to readCUSTOM_ACTIONS_RESULTS_URLinstead ofACTIONS_RESULTS_URLfor its own internal connection.CUSTOM_ACTIONS_RESULTS_URLis set as a pod env var in the runner’sHelmRelease. This ensures the Worker process itself connects through the cache server.Workflow env (
ACTIONS_RESULTS_URL): The binary patch replaces all UTF-16LE occurrences ofACTIONS_RESULTS_URLin the DLL โ including the name the Worker injects into step process environments (renamed toACTIONS_RESULTS_ORLas a side effect). SettingACTIONS_RESULTS_URLin the workflowenv:block overrides this so step processes (upload-artifact,download-artifact,actions/cachev2) 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 cachehugoโ Hugo module cache ($HUGO_CACHEDIRonly, 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, plusversions.yaml,docs/compatibility.md,scripts/sync-versions.sh,scripts/test/**andscripts/sync-eso-pin.sh. Those last five are here because the onlysync-versions.sh checkinvocation lives in thevalidatejob: 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, forsync-eso-pin.sh, all ofvalidate/test) and still reported success โ thebuildgate accepts askippeddependency as passing. Also includesmise.toml,scripts/check-tool-versions.sh,scripts/sync-tool-versions.shand this file, for the same reason:check-tool-versionsalso runs only in thevalidatejob, and a PR touching only one of those would otherwise skip the golangci-lint pin-parity guard. Same reasoning coversscripts/check-govulncheck-docs.shandscripts/sync-govulncheck-docs.shโcheck-govulncheck-docsalso runs only invalidate. Same reasoning coversscripts/sync-go-version.shtoo โcheck-go-version(Go-version parity betweenmise.tomlandgo.mod) also runs only invalidate. Andscripts/gen-builders.sh: the generated-builders check (scripts/gen-builders.sh check) also runs only invalidate, and Renovate invokes the same script after Go module bumps.pkg/**/testdata/**is there for the same reason: Go testdata is test input, andpkg/**alone matches only thedocsfilter, so a PR editing just the admission exclusion list would skip the tests that check it.docs:filter โ triggers docs-build/docs-check jobs. Includessite/**,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: notreleases/*) - Development branches:
main,develop
Estimated CI Time
| Scenario | Before (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
makeincludes an explicit install step:sudo apt-get install -y --no-install-recommends make - All
wgetcalls have been replaced withcurl -fsSL -o
Maintenance Notes
- When adding/modifying workflows: Update this document with changes
- Version updates: Run
make sync-go-versionto update Go version in all files - Version check: Run
make check-go-versionto verify consistency - Action versions: Keep GitHub Actions up to date (currently using v3-v6)
- New jobs using
make: Add theInstall build toolsstep (see above) if the job runs onautops-kube
See Also
- Makefile - Local development commands
- mise.toml - Local tool version management
- gen-versions-toml.sh - Versioned docs config generator