# CI/CD Pipeline Template Save as `_docs/04_deploy/ci_cd_pipeline.md`. --- ```markdown # [System Name] — CI/CD Pipeline ## Pipeline Overview | Stage | Trigger | Quality Gate | |-------|---------|-------------| | Lint | Every push | Zero lint errors | | Test | Every push | 75%+ coverage, all tests pass | | Security | Every push | Zero critical/high CVEs | | Build | PR merge to dev | Docker build succeeds | | Push | After build | Images pushed to registry | | Deploy Staging | After push | Health checks pass | | Smoke Tests | After staging deploy | Critical paths pass | | Deploy Production | Manual approval | Health checks pass | ## Stage Details ### Lint - [Language-specific linters and formatters] - Runs in parallel per language ### Test - Unit tests: [framework and command] - Blackbox tests: [framework and command, uses docker-compose.test.yml] - Coverage threshold: 75% overall, 90% critical paths - Coverage report published as pipeline artifact ### Security - Dependency audit: [tool, e.g., npm audit / pip-audit / dotnet list package --vulnerable] - SAST scan: [tool, e.g., Semgrep / SonarQube] - Image scan: Trivy on built Docker images - Block on: critical or high severity findings ### Build - Docker images built using multi-stage Dockerfiles - Tagged with git SHA: `/:` - Build cache: Docker layer cache via CI cache action ### Push - Registry: [container registry URL] - Authentication: [method] ### Deploy Staging - Deployment method: [docker compose / Kubernetes / cloud service] - Pre-deploy: run database migrations - Post-deploy: verify health check endpoints - Automated rollback on health check failure ### Smoke Tests - Subset of blackbox tests targeting staging environment - Validates critical user flows - Timeout: [maximum duration] ### Deploy Production - Requires manual approval via [mechanism] - Deployment strategy: [blue-green / rolling / canary] - Pre-deploy: database migration review - Post-deploy: health checks + monitoring for 15 min ## Caching Strategy | Cache | Key | Restore Keys | |-------|-----|-------------| | Dependencies | [lockfile hash] | [partial match] | | Docker layers | [Dockerfile hash] | [partial match] | | Build artifacts | [source hash] | [partial match] | ## Parallelization [Diagram or description of which stages run concurrently] ## Notifications | Event | Channel | Recipients | |-------|---------|-----------| | Build failure | [Slack/email] | [team] | | Security alert | [Slack/email] | [team + security] | | Deploy success | [Slack] | [team] | | Deploy failure | [Slack/email + PagerDuty] | [on-call] | ``` --- ## Reference Implementation: Woodpecker CI two-workflow contract Use this when the project's CI is **Woodpecker** and the test layout follows the autodev e2e contract from [`../../decompose/templates/test-infrastructure-task.md`](../../decompose/templates/test-infrastructure-task.md) (an `e2e/` folder containing `Dockerfile`, `docker-compose.test.yml`, `conftest.py`, `requirements.txt`, `mocks/`, `fixtures/`, `tests/`). The contract is **two workflows in `.woodpecker/`**, scheduled on the same agent label, with the build workflow gated on a successful test run: - `.woodpecker/01-test.yml` — runs the e2e contract, publishes `results/report.csv` as an artifact, fails the pipeline on any test failure. - `.woodpecker/02-build-push.yml` — `depends_on: [01-test]`. Builds the image, tags it `${CI_COMMIT_BRANCH}-${TAG_SUFFIX}`, pushes it to the registry. Skipped automatically if test failed. The agent label is parameterized via `matrix:` so a single workflow file fans out across architectures: `labels: platform: ${PLATFORM}` routes each matrix entry to the matching agent. Both workflows for a repo must use the same matrix so test and build run on the same machine and share Docker layer cache. New architectures = new matrix entries; never new files. ### Multi-arch matrix conventions | Variable | Meaning | Typical values | |----------|---------|----------------| | `PLATFORM` | Woodpecker agent label — selects which physical machine runs the entry. | `arm64`, `amd64` | | `TAG_SUFFIX` | Image tag suffix appended after the branch name. | `arm`, `amd` | | `DOCKERFILE` *(only when arches need different Dockerfiles)* | Path to the Dockerfile for this entry. | `Dockerfile`, `Dockerfile.jetson` | Most repos use the same `Dockerfile` for both arches (multi-arch base images handle the rest), so `DOCKERFILE` can be omitted from the matrix and hardcoded in the build command. Repos with split per-arch Dockerfiles (e.g., `detections` uses `Dockerfile.jetson` on Jetson with TensorRT/CUDA-on-L4T) declare `DOCKERFILE` as a matrix var. When only one architecture is currently in use, keep the matrix block with a single entry and the second entry commented out — adding a new arch is then a one-line uncomment, not a structural change. ### `.woodpecker/01-test.yml` ```yaml when: event: [push, pull_request, manual] branch: [dev, stage, main] matrix: include: - PLATFORM: arm64 TAG_SUFFIX: arm # - PLATFORM: amd64 # TAG_SUFFIX: amd labels: platform: ${PLATFORM} steps: - name: e2e image: docker commands: - cd e2e - docker compose -f docker-compose.test.yml up --abort-on-container-exit --exit-code-from e2e-runner --build - docker compose -f docker-compose.test.yml down -v volumes: - /var/run/docker.sock:/var/run/docker.sock - name: report image: docker when: status: [success, failure] commands: - test -f e2e/results/report.csv && cat e2e/results/report.csv || echo "no report" volumes: - /var/run/docker.sock:/var/run/docker.sock ``` Notes: - `--abort-on-container-exit` shuts the whole compose down as soon as ANY service exits, so a crashed dependency surfaces immediately instead of hanging the runner. - `--exit-code-from e2e-runner` ensures the pipeline's exit code reflects the test runner's, not the SUT's. - The `report` step runs on `[success, failure]` so the report is always published; without this the CSV is lost on red builds. - `down -v` between runs drops mock state and DB volumes — every test run starts clean. ### `.woodpecker/02-build-push.yml` ```yaml when: event: [push, manual] branch: [dev, stage, main] depends_on: - 01-test matrix: include: - PLATFORM: arm64 TAG_SUFFIX: arm # - PLATFORM: amd64 # TAG_SUFFIX: amd labels: platform: ${PLATFORM} steps: - name: build-push image: docker environment: REGISTRY_HOST: from_secret: registry_host REGISTRY_USER: from_secret: registry_user REGISTRY_TOKEN: from_secret: registry_token commands: - echo "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" -u "$REGISTRY_USER" --password-stdin - export TAG=${CI_COMMIT_BRANCH}-${TAG_SUFFIX} - export BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) - | docker build -f Dockerfile \ --build-arg CI_COMMIT_SHA=$CI_COMMIT_SHA \ --label org.opencontainers.image.revision=$CI_COMMIT_SHA \ --label org.opencontainers.image.created=$BUILD_DATE \ --label org.opencontainers.image.source=$CI_REPO_URL \ -t $REGISTRY_HOST/azaion/:$TAG . - docker push $REGISTRY_HOST/azaion/:$TAG volumes: - /var/run/docker.sock:/var/run/docker.sock ``` Notes: - `depends_on: [01-test]` is enforced by Woodpecker — a failed `01-test` (any matrix entry) skips this workflow. - The build workflow does NOT trigger on `pull_request` events: PRs get test signal only; pushes to `dev`/`stage`/`main` produce images. Avoids polluting the registry with PR images. - Replace `` with the actual service name (matches the registry namespace pattern `azaion/`). - For repos with split per-arch Dockerfiles, add `DOCKERFILE: Dockerfile.jetson` (or similar) to the matrix entry and substitute `${DOCKERFILE}` for `Dockerfile` in the `docker build -f` line. ### Variations by stack The contract is language-agnostic because the runner is `docker compose`. The Dockerfile inside `e2e/` selects the test framework: | Stack | `e2e/Dockerfile` runs | |-------|----------------------| | Python | `pytest --csv=/results/report.csv -v` | | .NET | `dotnet test --logger:"trx;LogFileName=/results/report.trx"` (convert to CSV in a final step if needed) | | Node/UI | `npm test -- --reporters=default --reporters=jest-junit --outputDirectory=/results` | | Rust | `cargo test --no-fail-fast -- --format json > /results/report.json` | When the repo has **only unit tests** (no `e2e/docker-compose.test.yml`), drop the compose orchestration and run the native test command directly inside a stack-appropriate image. Keep the same two-workflow split — `01-test.yml` runs unit tests, `02-build-push.yml` is unchanged. ### Manual-trigger override (test infrastructure not yet validated) If a repo ships a complete `e2e/` layout but the test fixtures are not yet validated end-to-end (e.g., expected-results data is still being authored), gate `01-test.yml` on `event: [manual]` only and add a TODO comment pointing to the unblocking task. The `02-build-push.yml` workflow drops its `depends_on` clause for the manual-only window — an explicit and reversible exception, not a permanent split.