0011. CI/CD via GitHub Actions, Docker Hub Registry, Self-Hosted Daily Renovate¶
Status¶
Accepted. Amended by ADR 0012: release arm64 builds moved from QEMU to
native ubuntu-24.04-arm runners, the multi-arch :buildcache tag is
retired, and images are pushed by digest.
Context¶
The repo is a private GitHub repository with no CI: linting and the BATS suite
run only when someone remembers to run them, images are built and distributed
by hand, and Renovate exists purely as a local dry-run (--platform=local), so
no update PR has ever been opened. ADR 0010 introduces tag-driven releases,
which need automation to mean anything. Constraints: GitHub Free private repos
get 2000 Actions minutes/month; the image must serve macOS hosts (Apple
Silicon -> linux/arm64) as well as linux/amd64; the user chose Docker Hub
(docker.io/felipecoelho90/dev-container, private) as the canonical registry.
Decision¶
GitHub Actions (native to the repo host) with three workflows:
| Workflow | Trigger | What it does |
|---|---|---|
ci.yml |
PRs + pushes to master | lint (pre-commit at image-pinned versions, gitleaks history scan, Conventional Commits check) and build-test (make build + BATS + make test-init), gated by a path filter so doc-only changes skip the heavy build |
release.yml |
tags v* |
validate tag + changelog gate -> build + test amd64 -> a per-arch native build matrix (linux/amd64 on ubuntu-latest, linux/arm64 on ubuntu-24.04-arm) pushing each leg by digest with OCI labels -> a merge job that stitches the digests into one semver-tagged manifest list -> GitHub Release from the changelog section |
renovate.yml |
weekly cron Sunday 04:00 UTC + manual dispatch | self-hosted renovatebot/github-action opening update PRs with RENOVATE_TOKEN (classic PAT, repo + workflow scopes) |
Key mechanics:
- Lint parity (extends ADR 0008 to CI): the
repo: localhooks call binaries that exist only in the image.scripts/ci/install-lint-tools.shparses the pinned versions from the Dockerfile's# renovate:-annotated ARGs and installs the exact same binaries on the runner -- when Renovate bumps a pin, CI follows with no second place to update. - Per-arch registry cache tags in the same Docker Hub repo:
:buildcache-amd64is written by amd64 jobs (master pushes, release test job, the amd64 release leg);:buildcache-arm64is written by the arm64 release leg. Per-arch jobs writing per-arch cache tags cannot clobber each other. PR builds read the amd64 cache but never write. - arm64 builds natively, releases only. PR CI builds/tests amd64. Each
release builds
linux/amd64andlinux/arm64on native runners in a matrix and pushes by digest; amergejob publishes the tagged manifest list (amended from the original QEMU approach — see ADR 0012). - Actions pinned to commit SHAs with version comments; Renovate's
github-actionsmanager plushelpers:pinGitHubActionDigestskeeps them current (grouped into one PR). - Weekly Renovate cadence lives in the workflow cron (Sunday 04:00 UTC);
renovate.jsonhas noscheduleof its own. The custom regex manager convention is reused to pinrenovate-versionin the workflow.
Alternatives considered:
- ghcr.io -- native auth (
GITHUB_TOKEN), free private storage, one less credential. Rejected: Docker Hub explicitly chosen as the canonical registry. - Mend-hosted Renovate app -- zero maintenance, no PAT. Rejected: opaque scheduling/runtime; an explicit daily pipeline with dispatchable dry-run was wanted.
- Hosted arm64 runners -- native speed, originally rejected as paid for
private repos. Reversed by ADR 0012:
ubuntu-24.04-armbecame a standard hosted runner for private repos, and QEMU could not build the image at all. - release-please / semantic-release -- automated version calculation and changelog. Rejected in favor of the hand-curated Keep-a-Changelog flow (ADR 0010).
Consequences¶
Easier:
- Every PR is linted and tested; Renovate PRs arrive daily, pre-validated by the same pipeline, and patch-level linter updates can automerge once their checks pass.
- Releasing is
git tag vX.Y.Z && git push origin vX.Y.Zafter the changelog PR; consumers pull from Docker Hub instead of building locally.
Harder / accepted costs:
- Minutes budget: estimated ~1200-1500 of the 2000 free monthly minutes in a busy month (daily Renovate runs, PR builds, 1-2 releases). Mitigated by the path filter, cancel-in-progress concurrency, registry layer cache, and grouped Renovate PRs. If exceeded: GitHub Pro (3000 min) or a self-hosted runner.
- Secret upkeep:
DOCKERHUB_USERNAME/DOCKERHUB_TOKENand theRENOVATE_TOKENPAT need rotation; the Docker Hub repo must be created private before the first push (a push to a nonexistent repo would create it public).