Skip to content

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: local hooks call binaries that exist only in the image. scripts/ci/install-lint-tools.sh parses 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-amd64 is written by amd64 jobs (master pushes, release test job, the amd64 release leg); :buildcache-arm64 is 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/amd64 and linux/arm64 on native runners in a matrix and pushes by digest; a merge job 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-actions manager plus helpers:pinGitHubActionDigests keeps them current (grouped into one PR).
  • Weekly Renovate cadence lives in the workflow cron (Sunday 04:00 UTC); renovate.json has no schedule of its own. The custom regex manager convention is reused to pin renovate-version in 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-arm became 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.Z after 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_TOKEN and the RENOVATE_TOKEN PAT need rotation; the Docker Hub repo must be created private before the first push (a push to a nonexistent repo would create it public).