CI/CD Pipelines
Aveloxis uses GitHub Actions for continuous integration and deployment. All workflows are in .github/workflows/.
Workflows
Tests (test.yml) — unit tier
Trigger: Every push to any branch, every PR to main.
Runs go test -race ./... with race detection enabled. Uploads coverage artifact on main branch pushes. No database service; tests gated on AVELOXIS_TEST_DB self-skip.
Integration (integration.yml) — integration tier (v0.21.1)
Trigger: Every push to any branch, every PR to main. Runs in parallel with the unit-tier test.yml job.
Provisions a postgres:16 service container, sets AVELOXIS_TEST_DB=postgres://aveloxis_test:aveloxis_test@localhost:5432/aveloxis_test?sslmode=disable, runs the migration-integration tests, then re-runs the entire test suite with the env var still set so any AVELOXIS_TEST_DB-gated tests added later (e.g. additional *_integration_test.go files) get exercised automatically.
Why split from test.yml: separation of green-badge semantics. A green Tests status means unit-tier passed; a green Integration status means migrations + cross-table backfills actually ran against a real Postgres. If only one is green, the failure mode is obvious from the workflow badge.
Why this matters operationally: the v0.21.0 ship contained a backfill SQL referencing aveloxis_scan.scancode_scans.created_at — a column that doesn’t exist (the table uses data_collection_date). The source-contract test pinned MAX(created_at) and silently passed because both sides of the contract agreed on the wrong answer. Production migrate failed with SQLSTATE 42703. TestRunMigrationsOnFreshDB (added in v0.21.1) catches this class of bug end-to-end by actually running RunMigrations against a Postgres service container and observing whether each statement parses + executes cleanly.
Local dev — running the integration tier
The simplest path uses a throwaway Postgres container:
# Start an empty Postgres container.
docker run --rm -d --name aveloxis-test-pg -p 5433:5432 \
-e POSTGRES_PASSWORD=test -e POSTGRES_DB=aveloxis_test postgres:16
# Run the integration tests against it.
AVELOXIS_TEST_DB="postgres://postgres:test@localhost:5433/aveloxis_test?sslmode=disable" \
go test ./internal/db/ -run "TestRunMigrations" -v
# Run any other AVELOXIS_TEST_DB-gated tests in the codebase.
AVELOXIS_TEST_DB="postgres://postgres:test@localhost:5433/aveloxis_test?sslmode=disable" \
go test ./... -v
# Tear down when done.
docker stop aveloxis-test-pg
Or, if you have an existing Postgres you want to use:
# Create a scratch DB on your existing Postgres instance.
psql -h localhost -U aveloxis -d postgres -c "CREATE DATABASE aveloxis_test;"
# Run with the test DB.
AVELOXIS_TEST_DB="host=localhost port=5432 user=aveloxis password=... dbname=aveloxis_test sslmode=prefer" \
go test ./... -v
# Drop when done.
psql -h localhost -U aveloxis -d postgres -c "DROP DATABASE aveloxis_test;"
Never run the integration suite against the production aveloxis database. TestRunMigrationsOnFreshDB is destructive (it owns the schema), and RealignDueDates-style integration tests are unscoped — they update every matching row in the queue and would silently realign the entire fleet.
Conventions for adding integration tests:
Name the test
TestX_YIntegrationor put it in a file ending in_integration_test.go.Always gate on
os.Getenv("AVELOXIS_TEST_DB")witht.Skipwhen empty, sogo test ./...without the env var stays fast.For non-migration tests, seed rows with nanosecond-suffixed synthetic slugs (see
seedRealignRepoininternal/db/queue_realign_integration_test.go) so parallel or repeated runs do not collide onON CONFLICTconstraints.Prefer strict-equality assertions (
approxEqual(..., time.Millisecond)) where the SQL does not involveNOW(), since source-text tests cannot catch interval-arithmetic drift and this is where runtime regressions hide.
Lint (lint.yml)
Trigger: Every PR to main.
Runs golangci-lint with --only-new-issues so existing code doesn’t block PRs. 5-minute timeout for large codebases.
CodeQL (codeql.yml)
Trigger: Every PR to main, plus weekly Monday scan.
Runs GitHub’s CodeQL security analysis with security-extended query suite for Go. Results appear in the Security tab on GitHub.
Container Build (container-build.yml)
Trigger: Every PR to main.
Tests building the Docker image on:
Ubuntu with Docker
Ubuntu with Podman
macOS with Docker (via colima)
Verifies the binary runs inside the container (aveloxis version). Does NOT push images.
Docker Publish (docker-publish.yml)
Trigger: Every push to main.
Builds and publishes Docker images to GitHub Container Registry (ghcr.io/aveloxis/aveloxis). Tags:
latest— always the most recent main buildGit SHA — for pinning to a specific commit
Date stamp (
YYYY.MM.DD) — for pinning to a specific day
Status Badges
All workflows have status badges at the top of the README:
Dockerfile
The multi-stage Dockerfile in the repo root:
Builder stage —
golang:1.25-alpine, downloads dependencies, builds a static binaryRuntime stage —
alpine:3.20, copies the binary, includes git/curl/ca-certificates for facade and libyear phases
Exposed ports: 5555 (monitor), 8082 (web), 8383 (API).
Default command: aveloxis serve --workers 4 --monitor :5555
Running in Docker
# Pull from GHCR
docker pull ghcr.io/aveloxis/aveloxis:latest
# Start all three processes
docker run -d --name aveloxis-serve \
-v ./aveloxis.json:/app/aveloxis.json \
-v /data/repos:/data \
-p 5555:5555 \
ghcr.io/aveloxis/aveloxis:latest serve --workers 40
docker run -d --name aveloxis-web \
-v ./aveloxis.json:/app/aveloxis.json \
-p 8082:8082 \
ghcr.io/aveloxis/aveloxis:latest web
docker run -d --name aveloxis-api \
-v ./aveloxis.json:/app/aveloxis.json \
-p 8383:8383 \
ghcr.io/aveloxis/aveloxis:latest api
All containers share the same aveloxis.json and connect to the same PostgreSQL database.
Schema change verification
PRs that modify internal/db/schema.sql or internal/db/migrate.go
go through an extra release-gate workflow: the contributor (and the
maintainer reviewing the PR) runs aveloxis data-test against the
last released tag to verify no row-loss regressions. Shipped in
v0.22.8.
When the gate applies
A change is schema-touching if it:
Adds, removes, or renames any column in
schema.sql.Adds, removes, or modifies any constraint (FK, CHECK, UNIQUE, NOT NULL) in
schema.sql.Adds or modifies any DDL step in
migrate.gothat runs against existing data.Changes any index declaration that affects a constraint’s lookup path.
A change is NOT schema-touching (no gate required) if it only:
Changes Go code (API layer, collector, web, scheduler).
Adds tests, fixtures, or documentation.
Modifies build / deployment infrastructure.
Adjusts logging or metrics.
When in doubt, run the harness anyway. It’s idempotent and ~1 hour of wall-clock; cheap insurance.
Running the gate
# From the contributor's working branch, with the schema change
# checked out locally:
aveloxis data-test \
--released-tag 0.22.6 \
--repo https://github.com/augurlabs/augur
The harness:
Builds binaries from the released tag (via
git worktree) and the current working tree.Provisions two scratch PostgreSQL databases.
Collects
augurlabs/augurinto each.Diffs row counts table-by-table.
Writes a markdown report to
<work-dir>/report.md.Exits 0 if all rows PASS or FLAG; exits 1 on any FAIL.
The canonical test repo is augurlabs/augur — moderate size (a few
thousand issues, a few thousand PRs, lots of commits) that exercises
every collection path without taking hours.
Interpreting results
PASS — equal row counts. Ship.
FLAG — new schema captures more rows (likely new coverage). Review individually but generally fine.
FAIL — released schema captured rows the new schema rejects. Do not ship. Investigate the root cause (usually a new constraint rejecting INSERTs that previously succeeded). Fix the regression and re-run.
Full interpretation guidance: Schema-change verification.
CI integration (optional)
For projects that want automated gating, the harness can drive a CI job:
- name: Schema-change verification
if: contains(github.event.pull_request.labels.*.name, 'schema-change')
run: |
aveloxis data-test \
--released-tag $LAST_RELEASED \
--repo https://github.com/augurlabs/augur \
--work-dir /tmp/aveloxis-data-test-${{ github.run_id }}
# Exit code 1 on FAIL → fails the workflow.
- name: Upload report on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: data-test-report
path: /tmp/aveloxis-data-test-${{ github.run_id }}/report.md
This requires the CI runner to have:
Access to a PostgreSQL server with CREATEDB privilege.
API keys pre-seeded into a primary
aveloxis_ops.api_keystable on that server.Enough wall-clock budget for the ~1-hour cycle.
For projects without those, the harness still works as an operator-invoked local check before pushing the PR. The CI job is a convenience, not a requirement.