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 Claude Code hook guard and vrg-commit
enforce 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-container-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-containers && docker/build.sh
Running vrg-container-run -- uv run vrg-validate before each commit runs
common checks and per-language validation. The Claude Code hook guard
ensures agents use vrg-commit (which runs validation) rather than raw
git commit.
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-containers
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-containers 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