CI Architecture¶
This guide explains the continuous integration model used across all
mq-rest-admin-* repositories and the vergil-tooling ecosystem, and
how to implement it in new projects.
Table of Contents¶
- Overview
- Tier 1: Local pre-commit
- Tier 2: PR CI
- Architecture
- Implementation guide
- CI gates
- Dev container images
Overview¶
Testing is split into two tiers with increasing scope, cost, and feedback latency:
| Tier | Trigger | Time | Security |
|---|---|---|---|
| 1 | Manual (before commit) | Seconds | No |
| 2 | Pull request | ~8-10 min | Yes |
- Tier 1: Single version, full local validation, dockerized
- Tier 2: Full version matrix, all checks, security uploads
The goal is fast local feedback for the developer and comprehensive gated
checks on the PR itself. The pre-commit gate in .githooks enforces that
Tier 1 runs before any commit lands, so by the time a PR opens, it should
already be green.
Historical: three-tier CI
Earlier versions of this guide documented a third tier — push-CI — as
a thin workflow_call wrapper that ran a subset of checks on every
push to a feature branch. That tier was removed once vrg-validate
reached parity with PR-CI; the push-CI workflow added no coverage that
PR-CI didn't already provide and created a concurrency-group deadlock
with ci.yml. Integration-test coverage at push-time was deliberately
dropped and is tracked separately as future work on local integration
testing. See vergil-project/vergil-actions#176 for the parity audit
and removal rationale.
Tier 1: Local pre-commit¶
Run in a dev container on the developer's machine. Docker is the only host prerequisite.
./scripts/dev/test.sh # Unit tests + linting
./scripts/dev/lint.sh # Lint and formatting checks
./scripts/dev/audit.sh # Dependency and license audit
Each script follows the same pattern:
- Set
DOCKER_DEV_IMAGE(default:dev-<language>:<latevrg-version>) - Set
DOCKER_TEST_CMD(language-specific command) - Delegate to
vrg-docker-testif available, otherwise rundocker rundirectly
Environment overrides:
DOCKER_DEV_IMAGE— use a different container imageDOCKER_TEST_CMD— override the test command
Tip
Build the dev images locally before first use:
cd ../vergil-docker && docker/build.sh
The .githooks pre-commit gate runs vrg-docker-run -- uv run vrg-validate
on every commit, which runs common checks and per-language validation. Hook
bypass (--no-verify) is disallowed by policy.
Tier 2: PR CI¶
Triggers on pull_request events. Runs the full validation suite.
What runs:
- Unit tests across the full version matrix
- Integration tests across the full version matrix
- Security scanners (CodeQL, Trivy, Semgrep) via shared reusable workflow
- Standards compliance
- Dependency audit
- Release gates (version divergence, format validation)
The workflow file is .github/workflows/ci.yml, which runs directly on
pull_request and is also exposed as a reusable workflow via
workflow_call for any specialized callers (release pipelines, etc.).
Architecture¶
Reusable workflow pattern¶
ci.yml accepts workflow_call with inputs that control scope:
| Input | Type | Default |
|---|---|---|
versions |
string (JSON) | Full matrix |
integration-matrix |
string (JSON) | Full matrix |
run-security |
string | "true" |
run-release-gates |
string | "true" |
versions— language versions to testintegration-matrix— test entries with portsrun-security— enable security scannersrun-release-gates— enable release gate checks
When triggered directly by pull_request, all inputs are empty and
defaults produce the full Tier 2 behavior. The inputs remain in place so
specialized callers (e.g., release pipelines) can constrain scope when
needed.
String inputs, not booleans
Use type: string for gate inputs, not type: boolean. Boolean
inputs are unreliable for job-level if conditions when the
workflow is triggered directly (inputs are empty, not false).
Use != 'false' comparisons instead.
Shared security workflow¶
Security scanners and standards compliance are factored into a shared
reusable workflow at
vergil-project/vergil-actions/.github/workflows/ci-security.yml.
This provides four jobs:
ci: standards-compliancesecurity: codeqlsecurity: trivysecurity: semgrep
Call it from ci.yml:
security-and-standards:
if: ${{ inputs.run-security != 'false' }}
uses: vergil-project/vergil-actions/.github/workflows/ci-security.yml@develop
with:
language: ruby
# For Go, also set: semgrep-language: golang
permissions:
contents: read
security-events: write
Semgrep language names
Semgrep uses p/<language> rulesets. Most languages match their
common name (ruby, python, java) but Go requires golang.
Use the semgrep-language input to override when needed.
Default matrix pattern¶
Use fromJSON() with a fallback to embed the full default matrix
directly in the workflow:
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(inputs.versions || '["3.2", "3.3", "3.4"]') }}
This avoids needing a separate job to compute the matrix.
Implementation guide¶
Step 1: Define ci.yml¶
Trigger on pull_request and (optionally) expose workflow_call
alongside it for specialized callers. Define inputs with string types
and sensible defaults.
Step 2: Factor security into shared workflow¶
Replace inline CodeQL, Trivy, Semgrep, and standards-compliance jobs
with a single call to ci-security.yml.
Step 3: Add dev scripts¶
Create scripts/dev/test.sh, scripts/dev/lint.sh, and
scripts/dev/audit.sh following the Docker-first pattern. See
Dev container images for image details.
Step 4: Update CI gates¶
Update the repository ruleset to match new check names. Key changes:
- Remove
ci: docs-only(no longer exists) - Replace
ci: standards-compliancewithsecurity-and-standards / ci: standards-compliance - Replace
security: *withsecurity-and-standards / security: *
Use the GitHub API to update rulesets:
Step 5: Update CLAUDE.md¶
Add the two-tier CI model and Docker-first testing sections to the
repository's CLAUDE.md.
CI gates¶
When security and standards jobs move into the shared reusable workflow,
their check names gain a security-and-standards / prefix:
Old names and their replacements:
ci: standards-compliance→security-and-standards / ci: standards-compliancesecurity: codeql→security-and-standards / security: codeqlsecurity: trivy→security-and-standards / security: trivysecurity: semgrep→security-and-standards / security: semgrep
Jobs that remain inline keep their names unchanged:
ci: dependency-auditrelease: gatestest: unit (<version>)test: integration (<version>)
Dev container images¶
Published to ghcr.io/vergil-project/dev-<language>:<version> from the
vergil-docker
repository.
Available images¶
dev-ruby (3.2, 3.3, 3.4)
: Base: ruby:<v>-slim. Includes build-essential,
git, curl, bundler.
dev-python (3.12, 3.13, 3.14)
: Base: python:<v>-slim. Includes git, curl, uv.
dev-java (17, 21)
: Base: eclipse-temurin:<v>-jdk. Includes git, curl.
dev-go (1.25, 1.26)
: Base: golang:<v>. Includes golangci-lint,
govulncheck, go-licenses, gocyclo.
Building locally¶
This builds all images. Individual images can be built with:
Publishing¶
Images are published automatically on push to develop or main in
the vergil-docker repository via its
.github/workflows/docker-publish.yml workflow.
Design principles¶
- Thin images — language runtime + package manager + git/curl
- Project-managed dependencies — tools come from lockfiles at
container startup (e.g.,
bundle install,uv sync,go install) - No host requirements — Docker is the only prerequisite for local development