Skip to content

YourBOX CI/CD

The 2020 YourBOX pipeline runs three stages (test, init, plan, apply, build, deploy) targeting Azure only, with the test job pinned to PHP 8.1 and no security gate. The modernization rewires the pipeline to:

  • Consume the reusable components from gitlab-ci-templates for lint, test, security and build.
  • Pass the Plumber supply chain controls from ADR-0009.
  • Produce one image consumed by every deployment target documented in ADR-0010.
  • Emit DORA metrics into the workspace dashboards.
flowchart LR
Lint[Lint]
Test[Test]
Build[Build image]
Scan[Container hardening]
Sign[Sign image]
SmokeCompose[Smoke compose]
DeployVps[Deploy VPS]
E2eVps[E2E VPS]
DeployAzure[Deploy Azure]
DeployK8s[Deploy Kubernetes]
Dora[DORA metrics]
Lint --> Test --> Build --> Scan --> Sign --> SmokeCompose --> DeployVps
DeployVps --> E2eVps --> DeployAzure --> DeployK8s --> Dora

The pipeline is linear by default. Merge requests stop at the smoke compose stage. Deploy stages run only on the protected main branch.

YourBOX consumes the components from gitlab-ci-templates through immutable references. The component repository owns the version contract; the application repository selects a version per included file.

include:
- project: yourbox/gitlab-ci-templates
ref: v1.4.0
file: /templates/lint.gitlab-ci.yml
- project: yourbox/gitlab-ci-templates
ref: v1.4.0
file: /templates/test.gitlab-ci.yml
- project: yourbox/gitlab-ci-templates
ref: v1.4.0
file: /templates/security.gitlab-ci.yml
- project: yourbox/gitlab-ci-templates
ref: v1.4.0
file: /templates/build-image.gitlab-ci.yml
- project: yourbox/gitlab-ci-templates
ref: v1.4.0
file: /templates/dora.gitlab-ci.yml
- project: external-tools/plumber
ref: v0.3.4
file: /templates/plumber.yml
inputs:
config_file: ".plumber.yaml"
threshold: 100
output_file: "plumber-report.json"
fail_warnings: true
verbose: true
branch: $CI_COMMIT_REF_NAME

The current gitlab-ci-templates repository ships Bun and Node templates only. A PHP and Symfony variant is added under templates/test.gitlab-ci.yml to host the YourBOX test job patterns. The variant is documented in the gitlab-ci-templates README and reviewed before consumption.

The lint stage runs five jobs in parallel:

  • lint:php-cs-fixer runs vendor/bin/php-cs-fixer fix --dry-run --diff.
  • lint:phpstan runs vendor/bin/phpstan analyse --memory-limit=512M at level 8.
  • lint:hadolint runs hadolint Dockerfile and the sidecar nginx and php-fpm Dockerfiles if any.
  • lint:yaml runs yamllint -d relaxed . across the workspace.
  • lint:gitleaks runs gitleaks detect --no-git --source . to catch staged secrets.

Each job uses a digest pinned image per ADR-0009. Failures block the pipeline.

The test stage runs three jobs:

  • test:unit runs composer test:unit against an empty PHP container. No database, no storage. The job completes in under 30 seconds.
  • test:integration brings up a digest pinned PostgreSQL service and runs composer test:integration. The job seeds the schema with the Doctrine migrations, applies the data fixtures and exercises the repository ports.
  • test:functional boots the Symfony kernel through WebTestCase and exercises the public and admin routes against a SQLite kernel.

A coverage gate fails the pipeline below 70 percent overall on app/src/, with 90 percent on Domain/ and Application/.

test:integration:
image: docker.io/library/php:8.5-cli-alpine@sha256:<digest>
services:
- name: docker.io/library/postgres:16-alpine@sha256:<digest>
alias: postgres
variables:
DB_DSN: "postgresql://postgres:postgres@postgres:5432/yourbox_test"
before_script:
- apk add --no-cache git zip unzip ffmpeg
- composer install --no-interaction --no-progress
- php bin/console doctrine:database:create --if-not-exists -e test
- php bin/console doctrine:migrations:migrate --no-interaction -e test
script:
- composer test:integration
coverage: '/^\s*Lines:\s+(\d+\.\d+)%/'
artifacts:
when: always
paths:
- build/coverage.xml
reports:
coverage_report:
coverage_format: cobertura
path: build/coverage.xml

The build stage runs in a Kaniko or Buildah job (rootless) because the GitLab runner does not have privileged Docker access. The output image is pushed to registry.yourbox.site/yourbox:<commit_sha> and is also tagged with the semantic version on release branches.

The scan stage runs Trivy against the freshly built image:

scan:trivy:
image:
name: docker.io/aquasec/trivy:0.70.0-amd64@sha256:<digest>
entrypoint: [""]
needs:
- build:image
script:
- trivy image --severity HIGH,CRITICAL --exit-code 1 \
registry.yourbox.site/yourbox:$CI_COMMIT_SHA
artifacts:
when: always
paths:
- trivy.json

A scan:gitleaks job runs against the built image filesystem for defense in depth. A scan:sbom job generates a CycloneDX SBOM and stores it as an artifact.

The sign stage attaches a cosign signature and a CycloneDX SBOM attestation to the image. The signing key lives in OpenBao under kv/yourbox/cosign-key and is fetched at job time through the JWT auth method.

sign:image:
image:
name: docker.io/anchore/sbom-action:0.17.0@sha256:<digest>
entrypoint: [""]
needs: [scan:trivy]
script:
- cosign sign --key env://COSIGN_KEY \
registry.yourbox.site/yourbox@$IMAGE_DIGEST
- cosign attest --predicate sbom.json --type cyclonedx --key env://COSIGN_KEY \
registry.yourbox.site/yourbox@$IMAGE_DIGEST

The homelab Kubernetes cluster enforces the cosign signature through a Sigstore policy controller. An unsigned image is rejected by admission.

The Plumber job runs early in the pipeline as a lint stage entry. The score threshold is 100, which fails on any control regression. A failure produces a plumber-report.json artifact and a merge request widget with the breakdown.

The current Plumber controls active for YourBOX are listed on the Verification Baseline page. They include the digest pinning rules, the registry allowlist, the protected branches rules and the includes freshness rules.

The dora.gitlab-ci.yml template emits the deployment frequency, lead time for changes, mean time to restore service and change failure rate signals. The metrics flow to the Grafana dashboards documented on the Observability page.

Per ADR-0010 the same image deploys to four targets in order. Each deploy stage runs only on the protected main branch, requires manual approval and emits a GitLab Release on success.

Stage Target Surface
SmokeCompose docker compose CI smoke test on every merge to main
DeployVps VPS via Ansible public smoke test, manual approval
E2eVps VPS Cypress E2E suite against the VPS URL
DeployAzure Azure App Service staging, manual approval
DeployK8s Homelab k8s Argo CD reconciliation after manifest MR

Argo CD does not pull from GitLab directly. The deploy step opens a Merge Request against homelab-platform/kubernetes/apps/yourbox/ updating the image digest in the Helm release. A reviewer merges the change and Argo CD picks it up. The pattern matches the weather-app delivery flow.

  • A lint or test failure blocks the pipeline before the build stage.
  • A scan failure blocks the deploy stages and reports the CVE list as a job artifact.
  • A Plumber failure blocks every stage that runs after the lint stage.
  • A deploy failure on an environment freezes the next environments and posts to the GitLab MR with the runbook link.