mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
refactor(roles): declare every git URL in meta/services.yml + per-role REPOSITORY/REF vars
Co-locates every git-clone URL with its owning entity in the role's
`meta/services.yml` so the unified `image`/`version` ↔ `repository`/`ref`
naming convention covers secondary repos (plugins, build helpers,
bootstrap sources) just as cleanly as the primary entity source.
Migrations:
- sys-aur, pkgmgr (non-invokable bootstrap roles): `meta/services.yml`
gains `repository:`/`ref:` on the role's own entity; `vars/main.yml`
renders them via `lookup('config', role_name, …)` because these
roles never set `application_id`.
- web-app-{akaunting,bigbluebutton,bluesky,discourse,roulette-wheel,
taiga}: declare the secondary git URL on the most fitting entity
(`akaunting` itself for the docker-helper repo; `view` for bluesky's
social-app web client; `plugins.discourse-ldap-auth` for that
Discourse plugin; `oidc` for taiga's OIDC plugin). Each role gains
named `<ROLE>_REPOSITORY` / `<ROLE>_REF` constants in `vars/main.yml`
so tasks and templates stay readable instead of inlining a
`lookup('config', application_id, …)` string at every call site.
- svc-db-postgres `templates/Dockerfile.j2`: keep the `pgvector`
extension clone but mark the line with `{# nocheck: repository-declared #}`
since pgvector is an extension source, not the role's primary repo.
New integration test
`tests/integration/roles/meta/services/test_repository_declared_in_services.py`
walks every role's text files and fails if a literal `https://…/<…>.git`
URL is not declared as a `repository:` value somewhere in that role's
`meta/services.yml` (recursive — top-level entity, sub-entity, plugin
map). Legitimate exceptions opt out with `# nocheck: repository-declared`
on the same line or the preceding line; Jinja templates may use the
`{# … #}` form so a `\`-continued Dockerfile shell command stays valid.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
7b15c61dfc
commit
fd22d34559
24 files changed
+213
-19
No files matched your search
@@ -1,3 +1,5 @@
|
||||
---
|
||||
pkgmgr:
|
||||
lifecycle: beta
|
||||
repository: https://github.com/kevinveenbirkenbach/package-manager.git
|
||||
ref: stable
|
||||
@@ -30,7 +30,7 @@
|
||||
sys_util_git_pull_dest: "{{ PKGMGR_INSTALL_PATH }}"
|
||||
sys_util_git_pull_version: "main"
|
||||
sys_util_git_pull_depth: 1
|
||||
sys_util_git_pull_pin_tag: "stable"
|
||||
sys_util_git_pull_pin_tag: "{{ PKGMGR_REF }}"
|
||||
sys_util_git_pull_remove_tags:
|
||||
- latest
|
||||
- stable
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
# Variables for Kevin's Package Manager installation
|
||||
|
||||
# The Git repository URL for Kevin's Package Manager
|
||||
PKGMGR_REPO_URL: "https://github.com/kevinveenbirkenbach/package-manager.git"
|
||||
PKGMGR_REPO_URL: "{{ lookup('config', role_name, 'services.' ~ role_name ~ '.repository') }}"
|
||||
PKGMGR_REF: "{{ lookup('config', role_name, 'services.' ~ role_name ~ '.ref') }}"
|
||||
|
||||
# The directory where executable aliases will be installed (ensure it's in your PATH)
|
||||
PKGMGR_BINARIES_DIR: "/usr/local/bin"
|
||||
|
||||
@@ -6,6 +6,7 @@ RUN apt-get update \
|
||||
build-essential \
|
||||
git \
|
||||
postgresql-server-dev-{{ POSTGRES_VERSION_MAJOR | default('all', true) }} \
|
||||
{# nocheck: repository-declared — pgvector is an extension source, not the role's primary repo #}
|
||||
&& git clone https://github.com/pgvector/pgvector.git /tmp/pgvector \
|
||||
&& cd /tmp/pgvector \
|
||||
&& make \
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
---
|
||||
aur:
|
||||
lifecycle: beta
|
||||
repository: https://aur.archlinux.org/yay.git
|
||||
ref: master
|
||||
@@ -36,7 +36,8 @@
|
||||
become: true
|
||||
become_user: "{{ AUR_BUILDER_USER }}"
|
||||
git:
|
||||
repo: https://aur.archlinux.org/yay.git # nocheck: url — git clone URL, not a browser link
|
||||
repo: "{{ AUR_YAY_REPOSITORY }}"
|
||||
version: "{{ AUR_YAY_REF }}"
|
||||
dest: "/home/{{ AUR_BUILDER_USER }}/yay"
|
||||
clone: true
|
||||
update: true
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
# sys-aur is non-invokable, so it does not set `application_id` —
|
||||
# the entity config is keyed by `role_name`.
|
||||
AUR_YAY_REPOSITORY: "{{ lookup('config', role_name, 'services.aur.repository') }}"
|
||||
AUR_YAY_REF: "{{ lookup('config', role_name, 'services.aur.ref') }}"
|
||||
@@ -31,6 +31,8 @@ akaunting:
|
||||
no_stop_required: true
|
||||
image: docker.io/akaunting/akaunting
|
||||
version: latest
|
||||
repository: https://github.com/akaunting/docker.git
|
||||
ref: master
|
||||
name: akaunting
|
||||
min_storage: 8GB
|
||||
company:
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
include_role:
|
||||
name: sys-stk-full
|
||||
vars:
|
||||
docker_git_repository_address: "https://github.com/akaunting/docker.git"
|
||||
docker_git_repository_address: "{{ AKAUNTING_REPOSITORY }}"
|
||||
docker_git_repository_pull: true
|
||||
docker_git_repository_version: "master"
|
||||
docker_git_repository_version: "{{ AKAUNTING_REF }}"
|
||||
docker_compose_flush_handlers: true
|
||||
|
||||
- name: "Akaunting | Create first-run marker to disable future setup"
|
||||
|
||||
@@ -9,6 +9,8 @@ database_password: "{{ lookup('config', application_id, 'credentials.database_pa
|
||||
AKAUNTING_URL: "{{ lookup('tls', application_id, 'url.base') }}"
|
||||
AKAUNTING_VERSION: "{{ lookup('config', application_id, 'services.akaunting.version') }}"
|
||||
AKAUNTING_IMAGE: "{{ lookup('config', application_id, 'services.akaunting.image') }}"
|
||||
AKAUNTING_REPOSITORY: "{{ lookup('config', application_id, 'services.akaunting.repository') }}"
|
||||
AKAUNTING_REF: "{{ lookup('config', application_id, 'services.akaunting.ref') }}"
|
||||
AKAUNTING_CONTAINER: "{{ lookup('config', application_id, 'services.akaunting.name') }}"
|
||||
AKAUNTING_VOLUME: "{{ lookup('config', application_id, 'volumes.data') }}"
|
||||
AKAUNTING_COMPANY_NAME: "{{ lookup('config', application_id, 'services.akaunting.company.name') }}"
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
vars:
|
||||
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 ~ '.ref') }}"
|
||||
docker_git_repository_address: "{{ BBB_REPOSITORY }}"
|
||||
docker_git_repository_version: "{{ BBB_REF }}"
|
||||
webserver_extra_configuration: >-
|
||||
{{ lookup(
|
||||
'ansible.builtin.template',
|
||||
|
||||
@@ -7,6 +7,10 @@ entity_name: "{{ application_id | get_entity_name }}"
|
||||
domain: "{{ lookup('domain',application_id) }}"
|
||||
http_port: "{{ lookup('config', application_id, 'services.bigbluebutton.ports.local.http') }}"
|
||||
|
||||
# Source
|
||||
BBB_REPOSITORY: "{{ lookup('config', application_id, 'services.' ~ entity_name ~ '.repository') }}"
|
||||
BBB_REF: "{{ lookup('config', application_id, 'services.' ~ entity_name ~ '.ref') }}"
|
||||
|
||||
# BigBlueButton
|
||||
_BBB_COTURN_ROLE: "web-svc-coturn"
|
||||
|
||||
|
||||
@@ -54,6 +54,8 @@ web:
|
||||
# nocheck: playwright-service-flag — internal sub-component (multi-entity role)
|
||||
view:
|
||||
enabled: false
|
||||
repository: https://github.com/bluesky-social/social-app.git
|
||||
ref: "1.121.0"
|
||||
ports:
|
||||
local:
|
||||
http: 8051
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
---
|
||||
- name: clone social app repository
|
||||
git:
|
||||
# Pin to a tagged stable release; tracking `main` would silently
|
||||
# break the broker's `BSKY_STORAGE` localStorage handoff every
|
||||
# time upstream changes the persistent-state schema. Bump this
|
||||
# pin deliberately when bumping social-app (and re-verify the
|
||||
# handoff in Playwright).
|
||||
repo: "https://github.com/bluesky-social/social-app.git"
|
||||
repo: "{{ BLUESKY_SOCIAL_APP_REPOSITORY }}"
|
||||
dest: "{{ BLUESKY_SOCIAL_APP_DIR }}"
|
||||
version: "1.121.0"
|
||||
version: "{{ BLUESKY_SOCIAL_APP_REF }}"
|
||||
force: true
|
||||
notify:
|
||||
- compose up
|
||||
|
||||
@@ -6,6 +6,8 @@ application_id: "web-app-bluesky"
|
||||
|
||||
## Social App
|
||||
BLUESKY_SOCIAL_APP_DIR: "{{ [ lookup('container', application_id, 'directories.services'), 'social-app' ] | path_join }}"
|
||||
BLUESKY_SOCIAL_APP_REPOSITORY: "{{ lookup('config', application_id, 'services.view.repository') }}"
|
||||
BLUESKY_SOCIAL_APP_REF: "{{ lookup('config', application_id, 'services.view.ref') }}"
|
||||
BLUESKY_GEOLOCATION_PATH: "{{ [ BLUESKY_SOCIAL_APP_DIR,'src/geolocation/index.tsx' ] | path_join }}"
|
||||
|
||||
# This should be removed when the following issue is closed:
|
||||
|
||||
@@ -61,6 +61,10 @@ discourse:
|
||||
enabled: true
|
||||
discourse-akismet:
|
||||
enabled: true
|
||||
discourse-ldap-auth:
|
||||
enabled: "{{ lookup('config', 'web-app-discourse', 'services.ldap.enabled') | bool }}"
|
||||
repository: https://github.com/jonmbake/discourse-ldap-auth.git
|
||||
ref: master
|
||||
ports:
|
||||
local:
|
||||
http: 8020
|
||||
|
||||
@@ -138,7 +138,7 @@ hooks:
|
||||
{% endfor %}
|
||||
|
||||
{% if DISCOURSE_LDAP_ENABLED | bool %}
|
||||
- git clone --depth=1 https://github.com/jonmbake/discourse-ldap-auth.git
|
||||
- git clone --depth=1 --branch {{ DISCOURSE_LDAP_AUTH_PLUGIN_REF }} {{ DISCOURSE_LDAP_AUTH_PLUGIN_REPOSITORY }}
|
||||
{% endif %}
|
||||
|
||||
## Any custom commands to run after building
|
||||
|
||||
@@ -16,6 +16,8 @@ DISCOURSE_PG_NETWORK: "{{ ('svc-db-' ~ DISCOURSE_DATABASE_TYPE) | get_entity_nam
|
||||
DISCOURSE_REDIS_HOST: "{{ application_id | get_entity_name }}-redis"
|
||||
DISCOURSE_REPOSITORY_URL: "{{ lookup('config', application_id, 'services.' ~ entity_name ~ '.repository') }}"
|
||||
DISCOURSE_REF: "{{ lookup('config', application_id, 'services.' ~ entity_name ~ '.ref') }}"
|
||||
DISCOURSE_LDAP_AUTH_PLUGIN_REPOSITORY: "{{ lookup('config', application_id, 'services.' ~ entity_name ~ '.plugins.discourse-ldap-auth.repository') }}"
|
||||
DISCOURSE_LDAP_AUTH_PLUGIN_REF: "{{ lookup('config', application_id, 'services.' ~ entity_name ~ '.plugins.discourse-ldap-auth.ref') }}"
|
||||
|
||||
## Database
|
||||
DISCOURSE_DATABASE: "{{ lookup('database', application_id ) }}"
|
||||
|
||||
@@ -27,6 +27,8 @@ css:
|
||||
roulette-wheel:
|
||||
image: node
|
||||
version: latest
|
||||
repository: https://github.com/kevinveenbirkenbach/roulette-wheel.git
|
||||
ref: master
|
||||
min_storage: 1GB
|
||||
cpus: 0.5
|
||||
mem_reservation: 126m
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
name: sys-stk-full
|
||||
vars:
|
||||
docker_compose_flush_handlers: true
|
||||
docker_git_repository_address: "https://github.com/kevinveenbirkenbach/roulette-wheel.git"
|
||||
docker_git_repository_address: "{{ ROULETTE_REPOSITORY }}"
|
||||
docker_git_repository_pull: true
|
||||
docker_git_repository_version: "master"
|
||||
docker_git_repository_version: "{{ ROULETTE_REF }}"
|
||||
@@ -5,4 +5,6 @@ entity_name: "{{ application_id | get_entity_name }}"
|
||||
ROULETTE_WHEEL_APP_REL: "services/repository"
|
||||
ROULETTE_IMAGE: "{{ lookup('config', application_id, 'services.roulette-wheel.image') }}"
|
||||
ROULETTE_VERSION: "{{ lookup('config', application_id, 'services.roulette-wheel.version') }}"
|
||||
ROULETTE_REPOSITORY: "{{ lookup('config', application_id, 'services.roulette-wheel.repository') }}"
|
||||
ROULETTE_REF: "{{ lookup('config', application_id, 'services.roulette-wheel.ref') }}"
|
||||
ROULETTE_IMAGE_CUSTOM: "{{ entity_name }}_custom"
|
||||
@@ -2,6 +2,8 @@
|
||||
oidc:
|
||||
enabled: "{{ 'web-app-keycloak' in group_names }}"
|
||||
shared: "{{ 'web-app-keycloak' in group_names }}"
|
||||
repository: https://github.com/infinito-nexus/taiga-oidc.git
|
||||
ref: c623b753047ce9923f3e38614d86e81fc5eb2bb1
|
||||
ldap:
|
||||
enabled: false # nocheck: dynamic-flag
|
||||
shared: "{{ 'svc-db-openldap' in group_names }}"
|
||||
|
||||
@@ -18,8 +18,8 @@ TAIGA_OIDC_ENABLED: "{{ lookup('config', application_id, 'services.oidc.enabled'
|
||||
TAIGA_LDAP_ENABLED: "{{ lookup('config', application_id, 'services.ldap.enabled', false) }}"
|
||||
TAIGA_PLUGINS_ENABLED: "{{ TAIGA_OIDC_ENABLED | bool or TAIGA_LDAP_ENABLED | bool }}"
|
||||
TAIGA_CONFIG_APPEND_MARKER: "INFINITO_TAIGA_AUTH_CONFIG"
|
||||
TAIGA_OIDC_PLUGIN_REPOSITORY: "https://github.com/taigaio/taiga-contrib-oidc-auth.git"
|
||||
TAIGA_OIDC_PLUGIN_REF: "c623b753047ce9923f3e38614d86e81fc5eb2bb1"
|
||||
TAIGA_OIDC_PLUGIN_REPOSITORY: "{{ lookup('config', application_id, 'services.oidc.repository') }}"
|
||||
TAIGA_OIDC_PLUGIN_REF: "{{ lookup('config', application_id, 'services.oidc.ref') }}"
|
||||
TAIGA_OIDC_BACKEND_PIP_SPEC: "{{ 'git+' ~ TAIGA_OIDC_PLUGIN_REPOSITORY ~ '@' ~ TAIGA_OIDC_PLUGIN_REF ~ '#egg=taiga-contrib-oidc-auth&subdirectory=back' }}"
|
||||
TAIGA_OIDC_FRONTEND_BUILD_COMMAND: "./node_modules/.bin/gulp build"
|
||||
TAIGA_LDAP_PIP_SPEC: "taiga-contrib-ldap-auth-ext==0.5.0"
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
"""Integration guard: every git-clone URL referenced inside a role
|
||||
(Dockerfile, tasks, templates, files) MUST be declared as a
|
||||
`repository:` value in that role's ``meta/services.yml``.
|
||||
|
||||
The same applies symmetrically to git refs: any `docker_git_repository_version`
|
||||
or `--branch <ref>` (clone) value that ties to a declared
|
||||
`repository:` MUST come from the same role's ``meta/services.yml``
|
||||
under `ref:`. Hardcoded git URLs / refs in task files or Dockerfiles
|
||||
silently bypass the entity contract enforced by
|
||||
``test_repository_requires_ref.py`` and the unified naming convention.
|
||||
|
||||
Scope
|
||||
=====
|
||||
Per-role recursive scan of ``roles/<role>/`` for any literal HTTPS
|
||||
git URL (matches ``https?://…/<…>.git``). Each match must appear
|
||||
as a ``repository:`` value in ``roles/<role>/meta/services.yml``.
|
||||
|
||||
Detection
|
||||
=========
|
||||
* `https://github.com/foo/bar.git` literal in source → fail unless
|
||||
declared in the role's services.yml.
|
||||
* The same role MAY reference the URL in multiple files; each
|
||||
reference is fine as long as the URL itself is declared once in
|
||||
services.yml.
|
||||
|
||||
Opt-out
|
||||
=======
|
||||
A specific call site that legitimately uses a literal git URL (for
|
||||
example a plugin / build-helper / extension repo) can add
|
||||
``# nocheck: repository-declared`` on the same line as the URL OR
|
||||
on the line immediately preceding it (so Dockerfiles with `\\`
|
||||
line continuation can put the marker on a standalone comment line).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unittest
|
||||
|
||||
from utils.cache.files import iter_project_files, read_text
|
||||
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"
|
||||
|
||||
# Matches HTTPS git URLs of the form `https://host/<…>/<repo>.git`.
|
||||
# Requires at least one `/<path>/` segment after the host AND that
|
||||
# `.git` ends the URL path (followed by URL-terminator or end of
|
||||
# string). Without the path requirement, hostnames like
|
||||
# `raw.githubusercontent.com` partial-match as `https://raw.git`.
|
||||
_GIT_URL_RE = re.compile(
|
||||
r"https?://[A-Za-z0-9.-]+(?:/[A-Za-z0-9._~-]+)+\.git(?=[\s\"'`<>?#/]|$)"
|
||||
)
|
||||
|
||||
_OPTOUT_MARKER = "nocheck: repository-declared"
|
||||
|
||||
# File extensions worth scanning. Plus extensionless `Dockerfile`.
|
||||
_SCAN_EXTENSIONS = (
|
||||
".yml",
|
||||
".yaml",
|
||||
".j2",
|
||||
".sh",
|
||||
".py",
|
||||
".rb",
|
||||
".js",
|
||||
".ts",
|
||||
)
|
||||
|
||||
|
||||
def _walk_repositories(node) -> set[str]:
|
||||
"""Recursively collect every `repository:` string anywhere in the
|
||||
services.yml tree (top-level entity, sub-entity, plugin map, etc.)."""
|
||||
found: set[str] = set()
|
||||
if isinstance(node, dict):
|
||||
for key, value in node.items():
|
||||
if key == "repository" and isinstance(value, str) and value:
|
||||
found.add(value.strip())
|
||||
else:
|
||||
found |= _walk_repositories(value)
|
||||
elif isinstance(node, list):
|
||||
for item in node:
|
||||
found |= _walk_repositories(item)
|
||||
return found
|
||||
|
||||
|
||||
def _collect_declared_repositories(services_path) -> set[str]:
|
||||
data = load_yaml_any(str(services_path), default_if_missing=None)
|
||||
return _walk_repositories(data)
|
||||
|
||||
|
||||
def _iter_role_files(role_dir):
|
||||
"""Every text file under the role that could plausibly contain a
|
||||
literal git URL. Uses the project-wide path cache so the scan
|
||||
re-uses what other tests already enumerated."""
|
||||
role_prefix = str(role_dir) + "/"
|
||||
yield from (
|
||||
p
|
||||
for p in iter_project_files(extensions=_SCAN_EXTENSIONS)
|
||||
if p.startswith(role_prefix)
|
||||
)
|
||||
# `files/Dockerfile` (no extension) is not picked up by the
|
||||
# extension filter; iterate the cache once more explicitly.
|
||||
for p in iter_project_files():
|
||||
if p.startswith(role_prefix) and p.endswith("/Dockerfile"):
|
||||
yield p
|
||||
|
||||
|
||||
class TestRepositoryDeclaredInServices(unittest.TestCase):
|
||||
def test_git_urls_in_role_files_are_declared_in_services_yml(self):
|
||||
offenders: list[str] = []
|
||||
|
||||
for role_dir in sorted(p for p in ROLES_DIR.iterdir() if p.is_dir()):
|
||||
role_name = role_dir.name
|
||||
services_path = role_dir / ROLE_FILE_META_SERVICES
|
||||
declared = (
|
||||
_collect_declared_repositories(services_path)
|
||||
if services_path.is_file()
|
||||
else set()
|
||||
)
|
||||
|
||||
seen_paths: set[str] = set()
|
||||
for fpath in _iter_role_files(role_dir):
|
||||
if fpath in seen_paths:
|
||||
continue
|
||||
seen_paths.add(fpath)
|
||||
# Skip the services.yml itself — the declaration is the
|
||||
# authoritative source we are checking against.
|
||||
if fpath.endswith("/" + ROLE_FILE_META_SERVICES):
|
||||
continue
|
||||
try:
|
||||
text = read_text(fpath)
|
||||
except (OSError, UnicodeDecodeError):
|
||||
continue
|
||||
if "https://" not in text and "http://" not in text:
|
||||
continue
|
||||
lines = text.splitlines()
|
||||
for line_no, line in enumerate(lines, 1):
|
||||
if _OPTOUT_MARKER in line:
|
||||
continue
|
||||
# Marker on the preceding line also counts (handy
|
||||
# for Dockerfile `\\` continuations and similar).
|
||||
if line_no >= 2 and _OPTOUT_MARKER in lines[line_no - 2]:
|
||||
continue
|
||||
for m in _GIT_URL_RE.finditer(line):
|
||||
url = m.group(0)
|
||||
if url in declared:
|
||||
continue
|
||||
rel = fpath.split(str(PROJECT_ROOT) + "/", 1)[-1]
|
||||
offenders.append(
|
||||
f"{role_name}: {rel}:{line_no} "
|
||||
f"references `{url}` but it is not declared "
|
||||
f"as `repository:` in "
|
||||
f"{role_name}/{ROLE_FILE_META_SERVICES}"
|
||||
)
|
||||
|
||||
if offenders:
|
||||
self.fail(
|
||||
f"{len(offenders)} hardcoded git URL(s) bypass the "
|
||||
f"`meta/services.yml` repository declaration:\n"
|
||||
+ "\n".join(f"- {o}" for o in offenders)
|
||||
)
|
||||
Reference in new issue
Block a user