ADR-0012: Share DevSecOps Tooling Across Application Repositories Through A Copier Template
Status
Section titled “Status”Accepted
Context
Section titled “Context”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.yamlwith role tagged age recipients (per ADR-0008)..plumber.yamlwith supply chain controls (per ADR-0009)..pre-commit-config.yamlwith gitleaks, yamllint, hadolint, php-cs-fixer and language specific hooks.- A
Makefileor mise tasks skeleton forinit,lint,test. - A
mise.tomlbaseline pinning the tooling versions. scripts/age-bootstrap.shandscripts/secrets-rekey.sh.renovate.json5extending a workspace preset.- A
.gitignorewith the shared exclusions. - A
.editorconfigand a.gitattributesfor 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.
Decision
Section titled “Decision”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:
copier copy gh:yourbox/homelab-app-template my-new-appCopier 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:
copier updateCopier 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.
Consequences
Section titled “Consequences”- 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 onhomelab-app-template, then propagates as acopier updateMR 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-templatesallowlist (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.yamlrecipients 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-bootstrapandmise run secrets-rekey. The scripts and.sops.yamlskeleton 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.