refactor(profile): split runtime capabilities from runtime identity

One boolean answered five unrelated questions: whether the pull-through cache stack runs, whether image pulls take the GHCR mirror, whether the docker data root may be wiped, plus runtime naming and inventory resolution. The workspace is the environment where those answers diverge — it simulates a developer machine on a real runner — so getting one of them meant misreporting where the suite runs and silently paying for the other four.

is_ci() and runs_on_github() stay facts about the machine. cache_stack_enabled, image_mirror_enabled and docker_root_ephemeral become decisions that default off those facts and can be declared explicitly, so an environment states what it wants. Every default reproduces the previous condition exactly, including the distinction the old mirror and wipe gates drew between a real runner and act.

73ae40585 got the cache stack by flipping the identity flag and lost the docker-root wipe with it; the workspace now declares all three capabilities instead. Pairs with 4c068d1cf, which makes the resulting stray bind sources harmless rather than fatal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-08-23 19:52:03 +02:00
1 parent b387c80528
commit 48845db979
11 files changed
+109 -44

No files matched your search

@@ -201,7 +201,7 @@ class Compose:
if self.profile.owns_cache_stack():
self._bootstrap_package_cache(env)
if self.profile.registry_cache_active():
if self.profile.cache_stack_enabled():
self._install_package_frontend_ca_in_runner()
if run_entry_init:
@@ -38,7 +38,9 @@ def _wipe_docker_root(docker_root: Path) -> None:
def _should_wipe_docker_root() -> bool:
if os.environ.get("INFINITO_RUNNING_ON_GITHUB") != "true":
from .profile import Profile
if not Profile().docker_root_ephemeral():
return False
return os.environ.get("INFINITO_PRESERVE_DOCKER_CACHE") != "true"
+1 -1
View File
@@ -21,7 +21,7 @@ def compose_file_args() -> list[str]:
out = ["-f", "compose.yml"]
if profile.shared_git_dir():
out += ["-f", "compose/worktree.override.yml"]
if profile.registry_cache_active():
if profile.cache_stack_enabled():
out += ["-f", "compose/cache.override.yml"]
if profile.shared_cache_network():
out += ["-f", "compose/cache.shared.override.yml"]
@@ -15,7 +15,7 @@ from typing import TYPE_CHECKING, Any
from cli.administration.deploy.development.common import DEV_INVENTORY_VARS_FILE
from cli.administration.deploy.development.mirrors import (
generate_ci_mirrors_file,
should_use_mirrors_on_ci,
should_use_mirrors,
)
from .payload import _bake_overrides, _resolve_variant_payloads
@@ -66,7 +66,7 @@ def build_dev_inventory(compose: Compose, spec: DevInventorySpec) -> None:
if variant_selectors:
cmd += ["--app-variants", json.dumps(variant_selectors, sort_keys=True)]
if should_use_mirrors_on_ci():
if should_use_mirrors():
mirrors_file = generate_ci_mirrors_file(compose, inventory_dir=inv_root)
cmd += ["--mirror", mirrors_file]
@@ -14,22 +14,11 @@ def _require_env(name: str) -> str:
return str(val).strip()
def _require_bool_env(name: str) -> bool:
raw = _require_env(name)
if raw == "true":
return True
if raw == "false":
return False
raise RuntimeError(
f"Environment variable {name} must be 'true' or 'false', got {raw!r}"
)
def should_use_mirrors() -> bool:
"""True iff image references should be rewritten to the GHCR mirror."""
from .profile import Profile
def should_use_mirrors_on_ci() -> bool:
"""
Mirrors are enabled strictly based on INFINITO_RUNNING_ON_GITHUB.
"""
return _require_bool_env("INFINITO_RUNNING_ON_GITHUB")
return Profile().image_mirror_enabled()
def _exec_env() -> dict[str, str]:
@@ -5,8 +5,18 @@ from __future__ import annotations
import os
def _declared(name: str) -> bool | None:
"""Explicit capability declaration, or None when it is left to the default."""
raw = (os.environ.get(name) or "").strip()
if raw == "true":
return True
if raw == "false":
return False
return None
class Profile:
"""Runtime-context flags consumed by the compose wrapper."""
"""Runtime identity and the capability decisions derived from it."""
def is_ci(self) -> bool:
"""True when any standard CI signal is set."""
@@ -16,9 +26,24 @@ class Profile:
or os.environ.get("CI") == "true"
)
def registry_cache_active(self) -> bool:
"""True iff the cache stack should be loaded (local dev only)."""
return not self.is_ci()
def runs_on_github(self) -> bool:
"""True on a real GitHub runner; act and generic CI are not that."""
return os.environ.get("INFINITO_RUNNING_ON_GITHUB") == "true"
def cache_stack_enabled(self) -> bool:
"""True iff the pull-through cache stack should be loaded."""
declared = _declared("INFINITO_CACHE_STACK")
return (not self.is_ci()) if declared is None else declared
def image_mirror_enabled(self) -> bool:
"""True iff image references should be rewritten to the GHCR mirror."""
declared = _declared("INFINITO_IMAGE_MIRROR")
return self.runs_on_github() if declared is None else declared
def docker_root_ephemeral(self) -> bool:
"""True iff the docker data root may be wiped when the stack goes down."""
declared = _declared("INFINITO_DOCKER_ROOT_EPHEMERAL")
return self.runs_on_github() if declared is None else declared
def instance(self) -> int:
"""Slot index of this checkout; 0 is the primary one.
@@ -40,4 +65,4 @@ class Profile:
def owns_cache_stack(self) -> bool:
"""True iff this instance runs the cache stack rather than sharing it."""
return self.registry_cache_active() and not self.shared_cache_network()
return self.cache_stack_enabled() and not self.shared_cache_network()
+1 -1
View File
@@ -17,7 +17,7 @@ Key facts an agent must hold before acting:
- `RUNNER_GITHUB_OWNER` and `RUNNER_GITHUB_REPO` resolve from `GITHUB_REPOSITORY_OWNER` / `GITHUB_REPOSITORY` env vars when set (CI context), falling back to `meta/services.yml` for production deploys.
- `GH_TOKEN` must be available on the Ansible control node (`delegate_to: localhost`) for the `gh api` registration call to succeed. In CI this is forwarded into the DinD container by `cli/administration/deploy/development/deploy/run.py`.
- The systemd services installed by `svc.sh` require a host with a real init system. Registration, service install, and service start are skipped when `DOCKER_IN_CONTAINER=true`; the role can be fully deployed in DinD (binary extraction and `.env` file work). The nested `deploy-fresh-purged-apps` call in `test.sh` is also skipped when `DOCKER_IN_CONTAINER=true` — in DinD, `make up` does not start the package-cache proxy (`registry_cache_active()` returns false in CI), so apt inside the fresh container cannot resolve the proxy hostname and package installs fail. Structural checks (binary + `.env`) are sufficient proof of a correct deploy in DinD; the full end-to-end deploy only runs on a real host.
- The systemd services installed by `svc.sh` require a host with a real init system. Registration, service install, and service start are skipped when `DOCKER_IN_CONTAINER=true`; the role can be fully deployed in DinD (binary extraction and `.env` file work). The nested `deploy-fresh-purged-apps` call in `test.sh` is also skipped when `DOCKER_IN_CONTAINER=true` — in DinD, `make up` does not start the package-cache proxy (`cache_stack_enabled()` returns false in CI), so apt inside the fresh container cannot resolve the proxy hostname and package installs fail. Structural checks (binary + `.env`) are sufficient proof of a correct deploy in DinD; the full end-to-end deploy only runs on a real host.
- `runner_count` defaults to `ansible_processor_vcpus // runner_cpus` (auto-scaled to hardware). Override explicitly when debugging a specific count.
## The loop
+1 -1
View File
@@ -45,7 +45,7 @@ Cert generation runs in a throw-away alpine container driven by [package-fronten
## Activation 🎚️
The `cache` decision is exposed via `Profile.registry_cache_active()` in [profile.py](../../../cli/administration/deploy/development/profile.py). When active:
The `cache` decision is exposed via `Profile.cache_stack_enabled()` in [profile.py](../../../cli/administration/deploy/development/profile.py). When active:
- [compose/cache.override.yml](../../../compose/cache.override.yml) is layered on top of the base [compose.yml](../../../compose.yml) by [compose.py](../../../cli/administration/deploy/development/compose.py) and [down.py](../../../cli/administration/deploy/development/down.py) via [common.py](../../../cli/administration/deploy/development/common.py)`compose_file_args`.
- The cache services are added.
+3 -2
View File
@@ -9,11 +9,12 @@
# base, compose and swarm.
set -euo pipefail
# Force local runtime context.
unset GITHUB_ACTIONS
unset ACT
unset CI
export INFINITO_RUNNING_ON_GITHUB=false
export INFINITO_CACHE_STACK=true
export INFINITO_IMAGE_MIRROR=true
export INFINITO_DOCKER_ROOT_EPHEMERAL=true
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/tests/workspace/utils/common.sh
@@ -30,7 +30,7 @@ class TestBuildDevInventory(unittest.TestCase):
},
)
@patch(
"cli.administration.deploy.development.inventory.builder.should_use_mirrors_on_ci",
"cli.administration.deploy.development.inventory.builder.should_use_mirrors",
autospec=True,
return_value=False,
)
@@ -69,7 +69,7 @@ class TestBuildDevInventory(unittest.TestCase):
},
)
@patch(
"cli.administration.deploy.development.inventory.builder.should_use_mirrors_on_ci",
"cli.administration.deploy.development.inventory.builder.should_use_mirrors",
autospec=True,
return_value=False,
)
@@ -110,7 +110,7 @@ class TestBuildDevInventory(unittest.TestCase):
return_value={"web-app-keycloak": [{}], "web-app-nextcloud": [{}]},
)
@patch(
"cli.administration.deploy.development.inventory.builder.should_use_mirrors_on_ci",
"cli.administration.deploy.development.inventory.builder.should_use_mirrors",
autospec=True,
return_value=True,
)
@@ -135,7 +135,7 @@ class TestBuildDevInventory(unittest.TestCase):
return_value={"web-app-keycloak": [{}], "web-app-nextcloud": [{}]},
)
@patch(
"cli.administration.deploy.development.inventory.builder.should_use_mirrors_on_ci",
"cli.administration.deploy.development.inventory.builder.should_use_mirrors",
autospec=True,
return_value=False,
)
@@ -160,7 +160,7 @@ class TestBuildDevInventory(unittest.TestCase):
return_value={"web-app-keycloak": [{}], "web-app-nextcloud": [{}]},
)
@patch(
"cli.administration.deploy.development.inventory.builder.should_use_mirrors_on_ci",
"cli.administration.deploy.development.inventory.builder.should_use_mirrors",
autospec=True,
return_value=False,
)
@@ -182,7 +182,7 @@ class TestBuildDevInventory(unittest.TestCase):
},
)
@patch(
"cli.administration.deploy.development.inventory.builder.should_use_mirrors_on_ci",
"cli.administration.deploy.development.inventory.builder.should_use_mirrors",
autospec=True,
return_value=False,
)
@@ -211,7 +211,7 @@ class TestBuildDevInventory(unittest.TestCase):
return_value={"web-app-keycloak": [{}], "web-app-nextcloud": [{}]},
)
@patch(
"cli.administration.deploy.development.inventory.builder.should_use_mirrors_on_ci",
"cli.administration.deploy.development.inventory.builder.should_use_mirrors",
autospec=True,
return_value=False,
)
@@ -230,7 +230,7 @@ class TestBuildDevInventory(unittest.TestCase):
return_value={"web-app-keycloak": [{}], "web-app-nextcloud": [{}]},
)
@patch(
"cli.administration.deploy.development.inventory.builder.should_use_mirrors_on_ci",
"cli.administration.deploy.development.inventory.builder.should_use_mirrors",
autospec=True,
return_value=False,
)
@@ -54,25 +54,73 @@ class TestProfileIsCI(unittest.TestCase):
self.assertFalse(Profile().is_ci())
class TestProfileRegistryCacheActive(unittest.TestCase):
class TestProfileCacheStackEnabled(unittest.TestCase):
@patch.dict(os.environ, _BLANK_CI_ENV, clear=False)
def test_active_locally_when_no_ci_signal_set(self) -> None:
self.assertTrue(Profile().registry_cache_active())
self.assertTrue(Profile().cache_stack_enabled())
@patch.dict(os.environ, {**_BLANK_CI_ENV, "GITHUB_ACTIONS": "true"}, clear=False)
def test_inactive_under_github_actions(self) -> None:
self.assertFalse(Profile().registry_cache_active())
self.assertFalse(Profile().cache_stack_enabled())
@patch.dict(os.environ, {**_BLANK_CI_ENV, "CI": "true"}, clear=False)
def test_inactive_under_generic_ci_signal(self) -> None:
self.assertFalse(Profile().registry_cache_active())
self.assertFalse(Profile().cache_stack_enabled())
@patch.dict(
os.environ, {**_BLANK_CI_ENV, "INFINITO_RUNNING_ON_GITHUB": "true"}, clear=False
)
def test_is_strict_inverse_of_is_ci(self) -> None:
def test_defaults_to_the_inverse_of_is_ci(self) -> None:
p = Profile()
self.assertEqual(p.registry_cache_active(), not p.is_ci())
self.assertEqual(p.cache_stack_enabled(), not p.is_ci())
_ON_CI = {**_BLANK_CI_ENV, "GITHUB_ACTIONS": "true"}
class TestProfileDeclaredCapabilities(unittest.TestCase):
@patch.dict(os.environ, {**_ON_CI, "INFINITO_CACHE_STACK": "true"}, clear=False)
def test_declaration_wins_over_the_ci_default(self) -> None:
p = Profile()
self.assertTrue(p.is_ci())
self.assertTrue(p.cache_stack_enabled())
@patch.dict(os.environ, {**_ON_CI, "INFINITO_IMAGE_MIRROR": "false"}, clear=False)
def test_mirror_can_be_declared_off_on_ci(self) -> None:
self.assertFalse(Profile().image_mirror_enabled())
@patch.dict(
os.environ,
{**_BLANK_CI_ENV, "INFINITO_DOCKER_ROOT_EPHEMERAL": "true"},
clear=False,
)
def test_docker_root_can_be_declared_ephemeral_off_ci(self) -> None:
self.assertTrue(Profile().docker_root_ephemeral())
@patch.dict(os.environ, {**_ON_CI, "INFINITO_CACHE_STACK": "yes"}, clear=False)
def test_a_non_boolean_declaration_falls_back_to_the_default(self) -> None:
self.assertFalse(Profile().cache_stack_enabled())
@patch.dict(os.environ, _ON_CI, clear=False)
def test_act_gets_no_mirrors_and_keeps_its_docker_root(self) -> None:
p = Profile()
self.assertTrue(p.is_ci())
self.assertFalse(p.runs_on_github())
self.assertFalse(p.cache_stack_enabled())
self.assertFalse(p.image_mirror_enabled())
self.assertFalse(p.docker_root_ephemeral())
@patch.dict(
os.environ,
{**_BLANK_CI_ENV, "INFINITO_RUNNING_ON_GITHUB": "true"},
clear=False,
)
def test_a_real_github_runner_mirrors_and_wipes(self) -> None:
p = Profile()
self.assertTrue(p.runs_on_github())
self.assertFalse(p.cache_stack_enabled())
self.assertTrue(p.image_mirror_enabled())
self.assertTrue(p.docker_root_ephemeral())
class TestProfileInstance(unittest.TestCase):
@@ -104,7 +152,7 @@ class TestProfileOwnsCacheStack(unittest.TestCase):
def test_shares_instead_of_owning_when_a_network_is_named(self) -> None:
p = Profile()
self.assertFalse(p.owns_cache_stack())
self.assertTrue(p.registry_cache_active())
self.assertTrue(p.cache_stack_enabled())
@patch.dict(os.environ, {**_BLANK_CI_ENV, "INFINITO_INSTANCE": "7"}, clear=False)
def test_a_stray_slot_alone_does_not_give_the_stack_away(self) -> None: