Reusable GitHub Actions composite actions and CI workflow for the Sparkgeo organisation.
All action references in this repo are pinned to full commit SHAs. See CONTRIBUTING.md for authoring standards and how to add new actions.
| Workflow | File | Triggers | Purpose |
|---|---|---|---|
| CI | ci.yml |
push to main, pull_request, schedule (weekly), workflow_dispatch |
Dogfoods all composite actions in this repo; serves as a live reference implementation |
| Secrets Pre-commit | secrets-precommit.yml |
workflow_call |
Reusable Gitleaks gate — call from a consuming repo's PR workflow to block secrets in CI |
| Secrets PR Scan | secrets-scan.yml |
workflow_call |
Reusable TruffleHog gate — verifies matched credentials are live; hard-blocks on verified secrets; uploads SARIF to the Security tab |
| Lint Pre-commit | lint-precommit.yml |
workflow_call |
Reusable gate that runs the consuming repo's .pre-commit-config.yaml hooks in CI; language-agnostic; shared across app/IaC/Helm lint stages |
| Lint App | lint-app.yml |
workflow_call |
Reusable MegaLinter gate — auto-detects all languages, blocks on any linter error, uploads SARIF to the Security tab |
| Lint IaC | lint-iac.yml |
workflow_call |
Reusable tflint gate — recursive Terraform/OpenTofu lint, provider-agnostic via .tflint.hcl, inline PR annotations, plugin caching |
| Lint Helm | lint-helm.yml |
workflow_call |
Reusable kubeconform gate — renders Helm charts / Kustomize overlays and validates against Kubernetes API schemas; blocks on schema errors |
Drop these into any job with a uses: step. Pin to a full commit SHA for supply-chain safety.
# Find the SHA to pin to
gh api repos/sparkgeo/github-actions/commits/main --jq '.sha'| Action | Path | Purpose | Inputs |
|---|---|---|---|
| GitHub Actionlint | github-actionlint |
Lints workflow and action YAML files using actionlint via reviewdog; posts annotations as GitHub Checks | None |
| Zizmor | zizmor |
Runs zizmor static security analysis against workflow and action YAML files; uploads findings as SARIF to the Security tab | None |
| OpenSSF Scorecard | scorecard |
Runs OpenSSF Scorecard checks; uploads SARIF to the Security tab | publish_results (default: false — always fails with HTTP 400 if set to true; see action description) |
| Dependency Review | dependency-review |
Blocks PRs introducing dependencies with known vulnerabilities or non-permitted licenses; posts a summary comment. Also supports non-PR invocation via base-ref/head-ref. |
fail-on-severity (default: high), allow-licenses (default: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, Unlicense, CC0-1.0), comment-summary-in-pr (default: on-failure), base-ref (default: ""), head-ref (default: "") |
| Storage Optimizer | storage-optimizer |
Frees disk space on GitHub-hosted runners by removing unused toolchains (JDK, .NET, Swift, Android SDK, etc.) and pruning Docker | None |
| Terramate + OpenTofu Setup | terramate-opentofu-setup |
Installs Terramate and OpenTofu, validates generated files are up to date, initialises changed stacks, and lists changed stacks | opentofu_version (default: 1.10.0), terramate_version (default: 0.14.7) |
| AWS OIDC Auth | aws-oidc-auth |
Assumes an IAM role via GitHub OIDC — no static credentials stored; enforces traceable session name {repo}-{run_id} |
role-arn (required), aws-region (required), role-session-name (default: {repo}-{run_id}) |
| Gitleaks Secret Scan | gitleaks |
Pattern-based secret detection; hard-fails on any match. Scans the PR commit range on pull_request, else the full git history. Installs a checksum-verified Gitleaks binary (no paid license) |
version (default: 8.30.1), config-path (default: .gitleaks.toml), fail-on-finding (default: true) |
| TruffleHog Verified Scan | trufflehog |
Verified active-secret detection; hard-fails on verified secrets, unverified matches are warnings. Converts findings to SARIF and uploads to the Security tab. Installs a checksum-verified TruffleHog binary | version (default: 3.95.5), only-verified (default: true), sarif-upload (default: true) |
| Pre-commit | pre-commit |
Runs the consuming repo's .pre-commit-config.yaml hooks; changed files on PRs, all files otherwise. Language-agnostic |
version (default: 4.6.0), config-path (default: .pre-commit-config.yaml), from-ref/to-ref (default: PR base/head) |
| TFLint | tflint |
Recursive Terraform/OpenTofu lint; provider rule sets via consuming-repo .tflint.hcl; inline PR annotations. Installs a checksum-verified tflint binary |
version (default: 0.63.1), directory (default: .), minimum-failure-severity (default: error) |
| Kubeconform | kubeconform |
Renders Helm charts (helm template) and Kustomize overlays (kustomize build) and validates output against Kubernetes API schemas. Installs a checksum-verified kubeconform binary |
version (default: 0.8.0), charts-dir (default: charts), kustomize-dir (default: ""), kubernetes-version (default: 1.32.0), ignore-missing-schemas (default: false) |
jobs:
lint:
runs-on: ubuntu-latest
permissions:
contents: read
checks: write
steps:
- uses: actions/checkout@<SHA>
with:
persist-credentials: false
- uses: sparkgeo/github-actions/.github/actions/github-actionlint@<SHA>jobs:
security-scan:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@<SHA>
with:
persist-credentials: false
- uses: sparkgeo/github-actions/.github/actions/zizmor@<SHA>SARIF is always uploaded to the GitHub Security tab. Does not require id-token: write — publish_results defaults to false and no OIDC token is consumed.
Note on
publish_results: Settingpublish_results: truealways fails with HTTP 400. The scorecard webapp verifies that the calling workflow directly invokesossf/scorecard-actionas a job step — composite action wrapping hides that call. Keep the default (false). To publish to the public OpenSSF database, callossf/scorecard-actiondirectly in your workflow.
jobs:
scorecard:
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
security-events: write
steps:
- uses: actions/checkout@<SHA>
with:
persist-credentials: false
- uses: sparkgeo/github-actions/.github/actions/scorecard@<SHA>
# publish_results defaults to false — do not set to true (always fails with HTTP 400)Works on pull_request events (automatic base/head from PR context) and on push/non-PR events by passing base-ref/head-ref explicitly. Skip on initial branch push (event.before = zero SHA — no base to compare).
jobs:
dependency-review:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main' && github.event.before != '0000000000000000000000000000000000000000')
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@<SHA>
with:
persist-credentials: false
- uses: sparkgeo/github-actions/.github/actions/dependency-review@<SHA>
with:
fail-on-severity: high # critical | high | moderate | low
allow-licenses: MIT, Apache-2.0, BSD-2-Clause # SPDX identifiers; deps with other licenses fail
comment-summary-in-pr: always # always | on-failure | never
base-ref: ${{ github.event_name == 'push' && github.event.before || '' }} # leave empty on pull_request
head-ref: ${{ github.event_name == 'push' && github.sha || '' }} # leave empty on pull_requestReclaims ~30 GB on ubuntu-latest runners — useful before large build or scan jobs.
steps:
- uses: sparkgeo/github-actions/.github/actions/storage-optimizer@<SHA>steps:
- uses: sparkgeo/github-actions/.github/actions/terramate-opentofu-setup@<SHA>
with:
opentofu_version: "1.10.0" # optional — matches the action default
terramate_version: "0.14.7" # optional — matches the action defaultThe action will fail the job if terramate generate produces uncommitted output, ensuring generated files are always in sync with the source of truth.
Exchanges a GitHub OIDC token for short-lived AWS credentials — no static key stored as a secret. See docs/oidc-trust-policies.md for IAM trust policy setup and migration checklist.
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # required for OIDC token exchange
contents: read
steps:
- uses: actions/checkout@<SHA>
with:
persist-credentials: false
- uses: sparkgeo/github-actions/.github/actions/aws-oidc-auth@<SHA>
with:
role-arn: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
aws-region: ca-central-1
# AWS credentials now available in environment
- run: aws sts get-caller-identityStore the role ARN as a repository or environment variable (not a secret — ARNs are not sensitive). For production deployments, add a environment: production key to the job and configure required reviewers in Settings → Environments.
Two layers of secret detection — a local pre-commit hook (instant, zero CI cost) and a CI gate (catches contributors without the hook).
Local hook — add to the consuming repo's .pre-commit-config.yaml:
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: 83d9cd684c87d95d656c1458ef04895a7f1cbd8e # v8.30.1 — pin to the SHA, not the tag
hooks:
- id: gitleaksCI gate — call the reusable workflow on every PR:
# .github/workflows/secrets.yml
name: Secrets Detection
on: [pull_request]
permissions:
contents: read
jobs:
gitleaks:
uses: sparkgeo/github-actions/.github/workflows/secrets-precommit.yml@<SHA>
with:
actions-ref: <SHA> # pin to the SAME SHA so the composite is immutable tooOr drop the composite action straight into an existing job:
- uses: actions/checkout@<SHA>
with:
persist-credentials: false
fetch-depth: 0 # full history so the PR commit range can be scanned
- uses: sparkgeo/github-actions/.github/actions/gitleaks@<SHA>Allowlisting false positives — add a .gitleaks.toml to the consuming repo root. Every entry must carry a comment explaining why the finding is a false positive:
[allowlist]
description = "Allowlisted patterns"
paths = [
'''tests/fixtures/''',
'''docs/examples/''',
]
regexes = [
'''EXAMPLE_KEY_[A-Z0-9]{20}''', # synthetic example keys in docs
]The PR-stage counterpart to gitleaks. Where gitleaks flags patterns fast, TruffleHog confirms which matches are live, verified credentials before blocking — cutting false-positive noise. Findings upload to the Security tab as SARIF (verified → error, unverified → warning).
Run both gates together on every PR:
# .github/workflows/secrets.yml
name: Secrets Detection
on: [pull_request]
permissions:
contents: read
jobs:
gitleaks:
uses: sparkgeo/github-actions/.github/workflows/secrets-precommit.yml@<SHA>
with:
actions-ref: <SHA>
trufflehog:
uses: sparkgeo/github-actions/.github/workflows/secrets-scan.yml@<SHA>
permissions:
contents: read
security-events: write # required for SARIF upload
with:
actions-ref: <SHA>Gate behaviour: a verified active secret hard-blocks the merge immediately. Unverified pattern matches are surfaced as warnings in the Security tab only (set only-verified: false to block on those too).
When a verified secret is detected: revoke it immediately (do not wait for the PR to close), rotate at the source, then purge it from git history with git filter-repo / BFG and coordinate a force-push. See SECURITY.md.
The non-bypassable CI counterpart to a developer's local pre-commit hook — runs the consuming repo's .pre-commit-config.yaml against the PR diff so anything that slipped past a missing local hook is still caught. Language-agnostic; all tool config lives in the consuming repo. Shared across the app, IaC, and Helm lint stages (#9 / #10 / #11).
# .github/workflows/lint.yml
name: Lint
on: [pull_request]
permissions:
contents: read
jobs:
pre-commit:
uses: sparkgeo/github-actions/.github/workflows/lint-precommit.yml@<SHA>
with:
actions-ref: <SHA> # pin to the SAME SHA so the composite is immutable tooThe consuming repo provides a .pre-commit-config.yaml at its root — e.g. for a Python + Node.js repo:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: <SHA> # v0.x.x
hooks:
- id: ruff
- id: ruff-format
- repo: https://github.com/rbubley/mirrors-prettier
rev: <SHA> # v3.x.x
hooks:
- id: prettierThe PR-stage gate for application source. MegaLinter auto-detects every language in the repo and runs the right linter/formatter check for each — no per-language config in the workflow call. Findings upload to the Security tab as SARIF; the job blocks on any linter error.
# .github/workflows/lint.yml
name: Lint
on: [pull_request]
jobs:
pre-commit:
uses: sparkgeo/github-actions/.github/workflows/lint-precommit.yml@<SHA>
with:
actions-ref: <SHA>
app:
uses: sparkgeo/github-actions/.github/workflows/lint-app.yml@<SHA>
permissions:
contents: read
security-events: write # required for SARIF upload
with:
filter-regex-exclude: 'dist/|generated/' # optionalTune linters via an optional .mega-linter.yml in the consuming repo (enable/disable specific linters, set DISABLE_ERRORS per tool, etc.).
Org allowlist:
lint-app.ymlusesoxsecurity/megalinter, sooxsecurity/*must be on the org allowlist (already added in approved-actions; apply with thegh apiallowlist update if not yet live).
The PR-stage gate for Terraform / OpenTofu. Runs tflint --recursive; cloud-provider rule sets are activated by a .tflint.hcl in the consuming repo, so the workflow is provider-agnostic. Findings show as inline PR annotations and the job blocks on any finding at or above minimum-failure-severity. Plugin downloads are cached.
# .github/workflows/lint.yml (add to the same file)
iac:
uses: sparkgeo/github-actions/.github/workflows/lint-iac.yml@<SHA>
with:
actions-ref: <SHA>
directory: . # default; point at a subdir if IaC lives there
minimum-failure-severity: error # default; 'warning' to be stricterAdd a .tflint.hcl to the consuming repo root to enable provider rule sets — e.g. AWS:
plugin "aws" {
enabled = true
version = "0.x.x"
source = "github.com/terraform-linters/tflint-ruleset-aws"
}Swap aws for google (tflint-ruleset-google) or azurerm (tflint-ruleset-azurerm). The default terraform ruleset is always active with no .tflint.hcl.
For the local fast-feedback stage, copy examples/iac.pre-commit-config.yaml to your repo root as .pre-commit-config.yaml (terraform_fmt / terraform_validate / terraform_tflint) and gate it in CI with lint-precommit.yml.
- OpenTofu: works on Terraform-compatible
.tffiles. tflint does not read.tofufiles and does not understand OpenTofu-only syntax (e.g. the state/planencryptionblock, 1.7+). Standard.tfOpenTofu code lints fine; keep OpenTofu-specific config out of.tfif you hit false positives. - Terramate: tflint ignores
.tm.hclstack files (only.tfis linted). If you commit terramate-generated.tf(_terramate_generated_*.tf),tflint --recursivelints those too — they often trip rules liketerraform_required_providersorterraform_unused_declarationsthat you can't hand-fix. Either fix the generate templates, disable those rules in.tflint.hcl, or rely on the defaulterrorseverity floor (these areWarning-level, so they annotate but do not block).
The PR-stage gate for Helm charts and Kustomize overlays. For each chart under charts-dir it runs helm lint, then helm template; for each overlay under kustomize-dir it runs kustomize build; the rendered manifests are validated against the Kubernetes API schemas with kubeconform. The job blocks on a helm lint failure, a render failure (helm template / kustomize build), or any schema error (invalid fields, missing required fields, deprecated/removed API versions) — each is reported with its own distinct annotation. helm and kustomize are preinstalled on GitHub-hosted runners.
# .github/workflows/lint.yml (add to the same file)
helm:
uses: sparkgeo/github-actions/.github/workflows/lint-helm.yml@<SHA>
with:
actions-ref: <SHA>
charts-dir: charts # default; '' to skip Helm
kustomize-dir: '' # optional; path to Kustomize overlays root
kubernetes-version: '1.32.0' # target cluster schema versionCRDs / custom resources: the official schema registry doesn't cover CRDs, so charts using custom resources will fail with "could not find schema". Set ignore-missing-schemas: true to skip kinds with no schema instead of failing on them:
with:
ignore-missing-schemas: trueFindings are emitted as job-level error annotations naming the failing chart/overlay — rendered manifests have no source-line mapping, so inline annotations aren't possible.
For the local fast-feedback stage, copy examples/helm.pre-commit-config.yaml to your repo root as .pre-commit-config.yaml (helm lint) and gate it in CI with lint-precommit.yml.
Public and private repos use different subsets of these actions. The key differences are harden-runner (sends egress telemetry to StepSecurity — omit on private repos) and scorecard (requires public visibility to produce meaningful scores).
name: CI
on:
push:
branches: [main]
pull_request:
schedule:
- cron: '0 6 * * 1'
permissions:
contents: read
jobs:
actionlint:
runs-on: ubuntu-latest
permissions: { contents: read, checks: write }
steps:
- uses: step-security/harden-runner@<SHA>
with: { egress-policy: audit }
- uses: actions/checkout@<SHA>
with: { persist-credentials: false }
- uses: sparkgeo/github-actions/.github/actions/github-actionlint@<SHA>
zizmor:
runs-on: ubuntu-latest
permissions: { contents: read, security-events: write }
steps:
- uses: step-security/harden-runner@<SHA>
with: { egress-policy: audit }
- uses: actions/checkout@<SHA>
with: { persist-credentials: false }
- uses: sparkgeo/github-actions/.github/actions/zizmor@<SHA>
scorecard: # public repos only — checks degrade on private repos
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
permissions: { contents: read, actions: read, security-events: write }
steps:
- uses: step-security/harden-runner@<SHA>
with: { egress-policy: audit }
- uses: actions/checkout@<SHA>
with: { persist-credentials: false }
- uses: sparkgeo/github-actions/.github/actions/scorecard@<SHA>
dependency-review:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
permissions: { contents: read, pull-requests: write }
steps:
- uses: step-security/harden-runner@<SHA>
with: { egress-policy: audit }
- uses: actions/checkout@<SHA>
with: { persist-credentials: false }
- uses: sparkgeo/github-actions/.github/actions/dependency-review@<SHA>Same structure — drop harden-runner from every job and drop the scorecard job entirely:
name: CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
jobs:
actionlint:
runs-on: ubuntu-latest
permissions: { contents: read, checks: write }
steps:
# harden-runner omitted — sends egress telemetry to StepSecurity (third party)
- uses: actions/checkout@<SHA>
with: { persist-credentials: false }
- uses: sparkgeo/github-actions/.github/actions/github-actionlint@<SHA>
zizmor:
runs-on: ubuntu-latest
permissions: { contents: read, security-events: write }
# security-events: write requires GitHub Advanced Security on private repos
steps:
- uses: actions/checkout@<SHA>
with: { persist-credentials: false }
- uses: sparkgeo/github-actions/.github/actions/zizmor@<SHA>
# scorecard omitted — most checks require public repo visibility
dependency-review:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
permissions: { contents: read, pull-requests: write }
steps:
- uses: actions/checkout@<SHA>
with: { persist-credentials: false }
- uses: sparkgeo/github-actions/.github/actions/dependency-review@<SHA>See docs/approved-actions.md for full data handling and telemetry details.
This repo is part of the Sparkgeo GitHub Actions security programme. The pillars are:
| Pillar | Issue | Summary |
|---|---|---|
| Workflow authoring standards | #25 | SHA pinning policy; actionlint/zizmor gate |
| Supply chain hardening | #26 | Org allowlist; dependency locking; approved actions |
| OIDC & secret federation | #27 | No static credentials; OIDC for cloud auth; environment-scoped secrets |
| Runner egress control | #28 | harden-runner audit → block; self-hosted runner isolation policy |
| Enterprise governance & observability | #29 | Org rulesets; OpenSSF Scorecard; audit log → SIEM |
To report a security vulnerability, use the Security Advisory process — do not open a public issue.