CI/CD Pipelines: Multi-Platform Production Infrastructure
Write, review, and architect CI/CD pipelines across GitHub Actions, GitLab CI/CD, Forgejo
Actions, Gitea Actions, and Woodpecker. The goal is secure, fast, auditable pipelines that
satisfy both engineering needs and compliance requirements (PCI-DSS 4.0).
Target versions: June 2026 snapshot. Read references/target-versions.md before
pinning forge, runner, CI, or supply-chain tool versions.
This skill covers workflow design, security, compliance, cross-platform migration,
runners, dependency updates, scanning, review gates, and rollout order.
When to use
Writing or reviewing CI/CD pipeline configs (GitHub/Forgejo/Gitea Actions, .gitlab-ci.yml, .woodpecker/*.yaml)
Migrating pipelines between platforms (GitLab -> GitHub, GitHub -> Forgejo)
Troubleshooting failed pipelines, flaky jobs, or runner issues
When NOT to use
Kubernetes manifests, Helm charts, cluster architecture - use kubernetes
Dockerfiles, Compose stacks, container image optimization - use docker
Terraform/OpenTofu infrastructure-as-code - use terraform
Ansible playbooks, configuration management - use ansible
Security audits of application code (SAST findings, auth bugs) - use security-audit
Code review of pipeline-adjacent code (the app itself) - use code-review
The code-review skill has a cicd-pipelines.md reference for bug patterns in existing
pipelines. This skill is for writing and architecting pipelines.
AI Self-Check
AI tools consistently produce the same CI/CD mistakes. Before returning any generated
pipeline config, verify against this list.
Review mode: if auditing an existing pipeline rather than generating one, invert this
checklist - each item that fails is a finding. Work through the list top-to-bottom and report
every failure with file and line reference.
SHA pinning: all third-party actions/images pinned to full commit SHA or digest, not mutable tags. Add # vX.Y.Z comment for readability.
Permissions: explicit permissions: block on every GitHub Actions workflow (read-only default). GitLab: protected variables scoped correctly.
No secrets in config: no hardcoded tokens, passwords, or API keys. Use CI/CD secret variables or vault integration.
No latest tags: runner images, tool images, and base images pinned to specific versions or SHA256 digests.
Fail-fast security: SAST, dependency scanning, and secret detection run early (not after deployment).
Manual gates for production: production deployments require explicit approval (not auto-deploy on merge).
SBOM generation: release pipelines generate and attach SBOMs (SPDX or CycloneDX). Required for PCI-DSS 4.0.
Minimal scope: jobs have minimum required permissions, access only needed secrets, and run only needed steps.
No allow_failure without justification: if a job can fail, explain why in a comment.
Version pinning on tools: node:22, not node:lts. python:3.13, not python:3. Specific versions prevent silent breakage.
Trigger scoping: on: push without branch/path filters runs on every push to every branch - scope to branches: [main] and/or paths: filters. Same for GitLab: rules: with if conditions, not bare only: [pushes].
No expression injection (GitHub Actions): ${{ }} expressions never used directly in run: blocks. Assign to env: first. github.event.* is attacker-controlled. Avoid github.ref_name in security-sensitive contexts (injectable via crafted tag/branch names).
Self-hosted runners ephemeral on public/untrusted repos: non-ephemeral shell runners on repos that accept outside PRs is the top self-hosted-runner compromise vector. Verify --ephemeral (GitHub, Gitea) or capacity-based single-job runners (Forgejo) + approval gates for outside contributors. See references/runners.md.
Docker socket mount scope: /var/run/docker.sock mounted into a job gives it root on the host. Only acceptable for trusted internal pipelines. Public/shared runners need DinD sidecar or rootless buildkit instead.
Scan gate has a baseline, not a blanket block: container/IaC/SAST scanners introduced with exit-code 1 and zero suppression always get disabled. Use the ratchet pattern (non-blocking -> baseline -> block new only) from references/best-practices.md.
Ignore-list entries have expiry dates: every .trivyignore, .grype.yaml, Dependabot ignore, or Renovate ignoreDeps entry includes a comment with revisit date + owner. No dates = zombie tech debt.
Lockfiles committed: package-lock.json, bun.lock, Cargo.lock, go.sum, uv.lock belong in version control for applications. Manifest-only commits break reproducibility.
Auto-merge gated on tests, not just lint: Dependabot/Renovate auto-merge without test coverage of the changed area is a supply-chain shortcut.
Current source checked: dated versions, CLI flags, API names, and support windows are verified against primary docs before repeating them
Hidden state identified: local config, credentials, caches, contexts, branches, cluster targets, or previous runs are made explicit before acting
Verification is real: final checks exercise the actual runtime, parser, service, or integration point instead of only linting prose or happy paths
Routing overlap checked: overlapping skills, trigger terms, and "When NOT to use" boundaries are checked before returning guidance
Spec claims verified: claims about tool behavior, output contracts, or repo conventions are checked against current docs, scripts, or skill files
Rule: cache is a speed optimization, not a correctness mechanism. Artifacts are for
inter-job data. Cache may evict at any time - pipelines must work without it.
Protected branches/tags, environments, masked in logs.
Forgejo
Repository/org secrets
Per-repo, per-org. No environment scoping yet.
All platforms: never echo secrets, never pass as CLI args (visible in ps), never write
to artifacts. Use environment variables or file-based injection.
Per-service workflows with paths: filters (simplest, recommended)
Single workflow with matrix + change detection job that outputs which services need building
Rule: always rebuild when the shared lib changes. A "nothing changed" optimization that misses a shared dependency is worse than rebuilding everything.
For Python monorepos with multiple services sharing a common library (libs/common/):
Cache the resolver output, not the install step. Key on hashFiles('**/requirements*.txt') or **/poetry.lock/**/uv.lock. With uv or pip, cache ~/.cache/uv or ~/.cache/pip plus each service's .venv/ keyed on the service path + lockfile hash.
Install the shared lib editable (pip install -e libs/common) so services import the in-repo version, not a stale wheel.
Scope jobs per service with path filters. GitLab: rules: - changes: paths: ['services/api/**', 'libs/common/**'] compare_to: refs/heads/main. GitHub/Forgejo: on.push.paths / on.pull_request.paths. Always include libs/common/** in every service's filter so a shared-lib change triggers all services.
YAML anchors (GitLab) / reusable workflows (GitHub) for the per-service job template. Three near-identical blocks for api, worker, scheduler is a maintenance trap.
See references/gitlab-ci.md for a full monorepo .gitlab-ci.yml (YAML anchors, compare_to, per-service change rules, shared-lib detection).
Forgejo CI/CD
Forgejo Actions is "designed to be familiar, not designed to be compatible" with GitHub Actions.
It reuses the workflow syntax but makes no compatibility guarantees.
Key differences from GitHub Actions
Feature
GitHub Actions
Forgejo Actions
permissions:
Controls GITHUB_TOKEN scope
Not enforced - token always has full rw (read-only for fork PRs)
continue-on-error: (job level)
Allows job failure without failing workflow
Not supported - step-level only
Runner images
Managed ubuntu-24.04 with 200+ tools
Self-hosted, typically lean Debian/Alpine
Action resolution
actions/checkout@v4 -> github.com
Resolves from Forgejo mirror (configurable)
OIDC
permissions: id-token: write
enable-openid-connect key
Workflow call defaults
inputs.<id>.default populated
Always empty
Matrix + dynamic runs-on
Supported
Supported since v14.0
LXC execution
Not supported
Supported (Forgejo-specific)
Forgejo workflow template
name:CIon:push:branches: [main]
pull_request:jobs:ci:runs-on:docker# self-hosted runner labelcontainer:image:oven/bun:1.2# pin to minor version minimumsteps:-uses:actions/checkout@<sha># pin to SHA; resolves from Forgejo mirror-run:buninstall--frozen-lockfile-run:bunrunlint-run:bunruntypecheck-run:bunruntest
Forgejo action SHA discovery
Forgejo resolves actions from its own mirror or a configured upstream, not from github.com.
Finding the correct SHA for a self-hosted mirror requires different steps than GitHub.
Find the SHA on your Forgejo instance:
# List tags and their SHAs from the Forgejo mirror
git ls-remote https://forgejo.example.com/actions/checkout.git 'refs/tags/v4*'# Or use the Forgejo API to get a tag's commit SHA
curl -s https://forgejo.example.com/api/v1/repos/actions/checkout/git/refs/tags/v4.2.2 \
| jq -r '.object.sha'
If your instance mirrors from code.forgejo.org (the default upstream):
# Clone at the specific SHA and inspect
git clone --depth 1 https://forgejo.example.com/actions/checkout.git /tmp/checkout-verify
cd /tmp/checkout-verify
git checkout <sha>
# Review action.yml and dist/ - compare against the known-good upstream release
Key differences from GitHub SHA discovery:
The same action (e.g., actions/checkout) may have different SHAs on Forgejo mirrors vs GitHub
because Forgejo forks maintain their own commits
code.forgejo.org/actions/* repos are Forgejo-maintained forks, not exact copies of GitHub repos
Always verify SHAs against your own instance, not against github.com
If the action repo is not mirrored yet, an admin must add it to the Forgejo mirror list
Forgejo-specific gotchas
No ubuntu-latest - runs-on maps to your registered runner labels (e.g., docker)
Missing tools - Forgejo runner containers are lean. Add apt-get install for git, curl, etc.
TLS certs - if Forgejo uses self-signed or internal CA certs, configure the runner's trust
store (GIT_SSL_CAINFO=/path/to/ca-bundle.crt) or install the CA into the container image.
GIT_SSL_NO_VERIFY=true is a last resort for dev/test only - never normalize TLS bypass in production
Third-party actions - many GitHub Marketplace actions use GitHub-specific API calls and will silently fail
Secrets in Forgejo - ${{ secrets.* }} works, but no environment-level scoping
permissions: not enforced - Forgejo parses the field but does not restrict the workflow token.
The token always has full read-write access (read-only for fork PRs only). Don't assume
least-privilege from permissions: alone - it has no effect on Forgejo.
Managing Forgejo Actions with fj
The community Forgejo CLI (fj, v0.4.1+) covers the day-to-day Actions surface: listing
runs, dispatching workflows, and managing variables/secrets. It is much faster than the web
UI for bulk secret updates and scriptable for one-shot runs. Install and auth details live
in the git skill's forge-workflows.md reference.
# List recent runs (for a quick "is CI green on main?" check)
fj actions tasks
# Trigger a workflow_dispatch run without opening the browser
fj actions dispatch publish.yaml main --inputs version=1.2.3
# Bulk variable/secret management (writes to the repo scope)
fj actions variables create CACHE_BUCKET gs://my-bucket
fj actions secrets create REGISTRY_TOKEN "$REGISTRY_TOKEN"
What fj does not do yet (as of 0.4.1): stream runner logs, re-run failed jobs, cancel
running tasks. For those, use the web UI or hit /api/v1/repos/{owner}/{repo}/actions/tasks/{id}
directly. Log streaming across the fleet still belongs in your observability stack, not fj.
On Gitea instead of Forgejo? Use tea (gitea.com/gitea/tea) - the Gitea CLI covers
a similar surface (issues, PRs, releases) against any Gitea 1.20+ instance. Gitea Actions
lacks fj-equivalent CLI tooling; use the web UI or API. If you're running Forgejo,
prefer fj - it tracks Forgejo-specific behavior (AGit, Forgejo Actions quirks) that
tea does not.
Gitea CI/CD
Gitea ships two viable CI paths: Gitea Actions (same act-based engine as Forgejo
Actions, since Gitea 1.21) and Woodpecker CI (separate service, container-native,
webhook-driven). Drone is legacy - do not start new installs.
Quick rule of thumb: if you are migrating from GitHub or want one service to operate,
use Gitea Actions. If you need proper matrix builds, caching primitives, or lighter
resource usage, use Woodpecker. Do not run both against the same repo.
See references/forgejo-gitea-actions.md for: action SHA discovery, Gitea-vs-Forgejo Actions
differences, Woodpecker YAML examples, plugin vs command steps, OAuth setup, matrix
patterns, and Drone migration guidance.
Forgejo release workflow pattern
name:Releaseon:push:tags: ['v*']
jobs:build-and-push:runs-on:dockercontainer:image:catthehacker/ubuntu:act-24.04# heavier image for multi-tool needs# Private-forge TLS: mount your CA and set GIT_SSL_CAINFO=/path/to/ca.crt.# GIT_SSL_NO_VERIFY is a dev/test-only last resort - never commit it to a release pipeline.steps:-uses:actions/checkout@<sha># pin to SHA; resolves from Forgejo mirror-name:Logintoregistryenv:TOKEN:${{secrets.REGISTRY_TOKEN}}HOST:${{secrets.REGISTRY_HOST}}USER:${{secrets.REGISTRY_USER}}run:echo"$TOKEN"|dockerlogin"$HOST"-u"$USER"--password-stdin-name:Buildandpushenv:REGISTRY:${{secrets.REGISTRY_HOST}}/${{secrets.REGISTRY_IMAGE}}TAG:${{github.ref_name}}run:|
docker build -t "$REGISTRY:$TAG" .
docker push "$REGISTRY:$TAG"
Note: use secrets for registry host/image to avoid hardcoding private domains in git history.
PCI-DSS 4.0: CI/CD Compliance Mapping
All future-dated requirements became mandatory March 31, 2025.
PCI-DSS Req
What it means for CI/CD
Implementation
6.2.1
Secure development training + OWASP-aware processes
SAST on every PR/MR, dependency scanning, secret detection
6.2.4
Access control + change tracking
Branch protection, required reviewers, signed commits, audit logs
6.3.2
Software inventory (SBOM)
Generate SPDX/CycloneDX SBOM per release, attach to artifacts
6.4.2
Changes approved, documented, tested
Gated deployments, required approvals for prod, IaC audit trails
6.5.3
Consistent security controls across environments
Same scanning in dev/staging/prod, not just prod
Customized Approach (v4.0.1): automated CI/CD controls can satisfy manual review requirements
if properly documented. An automated SAST/DAST/SCA gate with evidence = equivalent to manual
code review for QSA assessment.
Read references/supply-chain.md for detailed PCI-DSS compliance patterns.
AI-Age Considerations
AI tools consistently generate insecure CI/CD configs: unpinned actions, missing permissions:
blocks, allow_failure: true without justification, :latest tags, secrets in run: blocks.
Always run the AI Self-Check against AI-generated pipeline code.
For detailed coverage of slopsquatting, AI agents in CI/CD, prompt injection in pipelines, and
the OWASP Top 10 for Agentic Applications, read references/supply-chain.md
(AI-Age Supply Chain Risks section).
Template Conventions
@<sha> in GitHub Actions templates is a placeholder. Replace with the real 40-character
commit SHA for the indicated version. Look up SHAs on the action's releases page or use
Dependabot to manage them automatically.
Image tags in templates use floating minor versions (e.g., oven/bun:1.2, docker:27.5)
for readability. For production, pin to a specific patch version or SHA256 digest. The templates
show the minimum acceptable granularity, not the ideal.
Reference Files
references/github-actions.md - GitHub Actions patterns, templates, and security hardening
references/target-versions.md - June 2026 version snapshot for forges, runners, CI systems, and supply-chain tools
Output Contract
See references/output-contract.md for the full contract.
Skill name: CI-CD
Deliverable bucket:audits
Mode: conditional. When invoked to analyze, review, audit, or improve existing repo content, emit the full contract - boxed inline header, body summary inline plus per-finding detail in the deliverable file, boxed conclusion, conclusion table - and write the deliverable to docs/local/audits/ci-cd/<YYYY-MM-DD>-<slug>.md. When invoked to answer a question, teach a concept, build a new artifact, or generate content, respond freely without the contract.
Severity scale:P0 | P1 | P2 | P3 | info (see shared contract; only used in audit/review mode).
Related Skills
code-review - has a cicd-pipelines.md reference for CI/CD bug patterns (expression
injection, variable scoping, cache gotchas, ArgoCD sync issues)
security-audit - for auditing application code, not pipeline code
docker - for Dockerfile and container image optimization
kubernetes - for K8s manifests and Helm charts that pipelines deploy to
git - for git operations (commits, PRs/MRs, tags, releases) that trigger pipelines.
CI/CD reacts to git events; git handles the operations that produce them.
Rules
Platform-first. Always confirm which CI/CD platform before writing config. GitHub Actions
syntax that "mostly works" in Forgejo will silently break on edge cases.
SHA-pin everything. All third-party actions, all CI tool images. Tags are mutable. SHAs are not.
The tj-actions, reviewdog, and Trivy compromises proved this is non-negotiable.
Secrets are sacred. Never log, echo, artifact, or pass as CLI arguments. Never use
protected variables on unprotected branches.
Test the pipeline itself.act (GitHub Actions local runner), gitlab-ci-local, or dry-run
modes. Don't discover pipeline bugs in production.
Cache != artifact. Cache is ephemeral speed optimization. Artifacts are guaranteed inter-job
data. Confusing them causes intermittent failures.
Manual gates for prod. No exceptions. Auto-deploy to staging is fine. Auto-deploy to
production is how incidents happen.
Scan early, deploy late. Security scanning in the first stages, deployment in the last.
Finding a CVE after deployment is expensive.
PCI-DSS 4.0 is mandatory. If the pipeline touches CDE (cardholder data environment),
SBOM generation, signed artifacts, and gated deployments are not optional.