CI Pipeline Setup
Use this skill to set up GitHub Actions CI/CD for container images. It encodes the standard build → scan → sign → publish pattern used across CascadeGuard repos.
When to Use
- Setting up CI for a new containerized service
- Adding a Docker build workflow to an existing repo
- Reviewing or hardening an existing CI pipeline
- Creating reusable workflow components
Prerequisites
- GitHub repo with
actions/writepermissions GITHUB_TOKENorGHCR_TOKENsecret for container registry access- Dockerfile in the target repo
- Version file (plain text or
package.json)
Pipeline Architecture
The standard pipeline uses a reusable workflow pattern:
_docker-build.yml (reusable, called by service workflows)
├── Checkout
├── Version bump (semver, branch-aware)
├── Multi-arch build (amd64 + arm64)
├── Security scan (Trivy → SARIF → GitHub Security)
├── Push to GHCR
├── Commit version bump (main only)
└── Create GitHub Release (main only)
{service}.yml (caller workflow per service)
└── calls _docker-build.yml with service-specific inputs
Step 1 — Create the Reusable Docker Build Workflow
Create .github/workflows/_docker-build.yml with these inputs:
| Input | Required | Description |
|---|---|---|
app_name | yes | Service name (e.g. api, worker) |
image_name | yes | Full image name (e.g. org/api) |
dockerfile | yes | Path to Dockerfile |
context | yes | Docker build context path |
version_file | yes | Path to version file |
version_type | no | file (default) or package-json |
registry | no | Container registry (default: ghcr.io) |
version_bump | no | major, minor, or patch (default) |
Required Permissions
permissions:
contents: write # version bump commits
packages: write # push to GHCR
id-token: write # OIDC for signing
attestations: write # build attestations
security-events: write # upload Trivy SARIFPipeline Steps
- Checkout with submodules and full history
- Version detection — read current version from file or package.json
- Version bump — semver increment based on commit message or manual input:
[major]orbreaking change→ majorfeat:or[minor]→ minor- Everything else → patch
- Feature branches get
-draft-{branch-slug}suffix
- Multi-arch build — QEMU + Buildx for
linux/amd64,linux/arm64 - Push to registry — versioned tag always,
latesttag on main only - Security scan — Trivy scans the pushed image for CRITICAL/HIGH vulns, uploads SARIF to GitHub Security tab
- Version commit — on main, commits the bumped version file with
[skip ci] - GitHub Release — creates a release with the image reference
Build Caching
Use GitHub Actions cache for Docker layers:
cache-from: type=gha
cache-to: type=gha,mode=maxStep 2 — Create Service-Specific Caller Workflows
For each containerized service, create .github/workflows/{service}.yml:
name: {Service Name}
on:
push:
branches: [main]
paths:
- '{service-path}/**'
- '.github/workflows/{service}.yml'
- '.github/workflows/_docker-build.yml'
workflow_dispatch:
inputs:
version_bump:
description: 'Version bump type'
required: false
default: 'patch'
type: choice
options: [patch, minor, major]
jobs:
build:
uses: ./.github/workflows/_docker-build.yml
with:
app_name: {service-name}
image_name: {org}/{service-name}
dockerfile: {service-path}/Dockerfile
context: {service-path}
version_file: {service-path}/VERSION
version_bump: ${{ inputs.version_bump || 'patch' }}
secrets: inheritStep 3 — Supply-Chain Hardening
Apply these hardening measures to all workflow files:
SHA Pinning
Replace tag references with SHA-pinned versions:
# Find the SHA for an action tag
gh api repos/{owner}/{repo}/git/ref/tags/{tag} --jq '.object.sha'# Before
uses: actions/checkout@v4
# After
uses: actions/checkout@<full-sha> # v4Minimal Permissions
Set explicit permissions per workflow rather than relying on defaults:
permissions:
contents: readSecurity Checks
- No
pull_request_targetwith PR code checkout (supply-chain risk) - No secrets in workflow logs (use
::add-mask::) - Pin third-party actions to full SHA, not tags
Step 4 — Version File Setup
Create the version file for each service:
# Plain text version file
echo "0.1.0" > {service-path}/VERSION
# Or for Node.js services, use package.json version fieldStep 5 — Verification
After setup, verify the pipeline:
# Trigger a manual run
gh workflow run {service}.yml --ref main
# Watch the run
gh run list --workflow={service}.yml --limit=1
gh run watch $(gh run list --workflow={service}.yml --limit=1 --json databaseId -q '.[0].databaseId')
# Check the pushed image
gh api user/packages/container/{image-name}/versions --jq '.[0].metadata.container.tags'Notes
- Feature branch builds produce
-draft-{branch}tagged images, neverlatest - The version commit uses
[skip ci]to prevent infinite build loops - Trivy results are uploaded per-service using the
categoryparameter - For repos with multiple services, each gets its own caller workflow with path filters to avoid unnecessary builds
- This pattern was extracted from CAS-16 work on the CascadeGuard platform