Deploy-time placeholders were spread across four independent
implementations: sed in the Gitea deploy script, ansible.builtin.replace
in two playbooks, a hardcoded suffix in bin/sls_hla_launch_gui.sh, and
another in agebd/pv.py. Each defined its own values, so nothing kept them
in sync and a missed substitution failed silently.
Step 1 -- one source of truth:
* config/environments.yml declares every deploy-time token, its value
per environment, and where tokens are allowed to live. An environment
that is missing a token, or defines an undeclared one, is a load-time
error.
* agebd.subs loads it and renders. Rendering is a strict regex over
declared token names rather than a template engine: the rendered
files are EPICS db/substitution files and Qt XML, and an unknown
token must raise rather than survive into the output.
* scripts/ci/check_subs.py enforces the contract in CI: tokens in a
rendered path are declared, declared tokens only appear where they
are rendered or generated, and no pre-migration delimiters survive.
Step 2 -- separate the two substitution stages by delimiter:
* Creation-time (copier, once per new service) keeps {{ token }}.
Deploy-time (every deploy, per environment) becomes @@token@@.
* This drops the {{ '{{ token }}' }} escaping from templates/, which
existed only because one Jinja pass had to emit syntax for a second.
* Angle brackets were tried first and rejected: << >> is not
well-formed XML, and cli parses qt/*.ui with ElementTree.
* The existing sed and ansible consumers are repointed at the new
delimiters so this stays behaviour-preserving. Routing them through
agebd.subs is step 3.
Also fixes, as a side effect of having one implementation:
* The sed pattern '{{ *agebd_env_suffix_upper* }}' quantified 'r'
rather than the space, so it only matched one exact spacing.
* agebd.subs.read_ioc_port compares service names case-insensitively;
ansible/tasks/utils/get-ioc-port.yml still compares CamelCase
registry names against a lowercase input and never matches.
qt/*.ui is declared as a render path but nothing renders it yet -- the
Qt deploy step is still a plain rsync. Wiring that up is step 4.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
74 lines
1.9 KiB
Python
74 lines
1.9 KiB
Python
import pytest
|
|
from agebd.subs import SubstitutionsConfig
|
|
from agebd.utils import get_git_root
|
|
|
|
from ci.check_subs import (
|
|
check_declared,
|
|
check_location,
|
|
check_no_legacy_delimiters,
|
|
check_subs,
|
|
)
|
|
|
|
REPO_ROOT = get_git_root(__file__)
|
|
|
|
|
|
@pytest.fixture
|
|
def config():
|
|
return SubstitutionsConfig.load(REPO_ROOT / "config")
|
|
|
|
|
|
def test_repo_satisfies_the_substitution_contract():
|
|
check_subs()
|
|
|
|
|
|
def test_undeclared_token_in_a_render_path_is_reported(config):
|
|
errors = check_declared(config, "qt/A_BD_Tune.ui", "AGEBD-TUNE@@agebd_env_sufix_upper@@:AO")
|
|
|
|
assert len(errors) == 1
|
|
assert "agebd_env_sufix_upper" in errors[0]
|
|
|
|
|
|
def test_declared_token_in_a_render_path_is_accepted(config):
|
|
assert check_declared(config, "qt/A_BD_Tune.ui", "@@agebd_env_suffix_upper@@") == []
|
|
|
|
|
|
def test_token_outside_a_rendered_path_is_reported(config):
|
|
errors = check_location(config, "bin/start_service.sh", "SUFFIX=@@agebd_env_suffix_upper@@")
|
|
|
|
assert len(errors) == 1
|
|
assert "never rendered" in errors[0]
|
|
|
|
|
|
def test_token_in_a_render_path_is_accepted(config):
|
|
assert check_location(config, "qt/A_BD_Tune.ui", "@@agebd_env_suffix_upper@@") == []
|
|
|
|
|
|
def test_token_in_a_passthrough_path_is_accepted(config):
|
|
assert (
|
|
check_location(
|
|
config,
|
|
"templates/service/{{service_name_lower}}/current/ioc/x.jinja",
|
|
"@@agebd_env_suffix_upper@@",
|
|
)
|
|
== []
|
|
)
|
|
|
|
|
|
def test_legacy_delimiters_are_reported(config):
|
|
errors = check_no_legacy_delimiters(
|
|
config, "qt/A_BD_Tune.ui", "AGEBD-TUNE{{ agebd_env_suffix_upper }}:AO"
|
|
)
|
|
|
|
assert len(errors) == 1
|
|
assert "@@agebd_env_suffix_upper@@" in errors[0]
|
|
|
|
|
|
def test_creation_time_tokens_are_left_alone(config):
|
|
"""{{ service_name_upper }} is copier's, not ours."""
|
|
assert (
|
|
check_no_legacy_delimiters(
|
|
config, "templates/service/x/current/ioc/y.jinja", "{{ service_name_upper }}"
|
|
)
|
|
== []
|
|
)
|