feat: the two-level product-root layout, and a schema-driven dev environment - #25
Merged
Merged
Conversation
Make the layout host-agent adopted in #23 the pattern every repo scaffolded from this template inherits: the repository root holds what acts on the product, one directory named after the repo holds what it is built from. - LAYOUT.md: the rule, the placement test, invariants, ecosystem adapters, the integration-compatibility gate, adoption steps, and the evidence (release-please#1724, flox#3997, cryptography#11836, zksync-era#3456). - .repo/layout.toml declares the product (empty here: the template ships none). - New policies: `layout` (LAYOUT-01..11: no product content at the root; mounts, Dependabot and PRODUCT_DIR agree with the declaration; the env contract lives at <product>/env.schema.yaml, never under config/), `paths` (PATH-01..04, the generic half of host-agent's check-path-refs.py), `env` (ENV-01, the shared env.schema.yaml shape), and CFG-09 (a caller's .config/ path must exist). - A pytest suite, run in CI's governance job: most layout rules only fire once a product is declared, which the template never does. Fixes surfaced along the way: the two .gitattributes lines matching no tracked file; the governance hook's glob skipping .repo/**; `repo:install` reusing uv's cached build so policy edits never took effect; the comments policy and codespell scanning .repo/.venv. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replace the hand-written .env.example with .devcontainer/env.schema.yaml, the same shape products use for their runtime contract. The rendering stays, because `docker run --env-file` and Compose read dotenv and the host-side hook has only bash -- so it becomes a generated artifact that CI checks, not a file anyone edits. What the schema buys beyond documentation: - Requiredness follows the enabled COMPOSE_PROFILES: `consumers:` names the stacks that read a binding, and a value is only demanded while one of them is running. - A missing value never blocks the container. startup.sh starts every stack that can start and skips the rest, naming them; the MOTD repeats it. - `task env:setup` asks which stacks to run first, then only for what that answer makes necessary -- masked secrets, menus for fixed choices, the binding's own description as the prompt. - `source: host` fills from the host environment (a bash-readable `# @host` marker) and mirrors into devcontainer.json `secrets`, so Codespaces prompts. - `mirrored_in:` makes the Tempo/Loki credential duplication a check (ENV-05) rather than a note in the docs. - The shell profile re-loads .env (lib/env-load.sh, which parses rather than sources), so an edited value reaches new terminals without a rebuild, and startup.sh exports COMPOSE_PROFILES from the file so the stale copy `runArgs --env-file` froze at `docker run` cannot win. ENV-02..07 check the rendering, compose parity in both directions, the mirrored literals, the secrets block, and that no secret carries a committed default. That retires .devcontainer/scripts/lib/env-check.sh and the CI job that duplicated it, with its ruleset entry and hooks allowlist. The generated .env.example parses to exactly the keys and values the previous hand-written one had, so no existing .env changes meaning. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Feature's default distro ("ms", Microsoft OpenJDK) is gone from SDKMAN's
index -- `sdk list java` now returns no -ms builds at all -- so the pinned
version 17 resolves to nothing and the image build fails with:
JDK version 17 is available in ms...
Version 17 not found. Available versions:
ERROR: Feature "Java (via SDKMAN!)" failed to install!
Unrelated to the layout work: this is the first CI run on the repo since
2026-08-19, so an upstream change surfaced here. Temurin (Eclipse Adoptium)
carries 17 and is the conventional default elsewhere.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
Author
|
The two container-build jobs failed on something this PR did not touch: the Java Feature's default distro ( |
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.
Two halves of one idea: a repository has two levels, and each level declares the environment it reads.
1. The layout pattern
LAYOUT.mdstates the rule host-agent adopted in #23: the repository root holds what acts on the product; one directory named after the repo holds what it is built from. It covers the placement test, the invariants and which code enforces each, the per-ecosystem adapters (Rust proven, the rest marked "verify on first adoption"), the compatibility gate for tools that assume the root, adoption steps, and an Evidence section — every citation re-opened and confirmed..repo/layout.tomldeclares the product. The template shipsproduct = "", so product-dependent rules skip while the rule that the root stays free of product content still applies.New policies, all inside
repo check:layout(LAYOUT-01..11)env.schema.yaml; devcontainer mounts, editor links, Dependabot andPRODUCT_DIRagree with the declarationpaths(PATH-01..04).gitattributespattern, paths-filter,working-directory, Dependabot directory and Taskfile path var still names something — the generic half of host-agent'scheck-path-refs.pyenv(ENV-01..07)config(new CFG-09).config/path a caller names must exist (CFG-04 only checked the other direction)The product env contract lives at
<product>/env.schema.yaml, not underconfig/. LAYOUT-11 fails the old placement and its fix names the root location.2. The dev environment reads a schema too
.devcontainer/env.schema.yamlreplaces the hand-written.env.example, which becomes a generated artifact (task env:render, ENV-02 fails on drift). The rendering has to exist:docker run --env-fileand Compose read dotenv, andinitializeCommandruns on the host where only bash is guaranteed. No standard removes that step — varlock/@env-spec, dotenvx, Compose and the devcontainer spec were all checked, and platform keeps a hand-written file for the same reason.What the schema buys beyond documentation:
consumers:names the stacks that read a binding, so a value is only demanded while one of them runs.startup.shstarts every stack that can and skips the rest by name; the MOTD repeats it;task env:setupasks which stacks you want first, then only for what that answer makes necessary — masked secrets, menus for fixed choices, the binding's description as the prompt.source: hostfills from the host environment through a bash-readable# @hostmarker, and mirrors into devcontainer.jsonsecretsso Codespaces prompts (ENV-06).mirrored_in:turns the Tempo/Loki credential duplication from a note in the docs into a check (ENV-05)..env(lib/env-load.shparses rather than sources, because dotenv is not shell), andstartup.shexportsCOMPOSE_PROFILESfrom the file so the copyrunArgs --env-filefroze atdocker runcannot win.This retires
scripts/lib/env-check.shand the CI job that duplicated it, with its ruleset entry and hooks allowlist. The generated.env.exampleparses to exactly the keys and values the old one had, so no existing.envchanges meaning.Template bugs this surfaced
.gitattributeslines matched no tracked file.governancejob's glob skipped.repo/**, so editing a policy never re-ran the checks.task repo:installreused uv's cached build, so an edited policy never took effect..repo/.venv.COMPOSE_PROFILES=was flagged as an unfilled required key on every boot.Verification
repo check: every policy green. 90 pytest cases (.repo/tests/, new CI step) — most layout rules only fire once a product is declared, which the template never does.Cargo.toml, a declared-but-missing product, a stray schema, the.envgitattributes line, a mistyped config path and a renamed LAYOUT.md heading each fail with the expected code.doctorblocks only redis while minio still starts,setup's menu enables a profile and then asks for what it made necessary, and the loader exports a value containing spaces correctly.docker compose configwith every profile enabled.layout,pathsandenvall pass after only renamingWORKSPACE_DIRtoPRODUCT_DIR. That run caught a false positive in LAYOUT-08 (host-agent's release scripts legitimately carry their own uv project), now narrowed to updates scanning/.Follow-ups
layout.toml+PRODUCT_DIR, then retires the generic half ofcheck-path-refs.py.repo envto platform, whose devcontainer.env.exampleis still hand-written.🤖 Generated with Claude Code