Reusable GitHub Actions
See Pipeline Conventions for constraints on how actions are written, tested, and structured.
Validates whether a given version string follows semantic versioning (semver) format.
Location: .github/actions/semver-validation
Usage:
- name: Validate version
id: semver
uses: loft-sh/github-actions/.github/actions/semver-validation@semver-validation/v1
with:
version: '1.2.3'
- name: Check if valid
run: echo "Valid: ${{ steps.semver.outputs.is_valid }}"Inputs:
version(required): Version string to validate
Outputs:
is_valid: Whether the version is valid semver (true/false)parsed_version: JSON object with parsed version componentserror_message: Error message if validation fails
See semver-validation README for detailed documentation.
Syncs Linear issues to the "Released" state when a GitHub release is published. Finds PRs between releases, extracts Linear issue IDs, and moves matching issues from "Ready for Release" to "Released".
Location: .github/actions/linear-release-sync
Usage:
- name: Sync Linear issues
uses: loft-sh/github-actions/.github/actions/linear-release-sync@linear-release-sync/v1
with:
release-tag: ${{ needs.publish.outputs.release_version }}
repo-name: my-repo
github-token: ${{ secrets.GH_ACCESS_TOKEN }}
linear-token: ${{ secrets.LINEAR_TOKEN }}See linear-release-sync README for detailed documentation.
Runs Ginkgo tests with directory or label-based filtering and generates a JSON failure summary. Runtime-agnostic — callers handle their own cluster and image setup (vind, Kind, bare Docker).
Location: .github/actions/run-ginkgo
Usage:
- name: Run E2E tests
id: e2e
uses: loft-sh/github-actions/.github/actions/run-ginkgo@run-ginkgo/v1
with:
ginkgo-label: "my-suite && !non-default"
test-image: ghcr.io/loft-sh/vcluster:dev
# test-image-flag: "--platform-image" # default: --vcluster-image
# additional-ginkgo-flags: "-v --skip-package=linters"
# additional-args: "--use-license-server=false"
- name: Notify on failure
if: failure()
uses: loft-sh/github-actions/.github/actions/ci-test-notify@ci-test-notify/v1
with:
test-name: "E2E Tests"
status: failure
details: ${{ steps.e2e.outputs.failure-summary }}
webhook-url: ${{ secrets.SLACK_WEBHOOK }}Inputs:
| Input | Required | Default | Description |
|---|---|---|---|
test-image |
yes | Image passed to the test binary | |
test-image-flag |
no | --vcluster-image |
CLI flag name for the image |
timeout |
no | 60m |
Ginkgo test timeout |
procs |
no | 8 |
Parallel Ginkgo processes |
test-dir |
no | Directory-based test selection (mutually exclusive with ginkgo-label) |
|
ginkgo-label |
no | Label-based test selection (mutually exclusive with test-dir) |
|
append-pr-label |
no | true |
Append || pr to the label filter |
e2e-dir |
no | e2e-next |
Root test directory |
additional-args |
no | Extra args for the test binary (after --) |
|
additional-ginkgo-flags |
no | Extra ginkgo CLI flags |
Outputs:
failure-summary: Markdown-formatted test results summary
Validates Renovate configuration files when they change in a pull request.
Location: .github/workflows/validate-renovate.yaml
Usage:
name: Validate Renovate Config
on:
pull_request:
jobs:
validate-renovate:
uses: loft-sh/github-actions/.github/workflows/validate-renovate.yaml@mainDetected config files: renovate.json, renovate.json5, .renovaterc, .renovaterc.json, .github/renovate.json, .github/renovate.json5.
Approves (and optionally enables auto-merge on) PRs from trusted bot accounts
whose title or branch matches a known safe pattern (chore: / fix(deps): /
backport/ / renovate/ / update-platform-version-). Hardened to never
block caller CI: continue-on-error: true on the job, every shell step
catches its own errors and exits 0, self-approval is pre-empted before calling
the external approve action.
Location: .github/workflows/auto-approve-bot-prs.yaml
Usage:
name: Auto-approve bot PRs
on:
pull_request:
types: [opened, synchronize]
jobs:
auto-approve:
permissions:
pull-requests: write
contents: read
uses: loft-sh/github-actions/.github/workflows/auto-approve-bot-prs.yaml@main
with:
trusted-authors: 'renovate[bot],loft-bot,github-actions[bot],dependabot[bot]'
auto-merge: false
secrets:
gh-access-token: ${{ secrets.GH_ACCESS_TOKEN }}gh-access-token must be a PAT whose identity differs from PR authors you want
to auto-approve (GitHub forbids self-review). When identity matches, the job
skips gracefully instead of failing.
End-to-end coverage: scenario-level e2e lives in vClusterLabs-Experiments/auto-approve-e2e. Runs weekly and on demand. Creates real PRs exercising every decision-table branch (chore/fix(deps) titles, backport/renovate/update-platform-version branches, ineligible titles) and asserts the never-hard-fail invariant.
Lints GitHub Actions workflow files using actionlint with reviewdog integration.
Location: .github/workflows/actionlint.yaml
Usage:
name: Actionlint
on:
pull_request:
jobs:
actionlint:
uses: loft-sh/github-actions/.github/workflows/actionlint.yaml@mainInputs:
reporter(optional, default:github-pr-review): reviewdog reporter type
Packages a Helm chart and pushes one tarball per version to ChartMuseum.
Handles release pushes (single semver, optional --app-version) and head
pushes (multiple 0.0.0-* versions) under the same contract. Optionally
re-pushes the repo's highest semver afterwards so it stays first in the
upload-ordered ChartMuseum index.
Location: .github/actions/publish-helm-chart
Usage (release push):
jobs:
publish-chart:
runs-on: ubuntu-24.04
permissions:
contents: read
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: v1.2.3
persist-credentials: false
- uses: loft-sh/github-actions/.github/actions/publish-helm-chart@publish-helm-chart/v2
with:
chart-name: vcluster
app-version: 1.2.3
chart-versions: '["1.2.3"]'
chart-museum-user: ${{ secrets.CHART_MUSEUM_USER }}
chart-museum-password: ${{ secrets.CHART_MUSEUM_PASSWORD }}Usage (head/dev push):
jobs:
push-head-chart:
runs-on: ubuntu-24.04
permissions:
contents: read
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: loft-sh/github-actions/.github/actions/publish-helm-chart@publish-helm-chart/v2
with:
chart-name: vcluster-head
chart-description: "vCluster HEAD - Development builds from main branch"
app-version: head-${{ github.sha }}
chart-versions: '["0.0.0-latest","0.0.0-${{ github.sha }}"]'
chart-museum-user: ${{ secrets.CHART_MUSEUM_USER }}
chart-museum-password: ${{ secrets.CHART_MUSEUM_PASSWORD }}Inputs:
chart-name(required): chart name written toChart.yamland used in the tarball filenamechart-description(optional): value written to.descriptioninChart.yamlapp-version(optional): passed as--app-versiontohelm packagechart-versions(required): JSON array of versions, e.g.'["1.2.3"]'chart-directory(optional, default:chart): chart source pathvalues-edits(optional): newline-separatedjsonpath=valuepairs applied via yq to<chart-directory>/values.yamlhelm-version(optional, default:v4.1.4)republish-latest(optional, default:"false"): re-push highest semver to keep it first in the ChartMuseum indexchart-museum-url(optional, default:https://charts.loft.sh/)chart-museum-user(required)chart-museum-password(required)
Note: The ref input was removed — the caller owns actions/checkout and checks out the desired ref directly.
Runs govulncheck
against a Go module and, on scheduled runs, posts a Slack notification
(via ci-test-notify) when vulnerabilities are found. The scan always
marks the job failed on vulnerabilities — notification is the side
channel, not the gate.
Location: .github/actions/govulncheck
Usage (public repo, weekly schedule):
name: govulncheck
on:
schedule:
- cron: "0 12 * * 1" # Mon 12:00 UTC
workflow_dispatch:
pull_request:
paths:
- ".github/workflows/govulncheck.yaml"
jobs:
scan:
runs-on: ubuntu-latest
if: github.repository_owner == 'loft-sh'
permissions:
contents: read
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: loft-sh/github-actions/.github/actions/govulncheck@govulncheck/v1
with:
slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL_CI_TESTS_ALERTS }}Usage (private repo that depends on github.com/loft-sh/*):
jobs:
scan:
runs-on: ubuntu-latest
if: github.repository_owner == 'loft-sh'
permissions:
contents: read
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: loft-sh/github-actions/.github/actions/govulncheck@govulncheck/v1
with:
scan-paths: "./... ./cmd/..."
private-repo: "true"
gh-access-token: ${{ secrets.GH_ACCESS_TOKEN }}
slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL_CI_TESTS_ALERTS }}Inputs:
scan-paths(optional, default:./...): space-separated Go package patternstest-flag(optional, default:true): pass-testto govulncheckgo-version-file(optional, default:go.mod): passed toactions/setup-goprivate-repo(optional, default:false): enable git url rewrite +GOPRIVATEgoprivate(optional, default:github.com/loft-sh/*)govulncheck-version(optional, default:latest)test-name(optional, default:govulncheck): Slack headernotify(optional, default:true): send Slack on vulnerabilities; fires onscheduleevents onlygh-access-token(required whenprivate-repo: true)slack-webhook-url(required whennotify: trueand the run is onschedule)
Notes:
- The caller checks out its own source and controls
runs-on/timeout-minutes/fork guarding at the job level. - A composite action cannot declare
timeout-minuteson its steps; settimeout-minuteson the caller job (default ~10m is reasonable for most modules).
Run all action tests locally:
make testRun tests for a specific action:
make test-semver-validation
make test-linear-pr-commenter
make test-linear-release-syncRun linters (actionlint + zizmor):
make lintSee all available targets:
make helpEach testable action has a dedicated workflow that runs its tests on PRs when the action's files change:
test-semver-validation.yaml- triggers on.github/actions/semver-validation/**test-linear-pr-commenter.yaml- triggers on.github/actions/linear-pr-commenter/**test-linear-release-sync.yaml- triggers on.github/actions/linear-release-sync/**release-linear-release-sync.yaml- builds and publishes the binary on tag push orworkflow_dispatch
Each reusable workflow (workflow_call) also has a smoke/integration test
workflow that triggers on PRs when the workflow file changes:
test-validate-renovate.yaml- callsvalidate-renovate.yamlwith local ref. Note: When triggered by workflow YAML changes alone, the innerpaths-filterwon't match any renovate config files sonpx renovate-config-validatornever runs. The validator only exercises its full path whenrenovate.jsonis also changed.test-detect-changes.yaml- callsdetect-changes.yamland asserts outputs (true/false)test-actionlint-workflow.yaml- callsactionlint.yamlwithgithub-pr-checkreporter (PR-only). Note:actionlint.yamlskips fork PRs silently; the verify job emits a warning when this happens.test-backport.yaml- callsbackport.yamland asserts the result isskippedtest-clean-github-cache.yaml- callsclean-github-cache.yaml(PR-only, since the underlying workflow needsgithub.event.pull_request.number)test-cleanup-backport-branches.yaml- callscleanup-backport-branches.yamlwithdry-run: truetest-conflict-check.yaml- callsconflict-check.yamland asserts success or skippedtest-claude-code-review.yaml- callsclaude-code-review.yamlto validate workflow is callabletest-claude.yaml- callsclaude.yamland assertsskipped(no@claudecomment event)test-notify-release.yaml- callsnotify-release.yamlwith dummy inputs to validate the contract
Post-merge, dispatch-integration-tests.yaml triggers full E2E tests in
vClusterLabs-Experiments/github-actions-test.
-
Node.js actions - add a
test/directory with Jest tests. Seesemver-validation/test/index.test.jsfor the pattern: spawn the action'sindex.jswithINPUT_*env vars and a tempGITHUB_OUTPUTfile, then assert on the parsed outputs. -
Go actions - add
*_test.gofiles next to the source. Seelinear-pr-commenter/src/main_test.go. Use standardgo test. -
Composite actions (YAML-only like
release-notification) - these delegate to third-party actions and have no local business logic to unit test. Validate their YAML structure through actionlint instead. -
Add a Makefile target for the new action following the existing pattern.
-
Add a CI workflow at
.github/workflows/test-<action-name>.yamlwith apathsfilter scoped to the action's directory. -
Add
AUTO-DOC-INPUT/AUTO-DOC-OUTPUTmarkers to the action'sREADME.mdand runmake generate-docs(see Documentation).
Action and reusable workflow documentation is auto-generated from
action.yml / workflow YAML using tj-actions/auto-doc.
Each action README and each workflow doc in docs/workflows/ contains
AUTO-DOC-INPUT, AUTO-DOC-OUTPUT, and AUTO-DOC-SECRETS marker comments
that are filled in by the tool.
Regenerate all docs locally:
make generate-docsVerify docs are up to date (CI runs this on every PR):
make check-docsInstall the auto-doc binary only (downloaded to .bin/):
make install-auto-docReusable workflow documentation lives in docs/workflows/<workflow-name>.md.
Each file maps 1:1 to a workflow_call workflow in .github/workflows/.
-
Action -- add
## Inputsand## Outputssections with marker comments to the action'sREADME.md:## Inputs <!-- AUTO-DOC-INPUT:START - Do not remove or modify this section --> <!-- AUTO-DOC-INPUT:END --> ## Outputs <!-- AUTO-DOC-OUTPUT:START - Do not remove or modify this section --> <!-- AUTO-DOC-OUTPUT:END -->
-
Reusable workflow -- create
docs/workflows/<name>.mdwith## Inputs,## Outputs(if applicable), and## Secretsmarker sections. -
Run
make generate-docsand commit the result.
The existing release-notification action uses a repository-wide tag:
git tag -f v1
git push origin v1 --forceReferenced as:
uses: loft-sh/github-actions/release-notification@v1For all new actions, we use action-specific tags for independent versioning:
# For the ci-notify-nightly-tests action
git tag -f ci-notify-nightly-tests/v1
git push origin ci-notify-nightly-tests/v1 --force
# For the semver-validation action
git tag -f semver-validation/v1
git push origin semver-validation/v1 --force
# For other actions, follow the same pattern
git tag -f action-name/v1
git push origin action-name/v1 --force# Reference actions using their specific tag
uses: loft-sh/github-actions/.github/actions/ci-notify-nightly-tests@ci-notify-nightly-tests/v1
uses: loft-sh/github-actions/.github/actions/semver-validation@semver-validation/v1