Binding constraints for all reusable pipeline logic in loft-sh/github-actions.
Changes to this document require a PR with team review.
Three tiers based on complexity:
| Pattern | When to use | Example |
|---|---|---|
| YAML-only composite | Thin glue wiring inputs to third-party actions, no custom logic | release-notification |
| Compiled action (Go/Node.js) | Business logic, API calls, data transformation | linear-pr-commenter (Go), linear-release-sync (Go) |
| Composite over a released binary | The logic already ships as a versioned CLI; one action installs and verifies it, the others map its answers to outputs | setup-semstat (installs semstat), semver-validation (maps it) |
| Reusable workflow | Cross-repo orchestration of multiple jobs | backport.yaml, actionlint.yaml |
Key rule: Business logic (branching, loops, parsing, API calls) MUST live in
source files, not inline YAML run: blocks. YAML is glue only.
A composite over a released binary satisfies that rule: the logic under review lives in the binary's own repository and suite, and the scripts here only install it and translate. They may exceed the Bash line budget below, and in exchange they must pin the release, verify the download, and fail the step rather than report a broken binary's exit code as an answer.
Language choice should be made on a case-by-case basis depending on action requirements. The table below captures current status and guidance:
| Language | Status | Notes |
|---|---|---|
| Go | Supported | Org primary language. Used by linear-pr-commenter. |
| TypeScript | Preferred for actions | For runs.using: node* actions. Preferred over plain JS. |
| Python | Supported | Well-represented in AI training data, good for AI-assisted development. Adds runtime dependency overhead — use when it fits the problem best. |
| Bash | Glue, and wrappers over a released binary | Scripts under ~50 lines, or any length when they install and drive a versioned CLI (see the tier table above). Must pass shellcheck. |
Exception:
release-notification/detect-branch.shis longer, because it shares release-lib's branch and version rules withplatform-release, which is Bash too. Most of it guesses the branch a release was cut from, for releases whose dispatcher does not pass it. It shrinks once every dispatcher passes the branch it tagged, tracked in the DEVOPS-1598 Linear issue.
Note: Evaluate Dagger as a potential standardization layer — tracked in the DEVOPS-595 Linear issue.
- Explicit
permissionsat job level (least privilege). persist-credentials: falseon all checkouts.- Fork PRs have no secrets access — actions must skip gracefully or use a
fork-detection guard:
if: github.event.pull_request.head.repo.full_name == github.repository
- Secrets via
env:preferred overwith:where possible. - No hardcoded branch names in reusable workflows.
- Every action with business logic MUST have unit tests runnable locally.
- Makefile target:
test-<action-name>. - CI workflow:
.github/workflows/test-<action-name>.yamlwithpathsfilter scoped to the action's directory. - Tests must not require real API tokens or network access.
- YAML-only composites are validated by actionlint + zizmor (no unit tests needed for now).
- Testing frameworks: vitest for TypeScript, uv + pytest for Python,
standard
go testfor Go, bats for Bash scripts. CI usesbats-core/bats-actionto install bats, then runs the suite in an explicit step:- uses: bats-core/bats-action@<sha> # 4.0.0 with: support-install: false assert-install: false detik-install: false file-install: false - name: Run <action-name> tests run: bats .github/actions/<action-name>/test/*.bats
bats-actiononly installs bats. It does not run anything, and it has notestsinput. Handing it a path is an unknown input, which is a warning rather than an error, so a job that stops there passes without executing a single test.make check-bats-jobs(run bymake lintand by CI) fails the build on that shape. - Every test workflow's
pathsfilter MUST include the workflow file itself, so a change to the test harness is exercised by the PR that makes it. - Locally, install bats with your package manager:
# macOS brew install bats-core # Ubuntu / Debian sudo apt-get install bats # Arch Linux sudo pacman -S bats
- For actions with complex branching logic or external action interactions,
use the dedicated test repo:
vClusterLabs-Experiments/github-actions-test. - Reference integration test results in the PR.
- Integration tests are useful when unit tests alone cannot exercise the full workflow (e.g., cross-repo dispatch, branch protection rules).
Every PR that adds or modifies an action must verify:
-
persist-credentials: falseon all checkout steps - Explicit job-level
permissions(least privilege) - Third-party references use full commit SHAs with version comments for Renovate
- In-house
loft-sh/*references use their rolling version tags - actionlint + zizmor clean (
make lint) - Fork PRs handled (skip gracefully or guard on repo match)
CodeQL is enabled on vcluster, vcluster-pro, and loft-enterprise and will
flag additional security issues in those repos.
Note: Some action runs require manual approval (e.g., runs triggered by fork PRs or workflows referencing environments with protection rules).
Actions live under .github/actions/<action-name>/. Each action is versioned
independently with <action-name>/v<N> tags:
After a pull request merges to main, the action-tag workflow advances the
highest existing major tag for each changed action. Before merging a change that
needs a new major, add the major-release label to the pull request. The workflow
then leaves every tag alone, comments on the pull request, and fails so the
maintainer can cut and publish the new major. It also stops for releases that
need a separate binary build, including linear-release-sync. Actions without a
tag are skipped; create their first tag manually. Review callers and requirements
before merging, since a compatible update reaches callers automatically.
For a manual release or recovery, the commands are:
git tag -f <action-name>/v1
git push origin <action-name>/v1 --forceCallers reference in-house actions by their rolling version tags so shared fixes reach every caller:
uses: loft-sh/github-actions/.github/actions/<action-name>@<action-name>/v1Maintainers reserve the right to update the v<N> tag pointer at any time when
a new version is released, unless Renovate is configured to handle the upgrade
for a specific caller.
Reusable workflows live under .github/workflows/ and use their corresponding
rolling version tags. Third-party actions and reusable workflows use full commit
SHAs with version comments. Renovate configurations must exempt loft-sh/**
from digest pinning.