Commit Graph
20 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 4.7 eb45f0ffaf feat(cache): inner-build override + TTL env-var + SPOT docs
Extend the local cache stack with three independent capabilities:

1. Inner-`dockerd` build coverage for HTTP-only package mirrors.
   `roles/sys-svc-compose/files/compose.py` (the runner's `compose`
   wrapper) auto-generates a transient `compose.cache.override.yml`
   per app when `INFINITO_PACKAGE_CACHE_FRONTEND_IP` is exported. The
   override pins `build.extra_hosts` for `deb.debian.org`,
   `archive.ubuntu.com`, and `security.ubuntu.com` to the frontend.
   `pull.py` now delegates to the wrapper so build/pull operations see
   the same `-f` set the wrapper auto-detects. nginx in the frontend
   gains a port-80 listener with path rewrites for those hostnames,
   and `package.sh` registers two new Nexus proxies
   (`apt-debian-security`, `apt-ubuntu-security`) for the security
   suites whose upstream URL prefix differs from the main archive.

   Alpine is intentionally excluded from the inner-build extra_hosts:
   3.18+ defaults to HTTPS, and inner-`dockerd` build containers do
   not import the frontend CA. Routing apk through the frontend would
   trip TLS verification.

2. General cache freshness env-var. `scripts/meta/env/cache/package.sh`
   exports `INFINITO_PACKAGE_CACHE_MAX_AGE_MIN` (default `129600`,
   i.e. 90 days). The bootstrap helper consumes it for every Nexus
   proxy's `contentMaxAge`, `metadataMaxAge`, and
   `negativeCache.timeToLive`, replacing the previous 1440-minute
   literal. Lower the value when chasing fast-moving upstreams.

3. SPOT cache documentation at `docs/contributing/environment/cache.md`.
   `compose.yml.md`, `variables.md`, the per-service READMEs, and
   `requirements/012-...` now link to it instead of restating the
   stack's behaviour. Net documentation lines drop substantially while
   the cache page becomes the single user-facing reference.

Also: code-comment cleanup per `docs/contributing/documentation.md`
guidelines across all touched .py / .sh / .yml files. Multi-paragraph
docstrings, banner separators, and restate-the-obvious comments are
removed; one-line WHY comments remain where the rationale is not
already visible in the code.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 01:08:41 +02:00
kevinveenbirkenbachandClaude Opus 4.7 1dbc44af2c refactor(cache): SPOT for compose-file flags + override env vars
Move the `docker compose -f ...` flag list and the cache-override
env-var setup into single helpers in `cli/deploy/development/common.py`:

  * `compose_file_args()` — decides whether to layer
    `compose/cache.override.yml` on top of `compose.yml`.
  * `cache_env_overrides()` — produces the env-var dict the
    override file requires strictly via `${VAR:?…}`.

Both `cli.deploy.development.compose` (up flow) and
`cli.deploy.development.down` (down flow) now call these helpers, so
up and down resolve to the same compose-file set and the same env
vars. Previously the cache env-var setup was duplicated inline in
compose.py and missing entirely from down.py, which made `make down`
fail under the `cache` profile with a confusing "required variable
missing" error before even reaching the orphan-cleanup step.

Single SPOT discipline: the host CA directory default lives only in
`scripts/meta/env/cache/package.sh` (sourced via BASH_ENV by every
Makefile recipe). `cache_env_overrides()` refuses to fall back to a
duplicate literal — a missing env var raises SystemExit pointing at
the env script. Tests that bypass the env layer set the var
explicitly so the SPOT-strict guard does not raise.

Containerize cert-gen: the original
`scripts/docker/cache/package-frontend-certs.sh` ran `mkdir`/`openssl`
host-side, which fails when `/var/cache/infinito` is not writable from
the invoking shell (sandboxed tooling, non-root operators). Move the
openssl logic into `compose/package-cache-frontend/cert-gen.sh` and
have the host wrapper docker-run it inside a throw-away alpine
container so the docker daemon (root) creates the bind-mount source
paths. Same pattern as how rpardini's registry-cache generates its CA
in-container.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 22:47:34 +02:00
kevinveenbirkenbachandClaude Opus 4.7 334c915598 feat(cache): transparent TLS frontend + override-only gating
Add a TLS-terminating reverse proxy in front of the Sonatype Nexus 3 OSS
pull-through cache so the `infinito` runner can hit upstream
package-manager URLs (pypi.org, registry.npmjs.org, rubygems.org,
repo.packagist.org, proxy.golang.org, dl-cdn.alpinelinux.org,
files.pythonhosted.org, raw.githubusercontent.com, codeload.github.com)
unchanged and still flow through Nexus.

Mechanism:
  * scripts/docker/cache/package-frontend-certs.sh — host-side helper,
    idempotently generates a dedicated CA + per-hostname leaf certs.
  * compose/package-cache-frontend/upstreams.conf — nginx server-blocks
    that match SNI per hostname and reverse-proxy onto the matching
    Nexus repo path.
  * compose/cache.override.yml `infinito.extra_hosts` — DNS-hijack so
    the runner's clients resolve each upstream onto the frontend's
    static IP.
  * scripts/docker/cache/package-frontend-ca.sh — runner-side helper,
    drops the CA into the system trust store post-startup so apt/pip/
    curl/etc. accept TLS against the hijacked hostnames.
  * cli/deploy/development/compose.py — orchestrates cert gen before
    compose-up, CA install after health.

Scope today: runner-only. App-service Dockerfile builds executed by
the inner dockerd are NOT routed through the frontend; that requires
build-time CA injection and is deferred.

Also extends scripts/docker/cache/package.sh with five additional
Nexus proxies registered for transparent use: gem-proxy, go-proxy,
yum-rocky, yum-fedora, raw-packagist, raw-alpine, raw-codeload-github.

Refactor: move all cache services + runner-side cache mounts/depends_on/
extra_hosts from compose.yml into compose/cache.override.yml. Base
compose.yml now describes only the bare CI/runtime stack (coredns +
infinito). The override file is the single gate for the cache stack;
`profiles: ["cache"]` attributes and `--profile cache` flag emission
were redundant once file inclusion controls existence and removed
accordingly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 18:26:21 +02:00
kevinveenbirkenbachandClaude Opus 4.7 fe54854fe5 refactor(resolver): variant-aware combined resolver and per-round include
Single SPOT for matrix-deploy topology
=======================================

Before this change two paths derived the deploy plan from different
sources and could disagree:

 * Group inclusion (`--include` / `group_names`): driven by
   `cli.meta.applications.resolution.combined.CombinedResolver`,
   which read each role's `meta/services.yml` raw and ignored
   `meta/variants.yml` entirely. A variant that flipped
   `services.<X>.enabled` to a literal `true` did NOT pull `<X>`'s
   provider role into the deploy plan.

 * Inventory bake (`host_vars/applications.<app>.*`): driven by the
   variant-aware payload merge in `cli.deploy.development.inventory`,
   so the role saw the variant-merged services map at deploy time.

The split surfaced concretely for `web-app-wordpress` variant 0
(`services.discourse.enabled: true` in `meta/variants.yml`): the
inventory baked `discourse.enabled = true`, the wp-discourse plugin
task gate fired, and the install loop tried `docker exec discourse`
against a container the variant-blind resolver had never pulled.

This commit collapses both consumers onto the same per-round,
variant-merged services map. The variant-aware resolver (Phase 1+2)
is the additive plumbing, but the deploy/init wiring (Phase 3-5) is
what actually changes behaviour at runtime.

Variant-aware resolver (Phase 1+2)
----------------------------------

`role_introspection.load_shared_service_roles_for_app` accepts a
`services_override` kwarg; `CombinedResolver.__init__` accepts a
`{role_name: services_dict}` map and routes it through the loader.
A `CombinedResolver` instance is round-specific when overrides are
supplied — its per-role edge cache is variant-resolved, do NOT reuse
across rounds. The combined-resolver CLI gains
`--services-overrides <path>`.

`cli.deploy.development.deps.resolve_run_after` threads the
in-container path of the overrides JSON into the resolver
invocation.

Per-round include planner (Phase 3)
-----------------------------------

`cli.deploy.development.inventory.plan_dev_inventory_matrix` now
takes `primary_apps` (just the user-named apps) instead of a
pre-resolved `include` list and returns a 4-tuple per round
`(round_index, inv_dir, round_variants, include_R)`. Per round it:

 1. Computes `round_variants` clamped to each primary's variant
    count.
 2. Builds variant-merged services overrides for every role with a
    `meta/variants.yml`, picking that role's variant for the round
    (clamped) — so transitive deps with their own variants follow
    the round.
 3. Runs `CombinedResolver(services_overrides=...)` per primary and
    unions the result with the primary apps in user order.
 4. Extends `round_variants` with the discovered deps' clamped
    variant indices so the deploy step's per-round purge logic stays
    accurate.

`build_dev_inventory_matrix` keeps its name but signature changes:
it now takes `primary_apps` and an optional `include_filter` for
SERVICES_DISABLED-style consumer filters.

Init / deploy wiring (Phase 4-5)
--------------------------------

`init.py` and `deploy.py` consume the per-round `include_R`
directly, drop the old `resolve_deploy_ids_for_apps` pre-resolution
step, and apply the SERVICES_DISABLED provider filter on the
include set so the inventory `--include` flag and the final ansible
deploy ids stay in lockstep.

Behaviour invariance for non-variant roles
------------------------------------------

Apps with no `meta/variants.yml`, or without a `services.<X>.enabled`
override on any variant, behave identically to before:

 * The override map for those roles is empty — resolver falls
   through to the disk-only path.
 * Per-round `include_R` collapses to the same set every round.
 * Inventory bake outputs the same `applications.<app>.*` shape.

The behaviour change is scoped to roles that legitimately use
variants to flip topology. In the current repo only
`web-app-wordpress` is affected (variant 0 pulls
`web-app-discourse` in; variant 1 leaves it out, exercising the
multisite-without-discourse path).

Test surface
------------

 * `tests/unit/cli/deploy/development/test_inventory.py` updated
   for the new planner signature, mocking
   `_resolve_round_include` / `_build_services_overrides_for_round`
   so the planner's tuple shape is asserted independently of the
   live resolver.
 * `tests/unit/cli/deploy/development/test_matrix_deploy.py` adds a
   `_entry()` helper that defaults `include` to the keys of
   `round_variants`, so existing fixtures stay terse — round 0 of
   the deploy loop deploys whatever is in `include_R`, and the
   variant-aware planner naturally emits the same set.
 * `tests/unit/utils/cache/test_data.py` pins the planner contract
   (`primary_apps=`, 4-tuple plan entries with `include_R`).
 * `tests/utils/__init__.py` added so the
   `tests.lint.filesystem.test_init_files` lint test passes
   end-to-end (the directory existed in the tree without an
   __init__.py and tripped the lint).

Verification
------------

 * `make test` green (162 tests, 1 skipped) on the merged tree.
 * `make deploy-fresh-kept-apps APPS="web-app-wordpress"
   FULL_CYCLE=true SERVICES_DISABLED="matomo,email"` end-to-end:
   round 0 (variant 0, single-site + Discourse integration) and
   round 1 (variant 1, multisite) each ran PASS 1 (sync) and PASS 2
   (async), all four passes with `failed=0 rescued=0`. Round 0's
   per-round include contained `web-app-discourse`; round 1's did
   not — exactly the variant-driven topology the resolver is
   supposed to produce.

The unrelated Joomla CLI install race (CI run 25079786137 `Deploy
web-app-joomla`) is parked separately as infinito-nexus/core#209;
its candidate fix is in that issue body, not in this commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 16:28:12 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b34131ca1d feat(package-cache): add Sonatype Nexus 3 OSS pull-through cache (req-012)
Brings up the package-cache compose service alongside the existing
registry-cache, gated by the same `cache` profile. Bootstrap helper
(scripts/docker/cache/package.sh) auto-rotates the admin password from
the well-known default, accepts the Sonatype CE EULA (3.71+ blocks
artifact downloads with HTTP 403 until /v1/system/eula is POSTed —
disclaimer must be sent verbatim with its unicode quotes), enables
anonymous read, creates one default blobstore, and registers the MVP
proxy set: apt-debian, apt-ubuntu, pypi-proxy, npm-proxy, helm-bitnami,
raw-githubusercontent. The runner gets pip.conf / .npmrc /
sources.list snippets bind-mounted in (each defaulting to /dev/null
when the cache profile is inactive) so the deploy is byte-identical
with cache off.

Cache plumbing:
- scripts/meta/env/cache/{registry,package}.sh: per-cache env defaults,
  hostname-derived admin password seed (uses `hostname` command, not
  $HOSTNAME shell var, so the value is identical across interactive
  shells, non-interactive subshells, and Python subprocesses).
- scripts/docker/cache/{registry-ca,package}.sh: container-side
  bootstrap helpers, both moved under scripts/docker/cache/.
- scripts/system/cache/clean.sh + `make cache-clean`: wipes both
  /var/cache/infinito/core/cache/{registry,package} dirs and stops
  the cache containers.
- cli/deploy/development/compose.py: Profile gate exports the three
  new INFINITO_PACKAGE_CACHE_* client-snippet paths and runs the
  bootstrap helper after the package-cache reports healthy.

Compose service uses sonatype/nexus3:3.91.1-ubi pinned, runs as
root (UBI image refuses to chown a fresh bind-mount), and forces
NEXUS_SECURITY_RANDOMPASSWORD=false so the bootstrap can log in with
admin/admin123 before rotating; the random-password mode otherwise
auto-completes onboarding silently and leaves no readable secret on
disk for an automated bootstrap to pick up.

Tests: tests/unit/cli/deploy/development/test_compose.py grows two
new assertions (cache services listed in `up` args when local; build
tests pin CI=true to retain ci-only assertions).

Closes req-012.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 03:11:45 +02:00
kevinveenbirkenbachandClaude Opus 4.7 94936f198b feat(compose): gate registry-cache to local dev only via Profile class
Move the registry-cache pull-through proxy out of the always-on `ci`
profile into its own `cache` profile, and select it based on the runtime
environment instead of statically.

Why
   The proxy only earns its keep when image pulls amortize across many
   runs on the same host. That holds on developer machines (cross-run
   image dedup, persistent host disk) but not on GitHub-hosted runners
   (fresh disk per job), where the proxy startup overhead is net loss
   without the corresponding payoff.

How
* Compose: split `registry-cache` into its own `cache` profile;
  `infinito.depends_on.registry-cache` gets `required: false` so Compose
  does not auto-start the proxy via the dependency when the profile is
  inactive; the systemd drop-in bind-mount source is gated by the new
  `INFINITO_REGISTRY_CACHE_PROXY_CONF` env var with `/dev/null` default,
  so dockerd loads an empty drop-in (no `HTTP_PROXY`, no `ExecStartPre`)
  when the proxy is not running and pulls images directly.
* New `cli/deploy/development/profile.py` `Profile` class is the SPOT
  for which compose profiles activate. KISS + SRP: `is_ci()` reads the
  three CI signals (`GITHUB_ACTIONS`, `RUNNING_ON_GITHUB`, `CI`),
  `registry_cache_active()` is its strict inverse, and `args()` returns
  the matching `--profile ...` flag list.
* `Compose` and `down.py` consume the same `Profile` so up/down cannot
  drift on which services are part of the stack.
* Documentation under `compose/registry-cache/README.md`,
  `docs/contributing/artefact/files/compose.yml.md` and
  `docs/contributing/environment/variables.md` describes the new toggle
  and the `INFINITO_REGISTRY_CACHE_PROXY_CONF` env var. Pre-existing
  documentation.md violations in those files (one SPOT self-label, one
  em dash, two path-as-link-text instances) corrected in the same pass
  per the fix-nearby rule.
* Tests: 14 new `Profile` unit tests in `test_profile.py` plus two
  scenario tests in `test_compose.py` that lock down the
  `--profile ci` (CI runner) vs `--profile ci --profile cache` (local)
  argument shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 00:25:44 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b9353118d1 fix(meta): close req-008 sweep gaps — lost suppressions, silent test breakage, 1 prod bug
The req-008/009/010 refactor's mechanical migration left several gaps
the test suite could not catch because the test suite itself was
silently broken in spots. Audit-and-fix pass closes those.

1. Production bug: backup discovery filter
==========================================
roles/sys-ctl-bkp-docker-2-loc/filter_plugins/find_dock_val_by_bkp_entr.py
still read `app.compose.services` from the materialised application
payload. After req-008 the file root IS the services map (no
`compose.services` envelope), so the filter walked an empty dict and
silently returned `[]` for every backup discovery call. Rerouted to
the new `app.services` path. Test fixtures updated to match.

2. External test broken by source_file mismatch
===============================================
tests/external/docker/test_image_versions.py:61 filtered for
`ref.source_file == "config/main.yml"`, but
utils/docker/image/discovery emits `meta/services.yml` since req-008.
The filter rejected every candidate → `entries == []` →
`self.assertTrue(entries)` failed. Switched the filter to the new path.

3. Five lint/integration tests silently iterating empty dicts
=============================================================
The following tests still read `cfg.get("compose").get("services")`
(the legacy compose-wrapper path). After req-008 every meta/services.yml
file root IS the services map, so these tests have been iterating
empty dicts and quietly passing without checking anything:

* tests/lint/ansible/test_service_shared_consistency.py:72
* tests/integration/backups/test_enabled.py:33
* tests/integration/backups/test_no_stop_required.py:35
* tests/integration/docker/test_services_image_version_valid.py:28
* tests/integration/oauth2_oidc/test_mutual_exclusive.py:26

All five rerouted to the file-root convention. test_service_shared_consistency
immediately surfaced 24 missing `# noqa: shared` markers (see below).

4. Lost suppression markers (51 total)
======================================
The migration script (tasks/utils/migrate_meta_layout.py) used
ruamel.yaml round-trip which preserved most comments but lost
trailing-comment markers on inline list items (a known ruamel edge
case). Restored verbatim from main:

* 13 `# nocheck: url` markers on meta/server.yml CSP-whitelist entries
  (web-app-bluesky, web-app-flowise, web-app-matomo, web-svc-onlyoffice).
  These cover CDN root URLs that legitimately return 404 for the bare
  domain (Google Fonts, Matomo CDN, BlueSky API endpoint).
* 1 `# nocheck: docker-version` on web-app-moodle/meta/services.yml
  (LTS pin for OIDC compatibility).
* 20 `# noqa: shared` markers on meta/services.yml — only the markers
  that are still NEEDED. Initial restoration mirrored all 24 markers
  from main, but 4 of them turned out redundant against the current
  shape: bigbluebutton/postgres, bookwyrm/postgres, discourse/postgres,
  magento/mariadb already declare `shared:` (true or false), so the
  lint rule does not fire and the marker is dead weight. Skipped them.
* 13 `# noqa: email` markers on meta/services.yml — every covered
  `email:` block has `{enabled: false, shared: false}`, exactly the
  pattern the email-integration lint expects to suppress.

5. Test-fixture cleanups for backwards-compat clarity
=====================================================
Several unit tests still wrapped synthetic application payloads in
`{"compose": {"services": {...}}}`. Harmless because the production
code paths under test were already migrated, but misleading for future
contributors. Reshaped to match the post-req-008 file-root
convention:

* tests/unit/plugins/filter/test_node_autosize.py
* tests/unit/plugins/filter/test_docker_service_enabled.py
* tests/unit/utils/manager/test_inventory_manager.py (5 occurrences)
* tests/unit/cli/deploy/development/test_inventory.py
* tests/unit/roles/sys-ctl-bkp-docker-2-loc/filter_plugins/test_find_dock_val_by_bkp_entr.py

Validation
==========
* Unit:        1572 passed.
* Lint:        56 passed.
* Integration: 161 passed (1 skipped).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:25:38 +02:00
kevinveenbirkenbachandClaude Opus 4.7 3f2021b07d refactor: collapse INFINITO_DISTRO/INFINITO_CONTAINER to a single SPOT and align all consumers
Follow-up to d002796a5. The formula `infinito_nexus_${INFINITO_DISTRO}` is
now defined in exactly one place — `scripts/meta/env/defaults.sh:18`,
exported outside the load-once guard so it tracks INFINITO_DISTRO across
matrix iterations (caller re-sources defaults.sh after distro changes).
Every other site reads INFINITO_CONTAINER strictly from env; nothing
re-derives the formula, no lazy/colon-dash fallbacks, no per-call defaults.

Production code aligned:

- cli/deploy/development/common.py: add resolve_container() (strict env
  reader, raises SystemExit on unset). make_compose() calls it as an
  early-fail guard so a missing defaults.sh source surfaces here instead
  of as a confusing compose.yml interpolation error later.
- cli/deploy/development/deploy.py: stop re-deriving from compose.distro;
  use resolve_container().
- compose.yml: container_name reads ${INFINITO_CONTAINER:?...} strictly,
  so callers that bypass defaults.sh fail fast with a clear hint.
- scripts/meta/resolve/image/local.sh: drop ${INFINITO_DISTRO:-debian}
  fallback; require INFINITO_DISTRO via :? error.
- scripts/tests/deploy/ci/dedicated.sh: drop local _container fallback.
- scripts/tests/deploy/local/deploy/fresh-kept-app.sh: drop redundant
  ${INFINITO_DISTRO:-debian} reassignment after sourcing env/all.sh
  (defaults.sh already defaults the value).
- scripts/tests/deploy/local/deploy/fresh-purged-app.sh: drop
  target_container fallback; INFINITO_CONTAINER is already in env.
- scripts/tests/deploy/local/exec/container.sh: drop the lazy
  ${INFINITO_CONTAINER:=infinito_nexus_...} default; defaults.sh source
  in env/all.sh already guarantees the value.
- scripts/tests/dns.sh: source defaults.sh so callers only need to set
  INFINITO_DISTRO (the implicit dependency was previously a footgun).
- scripts/system/tls/trust/{wsl2,linux}.sh: same — source defaults.sh,
  drop the ${INFINITO_CONTAINER:-infinito_nexus_debian} fallback in
  linux.sh.
- .github/workflows/test-dns.yml: drop redundant
  INFINITO_CONTAINER: infinito_nexus_${{ matrix.distro }} from per-step
  env blocks (3 sites). The env now flows from INFINITO_DISTRO via the
  defaults.sh sourced inside the scripts.
- tests/unit/cli/deploy/development/test_matrix_deploy.py: replace the
  module-level os.environ.setdefault('INFINITO_CONTAINER', 'infinito_nexus_arch')
  shim — which itself encoded the formula — with a module-level patch of
  resolve_container that returns an opaque test-fixture string. Drop the
  now-unused distro="arch" field from the _args() Namespace fixture
  (deploy.handler stopped consuming it after the --distro arg removal).

Documentation aligned:

- docs/contributing/environment/variables.md: rewrite the INFINITO_CONTAINER
  row to spell out the SPOT contract — set automatically by defaults.sh,
  never re-derived elsewhere, callers re-source defaults.sh after
  changing INFINITO_DISTRO mid-process.
- docs/contributing/artefact/files/compose.yml.md: replace the
  INFINITO_DISTRO row (which described the now-removed
  `infinito_nexus_${INFINITO_DISTRO:-debian}` interpolation) with an
  INFINITO_CONTAINER row noting the strict ${INFINITO_CONTAINER:?...} read.

Verified:
  - Formula derivation: 1 production hit (the SPOT in defaults.sh:18).
  - INFINITO_CONTAINER fallback patterns (:-/:=): 0.
  - --distro CLI arg refs: 0 (one comment explaining the removal).
  - make test green: 1553 unit + 168 integration tests, no skipped suites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:21:35 +02:00
kevinveenbirkenbachandClaude Opus 4.7 3e31630d58 fix(ci/deploy)+refactor(cli/deploy/development): single-SPOT INFINITO_DISTRO/INFINITO_CONTAINER
Two related improvements anchored on the same principle: the env var
INFINITO_DISTRO is the single source of truth for the development distro,
and INFINITO_CONTAINER is purely derived from it.

1. Fix the matrix-deploy purge failure (CI run 24942172018, job 73038526513,
   web-app-wordpress @ fedora):
   scripts/meta/env/defaults.sh eager-exported INFINITO_CONTAINER once
   inside its load-once guard; ci/all.sh's iteration loop re-exported
   INFINITO_DISTRO per round but the cached INFINITO_CONTAINER stayed at
   the initial-default infinito_nexus_debian, so _purge_app_entities()
   between rounds tried to docker exec into a container from the wrong
   distro and failed with "No such container".

   Move the INFINITO_CONTAINER derivation OUTSIDE the load-once guard so
   every re-source picks up a CONTAINER name in lock-step with the current
   INFINITO_DISTRO. ci/all.sh now re-sources defaults.sh after each
   export INFINITO_DISTRO=\$distro to trigger that re-derivation. defaults.sh
   stays the single spot that owns the formula.

2. Retire the --distro CLI arg from cli.deploy.development {up,down,stop,
   restart,init,deploy,exec,logs,build}. The arg duplicated INFINITO_DISTRO
   (its default already read from the env), creating a hidden second source
   of truth that drifted from the env across matrix iterations. Replace it
   with cli.deploy.development.common.resolve_distro() which reads
   INFINITO_DISTRO strictly (raises SystemExit on unset/invalid), and
   simplify make_compose() to take no args. All bash callers
   (scripts/tests/deploy/{ci,local}/**, Makefile restart target) drop the
   redundant --distro \"\${INFINITO_DISTRO}\" pass-through.

Verified: make test passes (1553 unit + 168 integration). The mechanism of
the WP fix is verified by a synthetic distro-cycling test of defaults.sh
re-derivation; the original CI multi-distro repro can only be confirmed
end-to-end by the next CI push (local single-distro deploys do not exercise
the stale-env path).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:21:34 +02:00
kevinveenbirkenbachandClaude Opus 4.7 d97ed7a9e8 feat(deploy): per-role matrix-deploy variants with folder-per-round model
Introduces per-role `meta/variants.yml` for declaring multi-variant
deploys (e.g. WordPress Single-Site vs. Multisite) and reshapes the
development deploy CLI to materialise one inventory folder per variant
and iterate them with co-located sync+async passes.

Why:
- The previous single-inventory + Pass-1-then-Pass-2 split deployed each
  variant twice and ran the async pass against a host left in a
  different variant's state, masking real bugs.
- Inventory and runtime concerns were tangled: variant selection lived
  as a runtime extra-var and required cross-stage coordination.

How:
- `roles/<role>/meta/variants.yml` (file format docs at
  docs/contributing/artefact/files/role/variants.md): YAML list of
  deep-merge overrides on top of `config/main.yml`. Empty entry `{}` is
  variant 0 (= legacy payload). 6 roles get their dev overrides moved
  here from inventories/dev.yml; WordPress declares 2 variants.
- `utils/runtime_data.py` gains `_build_variants` / `get_variants` and
  drops the retired `_active_variants` runtime selector. Loader always
  returns variant 0 as the default; non-zero variants reach
  `get_merged_applications` as inventory-level `applications.<app>`
  overrides baked at init time.
- New `cli/deploy/development/inventory.py`: `DevInventorySpec`,
  `plan_dev_inventory_matrix` (pure planner shared between init and
  deploy), `build_dev_inventory_matrix` (materialises one folder per
  round at `<dir>-<R>`, or `<dir>` when single-variant). Single-folder
  paths preserve full backward compat.
- `cli/deploy/development/deploy.py`: matrix loop with per-round
  cleanup (only apps whose variant changed AND that are about to
  re-deploy), `--variant <idx>` (or `VARIANT=` env) to pin a single
  round, and `--full-cycle` (or `FULL_CYCLE=true`) which interleaves
  sync + async per round so the async re-deploy targets exactly the
  host state the matching sync deploy just produced.
- `cli/deploy/development/exec.py` gains repeatable `--env KEY=VAL` so
  bash callers can inject per-run context into in-container helpers
  without inlining heredocs.
- `scripts/meta/env/inventory.sh` exports `INVENTORY_VARS_FILE`
  (Python mirror in `cli.deploy.development.common.DEV_INVENTORY_VARS_FILE`;
  test_inventory_vars_file_spot.py locks them against drift).
- `make deploy-fresh-purged-apps` and `scripts/tests/deploy/ci/dedicated.sh`
  switch to a single init + single deploy `--full-cycle`. The
  `make deploy-reuse-*` bash scripts honour `VARIANT=` by suffixing
  `INVENTORY_DIR`/`INVENTORY_FILE`.
- The 5 `scripts/tests/deploy/local/deploy/*.sh` scripts now delegate
  their in-container heredocs to dedicated helpers in
  `scripts/tests/deploy/local/utils/`, removing the bash-in-bash
  quoting hell.

Tests:
- `tests/unit/utils/test_variants.py` (renamed from
  `test_application_variants.py`): loader edge cases, variant-zero
  default, retired-`_active_variants` guard.
- `tests/unit/cli/deploy/development/test_inventory.py` (new): planner,
  variant baking, builder, `filter_plan_to_variant`, `DevInventorySpec`.
- `tests/unit/cli/deploy/development/test_matrix_deploy.py` (new):
  per-round deploy filter, cleanup decisions, `--variant` pinning,
  `--full-cycle` per-round interleave.
- `tests/unit/cli/deploy/development/test_inventory_vars_file_spot.py`
  (new): drift guard for the bash <-> Python literal pair.
- `tests/unit/cli/deploy/development/test_exec.py` extended for `--env`.

Docs:
- `docs/contributing/artefact/files/role/variants.md` (new): file format.
- `docs/contributing/design/matrix-deploy.md` (new): runtime mechanism
  (folder layout, --variant, --full-cycle, cleanup logic).
- `docs/contributing/actions/deploy.md` (new): contributor dev-deploy
  flow (make targets, --variant pinning).
- `docs/administration/deploy.md` slimmed to admin-only (was mixed
  admin+dev), cross-links to the new contributor doc.
- `docs/agents/action/iteration/role.md` keeps only agent-iteration
  recipes; mechanism explanation moved out and linked.
- `docs/contributing/documentation.md` adds the "no self-SPOT
  declaration" rule; `applications.md` renamed its SoT header for
  consistency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 13:47:48 +02:00
kevinveenbirkenbachandClaude Sonnet 4.6 3cfdc4bdf7 feat(services-disabler): propagate SERVICES_DISABLED through deploy stack and add conflict detection
Adds inventory consistency guard that fails fast when SERVICES_DISABLED
conflicts with an already-deployed inventory, and ensures the env var is
forwarded through the dedicated runner, development exec command, and
local test scripts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 19:36:16 +02:00
Environment TestandClaude Sonnet 4.6 1530788dbe feat(mirror): add repository segment to GHCR image path
Image names now follow {owner}/{repository}/mirror/{registry}/{name}
instead of {owner}/mirror/{registry}/{name}.

- GHCRProvider: add `repository` param + `add_args()`/`from_args()` classmethods
  as SPOT for CLI argument registration and provider construction
- sync, resolver: use GHCRProvider.add_args() / from_args() (no duplication)
- Workflows: resolve repository name via scripts/meta/resolve/repository/name.sh
  and pass --ghcr-repository to sync and publish
- mirrors.sh: require and forward GHCR_REPOSITORY env var
- deploy/development/mirrors.py: derive repository from GITHUB_REPOSITORY
- Tests: update expected paths, argv and env fixtures

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 00:20:41 +02:00
kevinveenbirkenbach 9cbb12d540 Propagate host MTU to the local compose network 2026-03-31 21:00:51 +02:00
kevinveenbirkenbachandClaude Sonnet 4.6 a50c90c733 fix(deploy): resolve SERVICES_DISABLED service names to app IDs and verify inventory
- deploy.py: use find_provider_roles() to map service names (e.g. 'matomo')
  to provider app IDs (e.g. 'web-app-matomo') before filtering deploy_ids;
  forward SERVICES_DISABLED into the container via extra_env
- container.sh: rename INSPECT_CMD to CMD for consistency; fix indentation
- Makefile: expose CMD variable on exec target for non-interactive use
- environment.sh: source scripts/meta/env/all.sh for INVENTORY_DIR; add
  inventory verification after SERVICES_DISABLED deploy using make exec
- variables.md: document CMD variable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 01:26:44 +02:00
kevinveenbirkenbachandClaude Sonnet 4.6 135ceefa98 fix(build): add SPOT for local image name and fix missing REPO_PREFIX
Add scripts/meta/resolve/image/local.sh as SPOT for the local Docker
image tag (<repo-basename>/<distro>). Wire it into Makefile build
targets (IMAGE_TAG), compose.py (_base_env sets INFINITO_IMAGE), and
compose.yml (no default fallback). Fix build.sh to accept a pre-set
IMAGE_TAG without requiring REPO_PREFIX. Add make build + build-no-cache
to environment.sh test flow with correct section ordering.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 00:16:27 +02:00
kevinveenbirkenbach 44ea2df452 Fix dev workflow image build and docker repo bootstrap 2026-03-19 19:11:51 +01:00
kevinveenbirkenbach ad3f159548 Require Docker Compose in dev packaging 2026-03-19 11:25:49 +01:00
kevinveenbirkenbach ec30948546 fix(deploy): use bash -lc for commands with pipefail 2026-02-21 16:05:42 +01:00
kevinveenbirkenbach 0710c41f91 fix(ci): run mirror resolver with venv python in container 2026-02-20 11:25:46 +01:00
kevinveenbirkenbach efe2ea3357 **test: align deployment type tests with invokable-based semantics**
Update unit tests to reflect the new invokable-driven classification logic by mocking invokable path discovery, removing legacy alias expectations, and adding coverage for universal and mixed cases. Also relocate CLI deploy tests into the unit test hierarchy for consistency.
2026-02-10 21:38:02 +01:00