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/write permissions
  • GITHUB_TOKEN or GHCR_TOKEN secret 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:

InputRequiredDescription
app_nameyesService name (e.g. api, worker)
image_nameyesFull image name (e.g. org/api)
dockerfileyesPath to Dockerfile
contextyesDocker build context path
version_fileyesPath to version file
version_typenofile (default) or package-json
registrynoContainer registry (default: ghcr.io)
version_bumpnomajor, 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 SARIF

Pipeline Steps

  1. Checkout with submodules and full history
  2. Version detection — read current version from file or package.json
  3. Version bump — semver increment based on commit message or manual input:
    • [major] or breaking change → major
    • feat: or [minor] → minor
    • Everything else → patch
    • Feature branches get -draft-{branch-slug} suffix
  4. Multi-arch build — QEMU + Buildx for linux/amd64,linux/arm64
  5. Push to registry — versioned tag always, latest tag on main only
  6. Security scan — Trivy scans the pushed image for CRITICAL/HIGH vulns, uploads SARIF to GitHub Security tab
  7. Version commit — on main, commits the bumped version file with [skip ci]
  8. 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=max

Step 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: inherit

Step 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>  # v4

Minimal Permissions

Set explicit permissions per workflow rather than relying on defaults:

permissions:
  contents: read

Security Checks

  • No pull_request_target with 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 field

Step 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, never latest
  • The version commit uses [skip ci] to prevent infinite build loops
  • Trivy results are uploaded per-service using the category parameter
  • 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