Centralised reusable GitHub Actions workflows and composite actions for the
Sproncy org. Consumed by ~20 sibling repos via uses: pinned tags.
.github/
├── workflows/ # reusable workflows (callable via `uses:`)
│ ├── ansible.yml
│ ├── auto-assign.yml
│ ├── auto-revert-on-main-failure.yml
│ ├── bats.yml
│ ├── claude.yml
│ ├── claude-code-review.yml
│ ├── compose-validate.yml
│ ├── container-security.yml
│ ├── dependabot-auto-merge.yml
│ ├── dependabot-uv-lockfile.yml
│ ├── docker-build.yml
│ ├── elixir.yml
│ ├── expo.yml
│ ├── fastapi.yml
│ ├── go.yml
│ ├── komodo-deploy.yml
│ ├── lint-workflows.yml
│ ├── molecule.yml
│ ├── node-bun.yml
│ ├── opentofu.yml
│ ├── playwright-integration.yml
│ ├── python-uv.yml
│ ├── release.yml # auto-moves the floating vMAJOR tag
│ ├── scrapy.yml
│ ├── secret-scan.yml
│ ├── self-test.yml # this repo's own CI
│ ├── sops-audit.yml
│ ├── toml-lint.yml
│ ├── trivy-repo.yml
│ └── turbo.yml
├── actions/ # composite actions (callable via `uses:` from steps)
│ ├── setup-cosign/
│ ├── setup-deps-reader/
│ ├── setup-go/
│ ├── setup-mise/
│ ├── setup-sops/
│ ├── setup-token/
│ └── setup-trivy/
└── actionlint.yaml # declares Sproncy custom self-hosted labels
Most reusable workflows default to '["ubuntu-latest"]' and expose a
runs-on input (a JSON array string parsed with fromJSON). Consumers
route to self-hosted by passing a label array:
with:
runs-on: '["self-hosted", "linux", "x64"]'Exception: claude.yml and claude-code-review.yml default to the
self-hosted pool ('["self-hosted", "linux", "x64"]') since Claude jobs
run on private repos. Public/open-source consumers opt out with
runs-on: '["ubuntu-latest"]'.
The Sproncy self-hosted fleet uses capability labels (dind, fast, slow
for AI workloads). .github/actionlint.yaml declares them so actionlint
doesn't false-positive.
Workflows are consumed by tag. Tags follow vMAJOR.MINOR.PATCH and a
floating vMAJOR tag tracks the latest non-breaking release on that line.
- Pin to
@v3for low-friction updates within a major version. - Pin to
@v3.0.0(or a SHA) when you need bit-for-bit reproducibility.
Breaking changes bump the major. See CHANGELOG.md.
Drop a stub into the consumer repo at .github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
uses: nkg/github-actions/.github/workflows/python-uv.yml@v3
with:
python-version: "3.14"
secrets: inheritsecrets: inherit is the quickest path; for security-sensitive workflows
(claude, claude-code-review, sops-audit) prefer declaring secrets
explicitly so it's obvious what the workflow can see.
Every reusable takes a runs-on input that is a JSON array string, parsed
with fromJSON. Pass a quoted JSON array — not a bare label:
with:
runs-on: '["ubuntu-latest"]' # GitHub-hosted (the default)
# runs-on: '["self-hosted", "linux", "x64"]' # self-hosted poolA bare string like runs-on: ubuntu-latest (or unquoted '[self-hosted, ...]')
will not parse and the job will fail to schedule. The default is
'["ubuntu-latest"]' for every reusable except the claude* bot workflows,
which default to the self-hosted pool.
steps:
- uses: actions/checkout@v7
- uses: nkg/github-actions/.github/actions/setup-mise@v3
- run: mise run build| Workflow | Purpose |
|---|---|
python-uv.yml |
uv-based Python: ruff, mypy, pytest with coverage |
fastapi.yml |
python-uv + import smoke + OpenAPI export/diff |
scrapy.yml |
python-uv + scrapy list + scrapy check (+ optional smoke) |
node-bun.yml |
Node/TS: install, lint, typecheck, test, build (bun default; npm/yarn/pnpm/mise) |
playwright-integration.yml |
Integration tests inside the Playwright image (real Chromium; bun + private-registry aware) |
turbo.yml |
Bun + Turborepo monorepo (cache, tasks, optional filters) |
elixir.yml |
Mix: deps, format check, credo, test; opt-in dialyzer/sobelow/audits, private-deps, Postgres+Valkey DB tests |
go.yml |
Go: gofmt, vet, optional golangci-lint/staticcheck/govulncheck/fuzz, test+coverage, optional Postgres + private-deps |
expo.yml |
Expo/RN: install, lint, typecheck, test, optional EAS build (bun default; npm/yarn/pnpm/mise) |
opentofu.yml |
OpenTofu: fmt, init, validate, plan (apply gated by env) |
ansible.yml |
ansible-lint + syntax check |
molecule.yml |
Matrix molecule test across Ansible roles |
toml-lint.yml |
taplo fmt --check + Python tomllib parse over all .toml files |
docker-build.yml |
buildx multi-arch build + push to GHCR, optional build-secrets + private-deps |
compose-validate.yml |
docker compose config across profiles (DinD-safe pattern) |
komodo-deploy.yml |
POST to Komodo's /api/execute; optional poll-until-complete |
| Workflow | Purpose |
|---|---|
secret-scan.yml |
OSS betterleaks; PR-diff or full-history scan, SARIF artifact |
container-security.yml |
Trivy scan of every image in a compose file or explicit list |
trivy-repo.yml |
Trivy filesystem (lockfiles) or config (IaC) scan of the repo |
sops-audit.yml |
Verify SOPS encryption + plaintext-secret scan + shellcheck |
lint-workflows.yml |
actionlint + yamllint for the consumer's .github/ tree |
bats.yml |
bats-core test runner + companion shellcheck job (both opt-out) |
| Workflow | Purpose |
|---|---|
claude.yml |
@claude on-demand bot (issues, PRs, review comments) |
claude-code-review.yml |
Automated Claude code review on PR open/sync |
auto-assign.yml |
Auto-assign new issues and PRs to a user list |
dependabot-auto-merge.yml |
Auto-merges Dependabot PRs (major bumps gated; native auto-merge or Free-plan workflow_run path) |
dependabot-uv-lockfile.yml |
Regenerates uv.lock on Dependabot PRs and pushes it back (multi-dir aware) |
auto-revert-on-main-failure.yml |
Opens a revert PR when main CI fails (free-plan branch-protection workaround) |
stale-run-cleanup.yml |
Cancels superseded queued runs on a shared runner pool (min-age guard, dry-run) |
| Action | Purpose |
|---|---|
setup-mise |
Installs mise and runs mise install (caches the tool dir) |
setup-sops |
Installs sops + age (cross-arch), optionally decrypts a file |
setup-go |
Thin wrapper over actions/setup-go@v6 for org-wide version pin |
setup-trivy |
Installs Trivy CLI at a pinned version (linux/macOS × amd64/arm64) |
setup-cosign |
Wraps sigstore/cosign-installer@v3 so the version is pinned once |
setup-deps-reader |
Mints a scoped GitHub App token for private-deps access via git |
setup-token |
Mints a GitHub App installation token as an output (no git wiring); optional permission-* inputs scope it to least privilege |
Every reusable workflow accepts a runs-on input. Most default to
'["ubuntu-latest"]'; the Claude workflows (claude.yml,
claude-code-review.yml) default to '["self-hosted", "linux", "x64"]'.
Pass runs-on: '["self-hosted", "linux", "x64"]' (or a longer label array
like '["self-hosted", "linux", "x64", "dind"]') to route to your own
runner pool, or runs-on: '["ubuntu-latest"]' to opt back onto
GitHub-hosted runners.
The input is a JSON array string parsed with fromJSON, so it must be
quoted. Per-job matrix splits stay in the caller — this repo's workflows
are single-job.
Git hooks (lefthook) run the same checks as self-test.yml — actionlint,
yamllint, composite-action syntax, betterleaks — before commit/push, plus a
guard against direct pushes to main (bypass: ALLOW_MAIN_PUSH=1 git push).
This dogfoods the pattern documented in examples/pre-push-hook.md.
One-time setup per clone:
mise install && lefthook install
Renovate is configured (renovate.json) to bump pinned actions automatically.
Consumers should also enable Renovate to keep their pin to this repo
(nkg/github-actions) up to date.
Tag-based. From main:
git tag -a v1.2.0 -m "Release v1.2.0"
git push origin v1.2.0
The release.yml workflow handles the floating major-tag move automatically
when a vMAJOR.MINOR.PATCH tag is pushed.