Skip to content

ADR-0012: Share DevSecOps Tooling Across Application Repositories Through A Copier Template

Accepted

The homelab workspace already hosts multiple application repositories (YourBOX, weather-app, the future internal tools) and the platform repository (homelab-platform). Each application repository carries the same DevSecOps plumbing:

  • .sops.yaml with role tagged age recipients (per ADR-0008).
  • .plumber.yaml with supply chain controls (per ADR-0009).
  • .pre-commit-config.yaml with gitleaks, yamllint, hadolint, php-cs-fixer and language specific hooks.
  • A Makefile or mise tasks skeleton for init, lint, test.
  • A mise.toml baseline pinning the tooling versions.
  • scripts/age-bootstrap.sh and scripts/secrets-rekey.sh.
  • renovate.json5 extending a workspace preset.
  • A .gitignore with the shared exclusions.
  • A .editorconfig and a .gitattributes for line endings.

Without a sharing mechanism the files diverge silently across repositories. A security fix to age-bootstrap.sh written once in YourBOX never reaches weather-app. A new Plumber control adopted in gitlab-ci-templates requires manual replication. The drift is invisible until an audit catches it.

Three sharing mechanisms were considered:

  • Submodule pointing at a shared policy/ repository: drags submodules into every clone, contributors trip over them, no parameterization.
  • Renovate as the only sharing mechanism with regexManagers: handles version bumps inside files but cannot introduce a new file, restructure an existing one, or carry parameterized content.
  • Copier templates plus Renovate: Copier owns structural sharing (file shapes, parameterized values), Renovate owns the version bumps that flow through the templated files after generation.

The third option matches how mature poly repo organizations operate (Spotify, Datadog, Stripe internal templates). Copier supersedes cookiecutter for this use case because it natively supports the copier update flow that propagates template changes to downstream repositories through interactive merge.

Switching away from SOPS+age to avoid the sharing problem was rejected: the sharing problem applies to every cross-cutting concern (Plumber, pre-commit, Makefile), not only to SOPS. Switching the encryption choice for the wrong reason creates new problems without solving the root one.

The homelab workspace gains a new repository, homelab-app-template, that holds the canonical version of the cross-cutting tooling files listed above. The repository is consumed through Copier.

A new application repository bootstraps with:

Terminal window
copier copy gh:yourbox/homelab-app-template my-new-app

Copier prompts for the parameters declared in the template’s copier.yml: the app slug, the container registry, the default port, the Kubernetes namespace, the criticality, whether the app uses a database, whether the app uses object storage, and a few other answers. Copier renders the template files with those values substituted and writes them into the new repository. A .copier-answers.yml file at the new repository’s root records the answers and the template revision the repository was generated from.

When the template repository ships an update (a new Plumber control, a hardened age-bootstrap.sh, an extended .gitignore), each downstream repository runs:

Terminal window
copier update

Copier replays the answers stored in .copier-answers.yml, computes the diff between the previously generated state and the new template state, and presents a three way merge for any file where the downstream repository diverged from the template. The flow is the standard MR review path: a contributor runs the update, commits, opens an MR, the team reviews the diff.

Renovate handles the dimension Copier does not: pinned version bumps inside the templated files. A Renovate preset committed to gitlab-ci-templates covers Docker image tags + digests (ADR-0009), pre-commit hook revisions, mise tool versions, GitHub release versions referenced in the scripts. Every downstream repository extends the preset through its renovate.json5. Bumps flow as MRs on the downstream repositories, not on the template.

The template parameterizes the per-repository values that genuinely differ:

  • App identity: slug, display name, criticality, owner.
  • Networking: hostname, port, ingress controller, TLS policy.
  • Runtime: language and framework hint (php-symfony, node-bun, python-uv, go).
  • Persistence: needs database (yes or no), needs object storage (yes or no), needs cache (yes or no), needs messaging bus (yes or no).
  • Registry: container registry URL and namespace.
  • Secret backend role: which age role markers ship in .sops.yaml (admins always, readers optional).

The template does NOT parameterize the security controls themselves (Plumber control list, hadolint behavior, pre-commit hooks list). Those are workspace policies and stay identical across applications.

YourBOX, weather-app and the future repositories migrate to the template through copier copy --override in their existing tree, followed by a manual reconciliation of the differences. The migration is a deliberate one-off; the runbook for it lives under the matching workspace plan.

  • A new application repository is created in one command and ships with every DevSecOps baseline already in place. The bootstrap time drops from a half day of copy-paste to under five minutes.
  • A workspace level fix (security patch on age-bootstrap.sh, new Plumber control, hardened .editorconfig) lands in one MR on homelab-app-template, then propagates as a copier update MR on each downstream repository. Reviewers see the same diff everywhere.
  • Renovate keeps the pinned versions current without human intervention. Each MR runs the downstream CI gates, so the bump cannot land on a broken state.
  • Operating Copier and Renovate adds two tools to the workspace tooling surface. Both have shallow learning curves, are widely documented and are already in the gitlab-ci-templates allowlist (Copier is a Python package, Renovate runs as a GitLab CI component).
  • The template repository becomes a critical workspace dependency. It inherits the protected branch policy from ADR-0011, requires admin review on every MR, and bumps cosign signed.
  • Per-repository .sops.yaml recipients still live in the downstream repositories. The template ships the structural skeleton (role markers, path regex, comment headers), not the recipient list.
  • The split between Copier (structural) and Renovate (versions) is enforced through documentation. A version bump committed manually inside the template (instead of being managed by Renovate on the downstream) is rejected at review.
  • ADR-0008 secret management workflow stays unchanged in its end user shape: contributors still run mise run age-bootstrap and mise run secrets-rekey. The scripts and .sops.yaml skeleton come from the template instead of being repo-specific files maintained in place.
  • ADR-0009 digest pinning policy stays unchanged. The Renovate configuration that enforces it now ships from one place.
  • The future application template hand off is straightforward: a new contributor reads homelab-app-template/README.md, copies the template, fills in the answers, and starts coding the application logic immediately.