feat(deploy): inject WATCHTOWER_* variables directly via a compose override (ADR-0012) - #22
Merged
Merged
Conversation
…erride (ADR-0012)
The deploy engine now generates a compose override file and merges it as a
second -f into pull/up, placing the reserved variables straight into service
environment: — the repo's compose file no longer needs passthrough lines,
killing the silent-401 interpolation footgun.
- Policy (engine-neutral, ADR-0010 seam discipline): WATCHTOWER_STACK_ID to
all services; WATCHTOWER_URL to all when PublicBaseUrl is set; the token
scoped — watchtower.inject-token label ('true' opts in, 'false' opts out,
lenient parsing), else the tenant template's target service, else the sole
service of a single-service stack; multi-service plain stacks get nothing
by default.
- Mechanics (Docker-private): service/label discovery via docker compose
config --format json with the deploy's own args; override YAML rendered
with escaping for compose's own interpolation ($ -> $$); sibling temp
file, 0600, deleted on every exit path; deploy output names which
variables went to which services — values never logged, and the plan/
config records carry redacting ToString() overrides.
- Classic passthrough keeps working unchanged (override merges last, wins
per key; verified for both map- and list-form environment and env_file
precedence, recorded in the docs).
- Test seams reworked: ComposeCliService verbs route through protected
RunAsync/RunCapturedAsync, so every verb's argument assembly executes
under test (override forwarding mutation-verified); teardown stubs now
throw on any verb but down.
- ADR-0012 + rewritten injection docs (who-gets-what table, label, honest
exposure/threat-model paragraphs, profiles caveat).
swimmesberger
force-pushed
the
wt/env-injection
branch
from
August 10, 2026 13:16
51ef43d to
2814563
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #21 (which stacks on #19) — merge in order; base is the tenant-switcher branch so the diff stays scoped.
What
The deploy engine now generates a compose override file and merges it as a second
-fintopull/up, placing the reservedWATCHTOWER_*variables directly into serviceenvironment:. Template authors need zero compose changes for the App API / tenant switcher — the silent-401 interpolation footgun (--env-filevars interpolate but never enter containers unless passed through) is gone. Classic passthrough keeps working identically.Who gets what
WATCHTOWER_STACK_ID— every service, always.WATCHTOWER_URL— every service, whenPublicBaseUrlis set.WATCHTOWER_APP_TOKEN— scoped, it's a credential: services labeledwatchtower.inject-token: "true"("false"opts out, parsing lenient); absent labels → the tenant template's target service, or the sole service of a single-service stack; multi-service plain stacks get nothing by default.The policy is an engine-neutral component per ADR-0010's dual-engine rule (the future Kube engine applies the same plan as pod env); only the override rendering is Docker-private.
Reviewer notes
ToString(); override is a sibling temp file,0600, deleted on every exit path;$→$$escaping because compose interpolates the override too. ADR-0012 is explicit that the exposure bound is on Watchtower's defaults — the repo author controls service names/labels and can direct the token, which is in-threat-model (the repo is the code the operator chose, and already got the token under passthrough).environment:andenv_fileprecedence (override wins per key) — recorded in the docs.ComposeCliServiceverbs route through protectedRunAsync/RunCapturedAsync, so every verb's argument assembly executes under test — the review caught that override forwarding was previously untestable and the fix is mutation-verified. Teardown stubs now throw on any verb butdown, so no test can shell out to a real Docker CLI.profiles:may flip the single-service default across compose versions — the label is the escape hatch.Verification
dotnet buildRelease 0 warnings · 415 tests green (284 + 131) · norpc-schema.jsondrift ·npm run buildclean.