refactor(roles): migrate version/branch git-ref keys to ref + add integration guard

Unify the source-repo half of the entity naming convention introduced
by `0cc37d0fd`. Every entity that declares `repository:` MUST now
also declare `ref:` (git ref — tag, branch, or commit SHA). The
overloaded `version:` (when used as git ref) and the alternative
`branch:` key are gone.

Roles migrated:
* web-app-bigbluebutton: `version: main` → `ref: main`
  (consumed in tasks/01_core.yml as `docker_git_repository_version`).
* web-app-discourse: `version: main` → `ref: main`;
  `DISCOURSE_VERSION` renamed to `DISCOURSE_REF`.
* web-app-bridgy-fed: `branch: main` → `ref: main`;
  `BRIDGY_REPO_BRANCH` renamed to `BRIDGY_REF` across vars, the
  Dockerfile ARG, and the compose build-args.
* web-app-postmarks: identical migration to bridgy-fed.

New guard `tests/integration/roles/meta/services/test_repository_requires_ref.py`
walks every `meta/services.yml` and fails when an entity declares
`repository` without `ref` (or vice versa), so the next role that
forks the source-build pattern cannot regress to either half-
declaration without `make test` catching it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 4.7 committed 2026-05-15 20:12:07 +02:00
1 parent 2497e95e9a
commit 7b15c61dfc
14 files changed
+87 -15

No files matched your search

@@ -19,7 +19,7 @@ email:
shared: "{{ 'web-app-mailu' in group_names }}"
bigbluebutton:
repository: https://github.com/bigbluebutton/docker.git
version: main
ref: main
recording:
enabled: false
cleanup: true
@@ -6,7 +6,7 @@
docker_compose_flush_handlers: false
docker_git_repository_pull: true
docker_git_repository_address: "{{ lookup('config', application_id, 'services.' ~ entity_name ~ '.repository') }}"
docker_git_repository_version: "{{ lookup('config', application_id, 'services.' ~ entity_name ~ '.version') }}"
docker_git_repository_version: "{{ lookup('config', application_id, 'services.' ~ entity_name ~ '.ref') }}"
webserver_extra_configuration: >-
{{ lookup(
'ansible.builtin.template',
+2 -2
View File
@@ -3,7 +3,7 @@ ARG PY_BASE="python:3.12-bookworm"
FROM ${PY_BASE} AS build
ARG BRIDGY_REPO_URL
ARG BRIDGY_REPO_BRANCH
ARG BRIDGY_REF
# System deps: git, build tools, curl for healthchecks, and gunicorn
RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -11,7 +11,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
RUN git clone --depth=1 --branch "${BRIDGY_REPO_BRANCH}" "${BRIDGY_REPO_URL}" ./
RUN git clone --depth=1 --branch "${BRIDGY_REF}" "${BRIDGY_REPO_URL}" ./
# Python deps
RUN pip install --upgrade pip && \
+1 -1
View File
@@ -25,7 +25,7 @@ application:
version: 3.14-bookworm
name: web-app-bridgy-fed
repository: https://github.com/snarfed/bridgy-fed.git
branch: main
ref: main
min_storage: 5GB
cpus: "0.5"
mem_reservation: 256m
@@ -4,7 +4,7 @@
{{ lookup('template', 'roles/sys-svc-container/templates/build.yml.j2') | indent(4) }}
args:
BRIDGY_REPO_URL: "{{ BRIDGY_REPO_URL }}"
BRIDGY_REPO_BRANCH: "{{ BRIDGY_REPO_BRANCH }}"
BRIDGY_REF: "{{ BRIDGY_REF }}"
CONTAINER_PORT: "{{ container_port | string }}"
image: "{{ BRIDGY_IMAGE }}:{{ BRIDGY_VERSION }}"
container_name: "{{ BRIDGY_CONTAINER }}"
+1 -1
View File
@@ -17,7 +17,7 @@ BRIDGY_ADMIN_EMAIL: "{{ lookup('users', 'administrator').email }}"
# Source
BRIDGY_REPO_URL: "{{ lookup('config', application_id, 'services.application.repository') }}"
BRIDGY_REPO_BRANCH: "{{ lookup('config', application_id, 'services.application.branch') }}"
BRIDGY_REF: "{{ lookup('config', application_id, 'services.application.ref') }}"
# Runtime env defaults for Bridgy Fed (see upstream README)
BRIDGY_APPVIEW_HOST: "api.bsky.app"
+1 -1
View File
@@ -51,7 +51,7 @@ discourse:
shared: true
enabled: true
name: discourse
version: main
ref: main
min_storage: 15GB
repository: https://github.com/discourse/discourse_docker.git
plugins:
+1 -1
View File
@@ -15,7 +15,7 @@
git:
repo: "{{ DISCOURSE_REPOSITORY_URL }}"
dest: "{{ DISCOURSE_REPOSITORY_DIR }}"
version: "{{ DISCOURSE_VERSION }}"
version: "{{ DISCOURSE_REF }}"
update: true
depth: 1
register: discourse_repo_pull
+1 -1
View File
@@ -15,7 +15,7 @@ DISCOURSE_PLUGINS: "{{ lookup('config', application_id, 'services.discourse.plug
DISCOURSE_PG_NETWORK: "{{ ('svc-db-' ~ DISCOURSE_DATABASE_TYPE) | get_entity_name }}"
DISCOURSE_REDIS_HOST: "{{ application_id | get_entity_name }}-redis"
DISCOURSE_REPOSITORY_URL: "{{ lookup('config', application_id, 'services.' ~ entity_name ~ '.repository') }}"
DISCOURSE_VERSION: "{{ lookup('config', application_id, 'services.' ~ entity_name ~ '.version') }}"
DISCOURSE_REF: "{{ lookup('config', application_id, 'services.' ~ entity_name ~ '.ref') }}"
## Database
DISCOURSE_DATABASE: "{{ lookup('database', application_id ) }}"
+2 -2
View File
@@ -4,14 +4,14 @@ ARG NODE_BASE="node:20-bookworm"
FROM ${NODE_BASE} AS build
ARG POSTMARKS_REPO_URL
ARG POSTMARKS_REPO_BRANCH
ARG POSTMARKS_REF
RUN apt-get update && apt-get install -y --no-install-recommends \
git ca-certificates && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
RUN git clone --depth=1 --branch "${POSTMARKS_REPO_BRANCH}" "${POSTMARKS_REPO_URL}" ./
RUN git clone --depth=1 --branch "${POSTMARKS_REF}" "${POSTMARKS_REPO_URL}" ./
RUN npm install --no-fund --no-audit --omit=dev
# Final stage. Carry over the application directory and the npm
+1 -1
View File
@@ -34,7 +34,7 @@ application:
version: 26-bookworm
name: web-app-postmarks
repository: https://github.com/ckolderup/postmarks.git
branch: main
ref: main
min_storage: 1GB
cpus: "0.5"
mem_reservation: 256m
@@ -4,7 +4,7 @@
{{ lookup('template', 'roles/sys-svc-container/templates/build.yml.j2') | indent(4) }}
args:
POSTMARKS_REPO_URL: "{{ POSTMARKS_REPO_URL }}"
POSTMARKS_REPO_BRANCH: "{{ POSTMARKS_REPO_BRANCH }}"
POSTMARKS_REF: "{{ POSTMARKS_REF }}"
CONTAINER_PORT: "{{ container_port | string }}"
image: "{{ POSTMARKS_IMAGE }}:{{ POSTMARKS_VERSION }}"
container_name: "{{ POSTMARKS_CONTAINER }}"
+1 -1
View File
@@ -15,4 +15,4 @@ POSTMARKS_VERSION: "{{ lookup('config', application_id, 'services.application.ve
# Source
POSTMARKS_REPO_URL: "{{ lookup('config', application_id, 'services.application.repository') }}"
POSTMARKS_REPO_BRANCH: "{{ lookup('config', application_id, 'services.application.branch') }}"
POSTMARKS_REF: "{{ lookup('config', application_id, 'services.application.ref') }}"
@@ -0,0 +1,72 @@
"""Integration guard: an entity in ``meta/services.yml`` that declares
``repository`` MUST also declare ``ref`` (and vice versa).
Rationale
---------
The unified entity-naming convention pairs the two dimensions:
- ``image`` + ``version`` → OCI image used as ``FROM image:version``.
- ``repository`` + ``ref`` → git source clone (``git clone --branch
<ref> <repository>``).
Half-declarations are silent breakage waiting to happen: a Dockerfile
that reads ``ARG ... REPOSITORY/REF`` will render ``git clone --branch
"" "https://…"`` (empty branch flag) or attempt to clone an empty URL,
both of which surface only at build time and only on the variant the
role is built into. Catch it at lint time instead.
This guard is symmetric: declaring ``ref`` without ``repository``
fails just as loudly — the standalone ref has no source URL to
clone from.
"""
from __future__ import annotations
import unittest
from utils.cache.yaml import load_yaml_any
from utils.roles.mapping import ROLE_FILE_META_SERVICES
from . import PROJECT_ROOT
ROLES_DIR = PROJECT_ROOT / "roles"
class TestRepositoryRequiresRef(unittest.TestCase):
def test_repository_and_ref_are_both_set_or_both_absent(self):
offenders: list[str] = []
for role_dir in sorted(p for p in ROLES_DIR.iterdir() if p.is_dir()):
services_path = role_dir / ROLE_FILE_META_SERVICES
if not services_path.is_file():
continue
data = load_yaml_any(str(services_path), default_if_missing=None)
if not isinstance(data, dict):
continue
for entity_key, entry in data.items():
if not isinstance(entry, dict):
continue
has_repo = "repository" in entry and entry["repository"] not in (
None,
"",
)
has_ref = "ref" in entry and entry["ref"] not in (None, "")
if has_repo and not has_ref:
offenders.append(
f"{role_dir.name}: entity '{entity_key}' declares "
f"`repository` but is missing `ref`"
)
elif has_ref and not has_repo:
offenders.append(
f"{role_dir.name}: entity '{entity_key}' declares "
f"`ref` but is missing `repository`"
)
if offenders:
self.fail(
f"{len(offenders)} entity/entities violate the "
f"`repository`↔`ref` pairing:\n"
+ "\n".join(f"- {o}" for o in offenders)
)