GitHub Actions Pipelines
Established Patterns
This repository uses a small set of consistent patterns across all workflows. Follow these when creating or modifying workflows.
Tool Setup
All workflows use mise for tool version management, ensuring CI and local dev use identical versions:
steps:
- uses: actions/checkout@v6
- uses: jdx/mise-action@v3 # Installs tools at versions from .mise.toml
- run: task k8s:validate # Use Taskfile commands, not raw CLI
Prefer Off-the-Shelf Actions
Use established actions over custom scripts:
Need Action Notes
Checkout actions/checkout@v6
Always pin major version
Tool setup jdx/mise-action@v3
Reads .mise.toml
GHCR login docker/login-action@v3
Use GITHUB_TOKEN
Complex logic actions/github-script@v7
Prefer over raw bash for GitHub API
Flux CLI fluxcd/flux2/action@v2
For OCI artifact operations
Use github-script Over Raw Bash
For anything involving GitHub API calls, JSON parsing, or conditional logic, prefer actions/github-script over shell scripts:
- name: Discover clusters id: discover uses: actions/github-script@v7 with: script: | // Use the GitHub API client directly const versions = await github.rest.packages.getAllPackageVersionsForPackageOwnedByUser({ package_type: 'container', package_name: packageName, username: context.repo.owner, }); core.setOutput('result', JSON.stringify(data));
Dynamic Matrix Builds
For per-cluster or per-module validation, use a discovery job followed by matrix expansion:
jobs: discover: outputs: items: ${{ steps.find.outputs.items }} steps: - id: find uses: actions/github-script@v7 with: script: | // Discover items dynamically core.setOutput('items', JSON.stringify(items));
validate: needs: discover strategy: fail-fast: false matrix: item: ${{ fromJson(needs.discover.outputs.items) }} steps: - run: task validate-${{ matrix.item }}
Path-Based Triggers
Workflows only run when relevant files change:
on: pull_request: paths: - "kubernetes/" # Domain files - ".github/workflows/kubernetes-validate.yaml" # The workflow itself - ".mise.toml" # Tool versions - ".taskfiles/kubernetes/" # Task definitions workflow_dispatch: # Manual trigger
Always include workflow_dispatch for manual runs.
YAML Schema Validation
All workflow files should include the schema comment for IDE validation:
yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: My Workflow
Workflow Inventory
Workflow Trigger Purpose
kubernetes-validate.yaml
PR (kubernetes/) Lint, expand ResourceSets, build, template, kubeconform, pluto
infrastructure-validate.yaml
PR (infrastructure/) Format checks, module tests (matrix per module)
renovate-validate.yaml
PR (renovate config) Validate Renovate configuration
build-platform-artifact.yaml
Push to main (kubernetes/) Build OCI artifact, tag for integration
tag-validated-artifact.yaml
Status event / manual Promote validated artifact to stable semver
renovate.yaml
Scheduled (hourly) Dependency update automation
label-sync.yaml
Scheduled / manual Sync GitHub labels
OCI Promotion Pipeline
The promotion pipeline is the most complex workflow chain. Understand the full flow before modifying any part:
PR merged to main (kubernetes/ changes) -> build-platform-artifact.yaml -> flux push artifact :X.Y.Z-rc.N -> flux tag :sha-<short> and :integration-<short> -> Integration cluster polls OCIRepository (semver >= 0.0.0-0, accepts RCs) -> Flux reconciles, posts commit status on success -> tag-validated-artifact.yaml (triggered by status event) -> Idempotency check (skip if already validated) -> flux tag :validated-<short> and :X.Y.Z (stable) -> Live cluster polls OCIRepository (semver >= 0.0.0, stable only)
Key Design Decisions
-
Semver-based polling: Integration accepts pre-releases (-rc.N ), live accepts stable only
-
Idempotency guard: tag-validated-artifact checks for existing validated-* tag before re-tagging (Flux Alerts fire on every reconciliation cycle)
-
Version resolution via GHCR API: Build workflow queries GHCR for latest stable tag, bumps patch, creates next RC
Permissions
permissions: contents: read # For checkout packages: write # For GHCR push/tag statuses: read # For reading commit status events
Always use minimal permissions. Never use permissions: write-all .
Creating a New Validation Workflow
Step-by-Step
-
Create .github/workflows/<domain>-validate.yaml
-
Add YAML schema comment
-
Set path-based triggers including the workflow file itself
-
Use jdx/mise-action@v3 for tool setup
-
Delegate to Taskfile commands (not raw CLI)
-
Add matrix builds for per-item validation if applicable
-
Run task k8s:validate locally to verify the validation commands work
Template
yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: <Domain> Validate
on: pull_request: paths: - "<domain>/**" - ".github/workflows/<domain>-validate.yaml" - ".mise.toml" workflow_dispatch:
jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: jdx/mise-action@v3 - run: task <domain>:validate
Anti-Patterns
-
NEVER install tools with apt-get or brew in CI — use mise
-
NEVER use raw curl / jq for GitHub API — use actions/github-script
-
NEVER hardcode versions in workflow files — versions come from .mise.toml or versions.env
-
NEVER use permissions: write-all — specify exact permissions needed
-
NEVER skip workflow_dispatch — all workflows should support manual runs
Cross-References
-
.github/CLAUDE.md — Declarative knowledge about workflow architecture
-
promotion-pipeline skill — Debugging promotion failures
-
.taskfiles/CLAUDE.md — Task commands used in workflows