A library of GitHub Actions reusable workflows (on: workflow_call) that application
and infrastructure repositories across an organization call into for CI, security scanning,
container builds, GKE deployment, and releases — with sane defaults, explicit input/secret
contracts, OIDC cloud auth, caching, matrix testing, and automatic rollback on failure.
Status / honesty note. These are real, syntactically valid, actionlint-clean reusable workflows. They have not been executed against a live GitHub Actions run with a real GCP project / GKE cluster in this repo — no OIDC/WIF is wired here. The YAML is correct and
examples/shows exactly how a consumer wires and calls each workflow. Nothing here is "tested in production"; it is production-shaped.
When every service repo owns its own copy of pipeline YAML, you get:
- Duplication & drift — 40 repos, 40 slightly different
ci.ymlfiles. A fix in one never reaches the others. - Inconsistent security posture — some repos run Trivy, some run CodeQL, some run neither. Coverage is accidental, not guaranteed.
- Slow, error-prone upgrades — bumping an action version or changing the cache strategy means 40 PRs.
- Copy-paste secrets/permissions mistakes — over-broad
permissions:and long-lived cloud keys spread by copy-paste.
A shared reusable-workflow library solves this: pipeline logic lives once, is versioned, and consumers call it with a few lines. Security scanning, OIDC auth, caching, and rollback become defaults every repo inherits instead of choices every team re-makes.
Consumers keep a tiny caller workflow; the heavy lifting lives here and is pinned by tag.
flowchart TB
subgraph consumer["Consumer repo"]
cw[".github/workflows/ci-cd.yml (caller)"]
end
subgraph lib["github-actions-devops-templates"]
py[python-ci] & node[node-ci] & tf[terraform-ci]
db[docker-build] & sec[security-scan] & gke[deploy-gke] & rel[release]
end
cw -->|uses: ...@v1| py & db & sec & gke
db -->|push image| reg[(Registry)]
sec -->|SARIF| ghsec[(Code scanning)]
gke -->|OIDC / WIF| gcp[(GCP + GKE)]
See ARCHITECTURE.md for the full stage graph and job-dependency diagrams.
| Area | Tools |
|---|---|
| Orchestration | GitHub Actions reusable workflows (workflow_call) |
| Cloud auth | OIDC / Workload Identity Federation (google-github-actions/auth) |
| Build | Docker Buildx + GHA layer cache, docker/build-push-action |
| Deploy | Helm on GKE (gke-gcloud-auth-plugin, get-gke-credentials) |
| SAST | CodeQL |
| Vulnerabilities | Trivy (filesystem + image) |
| Secrets | Gitleaks |
| IaC scanning | Checkov / tfsec |
| Lint | ruff/flake8 (Python), eslint (Node), terraform fmt + tflint |
| Releases | Conventional Commits → semver tag → GitHub Release |
| Supply chain | Dependabot (github-actions, pip, npm, terraform, docker) |
| Local validation | actionlint (+ Python YAML fallback) |
.github/workflows/
python-ci.yml node-ci.yml terraform-ci.yml
docker-build.yml security-scan.yml deploy-gke.yml release.yml
.github/dependabot.yml
examples/ # caller workflows (python-app-ci, node-app-ci, terraform-infra-ci,
# app-ci-cd chained pipeline, release)
docs/ # one contract doc per workflow + pipeline-stages.md
scripts/ # validate-workflows.sh, bump-release.sh
tests/ # bump-release_test.sh
Makefile ARCHITECTURE.md SECURITY.md CONTRIBUTING.md LICENSE
# Validate all workflow YAML (uses actionlint if present, else a Python fallback)
make lint
# Install actionlint locally, then lint with it
make install-actionlint
export PATH="$PWD/bin:$PATH"
make lint
# Quick YAML parse sanity check + script tests
make fmt
make testmake lint runs scripts/validate-workflows.sh, which
prefers actionlint and otherwise parses every workflow with PyYAML and checks that each
reusable workflow declares on.workflow_call.
One-time setup on the GCP side (Terraform or gcloud):
- Create a Workload Identity Pool and a Provider for GitHub's OIDC issuer
(
https://token.actions.githubusercontent.com). - Add an attribute condition binding the provider to your repo, e.g.
attribute.repository == 'iarsingh/sample-app'. - Create a deploy-only service account (
roles/container.developer) and allow the WIF principal to impersonate it (roles/iam.workloadIdentityUser).
Then the consumer's caller grants id-token: write and calls the workflow:
permissions:
contents: read
id-token: write
jobs:
deploy:
uses: iarsingh/github-actions-devops-templates/.github/workflows/deploy-gke.yml@v1
with:
environment: production
gcp-workload-identity-provider: projects/123/locations/global/workloadIdentityPools/github-pool/providers/github-provider
gcp-service-account: gke-deployer@my-project.iam.gserviceaccount.com
gcp-project-id: my-project
cluster-name: platform-production
cluster-location: europe-west1
release-name: sample-app
chart-path: ./charts/sample-app
namespace: sample-app
image: ghcr.io/iarsingh/sample-app:sha-abc123
health-check-url: https://sample-app.example.com/healthzNo static keys are stored anywhere. See SECURITY.md.
Full stage order (see docs/pipeline-stages.md):
Checkout → Lint → Unit test → Coverage → SAST → Dependency scan
→ Docker build → Container scan → Push image → Deploy → Smoke test → Rollback on failure
Chained end-to-end in examples/app-ci-cd.yml:
ci (python-ci) → build (docker-build) → security (security-scan)
→ deploy-staging (deploy-gke) → deploy-production (deploy-gke, gated)
docker-build exposes image-tag / image-digest as outputs; security-scan and both
deploy jobs consume needs.build.outputs.image-tag, so the exact built artifact is what
gets scanned and shipped.
- OIDC / WIF for all GCP access — no long-lived keys.
- Least-privilege service accounts, ideally one per environment.
- Explicit secret contracts per workflow;
secrets: inheritavoided. - Scanning by default: CodeQL (SAST), Trivy (fs + image), Gitleaks (secrets), Checkov/tfsec (IaC) — all uploading SARIF to the Security tab.
- Environment approval gates for production deploys (Required reviewers).
- Dependabot across github-actions, pip, npm, terraform, docker.
- Pinned action versions, movable to commit SHAs for higher assurance.
Details in SECURITY.md.
- Each deploy writes a job summary table (
$GITHUB_STEP_SUMMARY) with release, namespace, cluster, image, and deploy/smoke-test outcomes — visible on the run page. - The smoke test curls the health endpoint up to N times; a failure produces an
::error::annotation and flips the job red, which surfaces in the Actions run list, commit status, and (if enabled) branch protection. - Security findings land in the repo Security → Code scanning tab, categorized per tool, independent of whether the job hard-fails.
matrixjobs report per-version status so a single failing runtime is obvious.
deploy-gke.yml rolls back automatically:
- name: Rollback on failure
if: failure() && steps.deploy.outcome == 'success'
run: helm rollback "<release>" 0 --namespace "<ns>" --wait --timeout 5m- If
helm upgradesucceeded but the smoke test failed, Helm rolls back to the previous good revision. - If the first
helm upgradefailed (no prior revision), the guard skips rollback — there is nothing to revert to — and the job simply fails. - A consumer re-runs a failed pipeline with Re-run failed jobs on the run page; because builds are content-addressed and layer-cached, the rerun reuses cache and is fast. For a gated prod deploy, the environment approval is requested again on rerun.
- Caching cuts minutes: pip/npm caches (
setup-python/setup-node) and Buildx GHA layer cache (cache-from/to: type=gha) avoid redundant reinstalls and rebuilds. - Right-size matrices: default to the versions you actually ship;
app-ci-cd.ymlruns a single Python version for the release path and reserves the wider matrix for PR CI. fail-fast: falsetrades a few extra minutes for complete signal — tune per repo.- Concurrency control (
concurrency:in the example) cancels or serializes superseded runs to avoid paying for stale work. - Self-hosted runners are a drop-in option: set
runs-on(every workflow exposes it) to a self-hosted label to move heavy or long-running builds off per-minute GitHub-hosted billing while keeping the same workflow code.
Copy-pasteable caller (single-workflow):
name: CI
on:
push: { branches: [main] }
pull_request: { branches: [main] }
jobs:
python-ci:
uses: iarsingh/github-actions-devops-templates/.github/workflows/python-ci.yml@v1
with:
python-version-matrix: '["3.11", "3.12"]'
lint-tool: ruff
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}Full chained pipeline: see examples/app-ci-cd.yml.
Sample run summary (what a green run looks like):
App CI/CD · #128 · main · ✓ 6m 12s
├─ ci ✓ python 3.12 lint ✓ pytest ✓ (coverage 91%)
├─ build ✓ ghcr.io/iarsingh/sample-app:sha-abc123 digest sha256:…
├─ security ✓ CodeQL ✓ Trivy fs ✓ Trivy image ✓ Gitleaks ✓ (0 CRITICAL)
├─ deploy-staging ✓ helm upgrade ✓ smoke test 200 OK (attempt 2/10)
└─ deploy-production ✓ (approved by @reviewer) helm upgrade ✓ smoke test 200 OK
On a smoke-test failure the production job goes red, the Rollback on failure step runs
helm rollback, and the job summary records Smoke test outcome: failure.
| Workflow | Doc |
|---|---|
python-ci.yml |
docs/python-ci.md |
node-ci.yml |
docs/node-ci.md |
terraform-ci.yml |
docs/terraform-ci.md |
docker-build.yml |
docs/docker-build.md |
security-scan.yml |
docs/security-scan.md |
deploy-gke.yml |
docs/deploy-gke.md |
release.yml |
docs/release.md |
MIT.