Skip to content

0015. Inject the image version at scaffold time

Status

Accepted

Context

setup-project.sh scaffolds a consuming project by copying the examples/ templates verbatim. examples/devcontainer.json carries an exact image pin (felipecoelho90/dev-container:X.Y.Z). Because the copy is byte-for-byte, every scaffolded project inherited whatever tag happened to be in the template.

That tag only advanced when the weekly Renovate run fired after a release published (the dev-container-image group). Between a release and that run — and whenever Renovate missed — a freshly scaffolded project pointed at a stale image. In practice the template had drifted as far back as 2.0.0 while the repo shipped 2.3.x (see CHANGELOG.md).

Two fixes were considered:

  1. Floating major tag — set the template to felipecoelho90/dev-container:2. Can never go stale and matches the recommended consumer pin documented in docs/reference/cicd.md, AGENTS.md, and ADR 0010. But it gives up an exact, Renovate-pinnable version by default.
  2. Inject the exact version at scaffold time — substitute the current release into the emitted devcontainer.json.

Decision

Inject the exact release version at scaffold time (option 2).

  • A shared helper, scripts/lib-version.sh, resolves the version from the dev-container clone: the newest v* git tag, falling back to the top released entry in CHANGELOG.md. It is sourced by both scaffolding scripts so the logic cannot drift between them.
  • setup-project.sh substitutes the resolved version into the image line as it writes .devcontainer/devcontainer.json. If resolution fails, it falls back to the template's literal pin (so it can never emit a broken tag).
  • The template keeps an exact pin as that fallback; Renovate's dev-container-image group keeps it current. It is not hand-bumped.
  • After scaffolding, the image tag is owned by the consumer's own Renovate. Therefore update-project.sh must never rewrite it: it renders the template for comparison with the consumer's existing tag substituted in, so a version-only difference is not treated as drift and the pin is never downgraded to the (possibly older) version in the dev-container clone. Only non-version template changes are synced.

Consequences

Accepted: - A newly scaffolded project pins the exact current release, with no dependence on Renovate's cadence and no staleness window. - One shared resolver; no duplicated version logic across the two scripts.

Trade-offs / known limits: - Freshness is only as good as the user's dev-container clone: a months-old checkout injects a months-old (but valid) exact pin. This is acceptable because the consumer's own Renovate advances it afterward, and update-project.sh never downgrades it. - Chose an exact pin over the floating :2 tag deliberately: consumers get reproducibility by default and can still opt into :2 manually.