Skip to content

feat(anvil): containerized execution - simplified - #83

Open
martinhavelka (wukchung) wants to merge 87 commits into
mainfrom
u/mhavelka/anvil-container-minimal
Open

feat(anvil): containerized execution - simplified#83
martinhavelka (wukchung) wants to merge 87 commits into
mainfrom
u/mhavelka/anvil-container-minimal

Conversation

@wukchung

@wukchung martinhavelka (wukchung) commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Impact

Any anvil-managed repository can run its checks in a pinned Linux image with one command, on Windows or Linux, with no image to publish and nothing to configure. This replaces the 0.4.0 container backend, which shipped nine generated files into every consumer repository and could not build its own image.

For a repository that does not use containers, nothing changes: three artifacts are emitted, and no recipe behaves differently.

Architecture

Three generated artifacts, plus an optional hook.

Artifact Role
justfiles/anvil/container.just Drives the engine, computes the image identity
.anvil/container/Dockerfile Defines the image. Composed: anvil owns five regions, your content goes in the gaps
.anvil/container/Dockerfile.dockerignore Scopes the build context
.anvil/container/hooks.ps1 Optional, not emitted by default. Credentials and image resolution

The image and the checks cannot disagree about the toolset. The image installs its tools by running just anvil-setup — the same recipe the checks use, from the same pins — so there is no second tool list to drift.

The Dockerfile is composed, not owned. An owned file that invites in-place edits freezes the base digest and four tool pins the moment a repository edits it, while the tag keeps resolving — because it hashes their file. The identity scheme works perfectly and still names a stale image. Anvil owns five regions (base-image, base, tools, setup, entry) and the four gaps between them are yours, each defined by what must already be true at that point in the build: a second ARG BASE_IMAGE before FROM, a root CA before the first download, a compile dependency before anvil-setup, a run-time tool after it. Regions do not make anvil's content unwritable — an edit inside one is still preserved, as everywhere else in the tool — they remove the reason to edit it.

Two constraints the region engine grew for this: # syntax=docker/dockerfile:1 is honoured only as line 1 and a sentinel is a comment, so it is seeded as a scaffold outside the regions; and region order is semantic, so ordering is enforced rather than assumed.

A host anvil cannot compose safely is refused, not guessed at. upsert_region appends a missing region at end-of-file, which is right only when the file is already the composed shape — so four states are rejected with a diagnostic and the file left byte-identical: a previous render the repository has since edited, a Dockerfile anvil has never owned, one carrying some regions but not all, and one whose regions are out of order. Refusal means the lock too: the recorded checksum is the provenance the next run reclassifies from, and reverting the edit is the clean recovery, which only exists while that entry survives. Classification happens once per host from its on-disk state, so a refusal is atomic rather than landing a write first.

The image tag is a digest of its inputs: everything under .anvil/container/, rust-toolchain.toml, and every file under justfiles/anvil/ — which is exactly what the build context copies. The container directory is walked rather than named file by file, so anything a gap COPYs is an input too. The whole recipe tree counts because just anvil-setup reaches the install recipes through the tier, group and check recipes, so the routing decides whether a tool is installed just as surely as tools.just decides how. A changed input names a tag that cannot exist yet, so a build follows, and an image that is present is current by construction. The cost is that editing any recipe rebuilds the image; a tag that could name contents the image does not have would make every guarantee here meaningless.

The image supplies the toolchain, never the code. Recipes always execute from the mount; the copy baked into the image exists only to install tools at build time.

Design: docs/design/containers.md.

Usage

just anvil-container just anvil-pr    # a tier
just anvil-container cargo build      # any other command
just anvil-container                  # interactive shell
just anvil-container-status           # engine, image, whether it is current

The arguments are the argv executed inside the image, so anvil recipes are reached by naming just. The first run builds the image; later runs start immediately. just anvil-pr still runs natively — the container is opt-in per invocation, never transparent routing.

Requires Docker or Podman (ANVIL_CONTAINER_ENGINE=podman). On Windows, an engine installed only inside WSL is reached through wsl.exe automatically, so Docker Desktop is not required.

A fork supplies an internal image and credentials by replacing the base and tool regions and adding hooks.ps1, which may define Anvil-BuildSecrets (build secrets), Anvil-RunEnv (run-time environment) and Anvil-ResolveImage (pull a published image). It inherits the catalog install and the entry contract, so a different base OS is two region replacements rather than a forked 112-line file. Credentials are passed by variable name in both phases, so a value never reaches a process argument or an image layer, and an empty value fails closed.

Validation

  • cargo test -p cargo-anvil --no-fail-fast447 passed, 0 failed
  • scripts/test-anvil-container.ps1 -Engine docker71/71 in 9m30s, black-box against a real daemon: artifacts, build, reuse, tag changes and reverts, content in a Dockerfile gap surviving regeneration, an edit inside a region being preserved, a non-just command running in the image, hook secrets reaching the build but not a layer, empty secrets failing closed
  • cargo anvil --dry-runexit 0, 87 items unchanged, so the regenerate gate stays green
  • cargo +nightly fmt -- --check, cargo clippy --workspace --all-targets --all-features -- -D warnings, just anvil-spellcheck — clean

Breaking

ANVIL_RUNNER, the anvil-runner managed region, justfiles/anvil/runner.just and the 0.4.0 .anvil/container/ asset set are gone. A repository that adopted 0.4.0 regenerates and uses just anvil-container just <recipe>. Nothing consumes that backend today.

A repository on the previous layout, where .anvil/container/Dockerfile was an owned file, is re-seeded into the composed form on the next run: a file tracked as owned in the lock and carrying none of the regions is a previous render, not composition. A Dockerfile the repository wrote itself is refused and left untouched, because there is no position for the regions that would not put its content above FROM; the refusal names the file and the recovery.

artifacts::container::dockerfile() never reached main — it was introduced and then replaced within this branch — so this is not a break against the released API. It matters to work already built on an earlier commit of this branch: the single constructor is now dockerfile_base_image(), dockerfile_base(), dockerfile_tools(), dockerfile_setup() and dockerfile_entry(), and a catalog replacing the whole file moves to replacing the regions it actually cares about.

Version and CHANGELOG.md are untouched: releasing is a separate PR via scripts/release-crate.ps1.

The 0.4.0 backend shipped nine generated files -- two shell drivers, two
image-id helpers, an entrypoint, a README -- plus a tier-routing seam
(runner.just, the anvil-runner managed region) and roughly 1200 lines of
duplicated .sh/.ps1. Customizing it meant owning that surface.

Containerized execution is now two artifacts and one optional hook:

- .anvil/container/Dockerfile (with its build-context ignore file) defines
  what the image contains. It installs tools by running just anvil-setup,
  so there is no second tool list to keep in step. A repository edits it in
  place; a downstream catalog replaces it via replace_artifact.
- justfiles/anvil/container.just drives the engine. The image tag *is* a
  SHA-256 over the Dockerfile, its ignore file, rust-toolchain.toml, the
  optional hook and the generated recipe tree, so presence implies freshness
  and there is no staleness bookkeeping.
- .anvil/container/hooks.ps1, when present, supplies credentials.
  Anvil-PreBuild returns BuildKit secrets, Anvil-PreRun returns run-time
  environment; both are passed by variable name so a value never reaches a
  process argument or an image layer, and an empty value fails closed.

There is no configuration file: whether the group is emitted is a catalog
decision, and the only host-specific value -- which engine to call -- is the
ANVIL_CONTAINER_ENGINE variable read at run time. There is no transparent
tier routing either: just anvil-pr runs natively and the container is
reached only through just anvil-container <recipe>.

aprz.just drops its container-specific token plumbing; inside the image a
credential now arrives as an ordinary environment variable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Docker installed inside WSL without Docker Desktop is a documented, common
Windows setup -- it is the one this repository's own docs describe -- and it
leaves no Windows CLI behind. Invoking the engine directly therefore made the
feature unreachable on exactly the configuration we tell people to build.

On Windows only, and only when the engine is absent from PATH, the recipe now
routes through the default WSL distribution and translates the repository root
with wslpath. Paths cross that boundary with forward slashes, since the
intervening shell would otherwise eat the separators and hand wslpath a mangled
path. Build secrets and forwarded run-time values are exported through WSLENV,
which is where the engine reads them from when it runs there. Docker Desktop
and Podman ship a Windows CLI, are found on PATH, and never take this path.

scripts/test-anvil-container.ps1 is a black-box walk through the feature from a
user's seat: it creates a repository, generates into it, and then only does what
a developer would do. Its setup is held to that standard deliberately -- if it
had to hand-write a generated file or work around a defect, that would be a bug
in the product rather than something the script should absorb.

43 checks, against a real Docker daemon: the emitted artifacts, a first run that
builds, a second that reuses, a toolchain bump that renames the tag and a revert
that restores it, a Dockerfile edit that survives regeneration, a hook whose
secret reaches the build without reaching a layer and whose value reaches a
recipe at run time, an empty secret that fails closed, and a nested invocation
that stays native.

The design doc is rewritten against the narrowed contract.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…r engine

The customization story was stated three times -- in containers.md, in
extensibility.md 6.1, and in the crate docs that generate the README. That
happened because main already carried a container subsection in extensibility.md
for the old backend, and it was rewritten in place rather than reconsidered.
Three copies of the same contract drift, and the one that drifts is whichever
the reader happens to find.

containers.md now owns it: the levers, who each one is for, and the coupling
between the Dockerfile and its ignore file, including what a fork inherits
rather than replaces.

extensibility.md 6.1 keeps only what is genuinely a rule of the extensibility
system -- justfiles/ holds .just recipes and nothing else, enforced by
CatalogBuilder::build -- and explains that containerized execution is why. The
customization detail is a pointer. The crate docs keep a sentence, since a
README reader needs "you can change the image", not the fork API.

Host setup gains a subsection per engine. Docker and podman need different
things on Windows, and the old prose buried that: Docker Desktop and podman
both ship a Windows CLI, while Docker-in-WSL does not and is reached through the
WSL fallback. Each path is now written out, and podman's status is stated
plainly -- wired up, expected to work, verified by nothing, with the BuildKit
assumptions in the credential path called out as the specific risk.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

⚠️ Breaking Changes Detected


--- failure function_missing: pub fn removed or renamed ---

Description:
A publicly-visible function cannot be imported by its prior path. A `pub use` may have been removed, or the function itself may have been renamed or removed entirely.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.49.0/src/lints/function_missing.ron

Failed in:
  function cargo_anvil::artifacts::container::image_id, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-origin_main/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/container.rs:76
  function cargo_anvil::artifacts::container::powershell_driver, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-origin_main/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/container.rs:94
  function cargo_anvil::artifacts::container::ignore_file, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-origin_main/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/container.rs:64
  function cargo_anvil::artifacts::justfile::runner, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-origin_main/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/justfile.rs:225
  function cargo_anvil::artifacts::container::shell_image_id, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-origin_main/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/container.rs:82
  function cargo_anvil::artifacts::container::readme, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-origin_main/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/container.rs:100
  function cargo_anvil::artifacts::container::customize_powershell, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-origin_main/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/container.rs:122
  function cargo_anvil::artifacts::container::entrypoint, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-origin_main/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/container.rs:70
  function cargo_anvil::artifacts::container::shell_driver, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-origin_main/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/container.rs:88
  function cargo_anvil::artifacts::region::justfile_runner, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-origin_main/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/region.rs:146
  function cargo_anvil::artifacts::container::customize_shell, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-origin_main/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/container.rs:113
  function cargo_anvil::artifacts::container::containerfile, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-origin_main/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/container.rs:58

If the breaking changes are intentional then everything is fine - this message is merely informative.

Remember to apply a version number bump with the correct severity when publishing a version with breaking changes (1.x.x -> 2.x.x or 0.1.x -> 0.2.x).

…onestly

Three fixes found by CI and by running the e2e against podman.

dependency_recipe_sources became dead when the old container tests were
removed. CI builds with -D warnings, so dead_code is a hard error there and a
silent warning locally; the function had no remaining caller and is deleted.

"natively" was missing from the dictionary, which is the whole of the
spell-check failure. cargo-spellcheck cannot run on this machine (it needs
libclang), so the word was found by CI rather than locally.

Running the e2e with -Engine podman surfaced a genuine engine defect: podman
6.0.2 on Windows cannot mount a build secret at all. It composes its own temp
path from the build context after translating it into its machine's view, then
joins it with a Windows separator, and fails before the build starts. A
four-line Dockerfile reproduces it with no anvil involved, and `src=` fails
identically to `env=`, so there is nothing to work around on our side.

The secret plumbing therefore stays as it was -- by environment variable name,
so the value never touches disk. The build failure path gains one hint,
conditioned on "secrets were passed and the build failed" rather than on the
engine, because that error names neither the secret nor the engine.

The e2e now skips the three hook sections on podman-for-Windows with the reason
stated, rather than reporting a known engine limitation as failure every run,
and its image cleanup matches anywhere in a reference: podman reports images
fully qualified (localhost/anvil-...) where docker does not, so the old prefix
match left images behind and the next run saw a warm cache it expected to be
cold.

Validation: docker 43/43, podman 31/31 (3 hook sections skipped),
cargo test -p cargo-anvil --all-features under -D warnings, --dry-run clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@codecov-commenter

Codecov Comments Bot (codecov-commenter) commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.7%. Comparing base (95089c5) to head (539f290).

❌ Your project status has failed because the head coverage (97.7%) is below the target coverage (100.0%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files
@@          Coverage Diff           @@
##            main     #83    +/-   ##
======================================
  Coverage   97.7%   97.7%            
======================================
  Files        286     286            
  Lines      62174   62333   +159     
======================================
+ Hits       60751   60911   +160     
+ Misses      1423    1422     -1     
Flag Coverage Δ
linux 97.6% <100.0%> (?)
linux-arm 97.6% <100.0%> (?)
windows 97.9% <100.0%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

martinhavelka (wukchung) and others added 13 commits August 13, 2026 11:34
…e tag

Section 9 of the e2e was headed "hook output does not change the tag" and did
not test it. One assertion rewrote the hook to byte-identical contents, which
only shows the hash is deterministic; the other changed the hook body and
asserted the tag *did* move, directly contradicting the comment above it. Both
were really re-testing that file content is hashed, which section 7 already
covers, so the invariant the design leans on -- a credential must never
influence a tag -- was unverified by a test that claimed to cover it.

Proving it needs the hook file to be byte-identical while what it returns
differs, so the fixture hook now reads its value from the environment and the
reference is resolved twice with two different values. That matters beyond
tidiness: if a minted value reached the hash, two developers holding different
tokens would compute different images from identical inputs, and every token
rotation would force a rebuild.

The file-content half is kept as its own assertion, since both halves are
load-bearing and they pull in opposite directions.

Found by an independent review pass over the PR.

docker 44/44.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
clippy::cloned_ref_to_slice_refs fires on \paths(&[replaced.clone()])\, and
the workspace denies warnings. The clone was pointless anyway -- the slice only
needs to borrow.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…se review gaps

An independent review pass over the PR found two functional regressions against
behaviour the deleted runner.just explicitly had, plus a set of comments and
assertions that claimed more than the code delivered.

Every `just` value pasted into a PowerShell literal is escaped again. The
removed runner.just wrapped each interpolation in replace(…, "'", "''") and the
new driver had none, so a repository path containing an apostrophe broke every
anvil-container recipe, and `target` let the remainder run as host PowerShell --
outside any container, and before the engine check. The image name and workdir
stay unescaped because the first is regex-sanitized at definition and the second
is a literal; a unit test now asserts nothing else slips through, since this
convention has been lost once already.

`*target` is split back into argv. Joining its parts into one argument made
`anvil-container anvil-setup binstall` look for a recipe named "anvil-setup
binstall", and around fifty generated recipes take a parameter -- including the
one the image's own Dockerfile runs.

ANVIL_CONTAINER_NO_REBUILD is checked outside the NO_CACHE guard. Nested inside
it, a developer with NO_CACHE exported got a from-scratch build out of
`anvil-container-status`, which is precisely what NO_REBUILD exists to prevent.

The e2e's "no secret in any image layer" assertion could not fail: `history`
reports the command that created each layer, not its contents, and the value is
never a build argument. It now greps the image filesystem, where a written
secret would actually land.

Documentation corrected where it outran the code: containers.md §5 still
described the src= transport that was reverted; the Dockerfile claimed the
recipe refuses a floating base image, which it never checks; the COPY comment
overstated what is copied; the cache-volume comment claimed collision-freedom
the basename cannot provide; extensibility.md kept a conclusion whose mechanism
had changed underneath it; and the crate docs omitted the WSL-only engine path
this feature exists to support. Podman's second limitation -- it reads only a
context-root ignore file, so the whole worktree is streamed -- is documented
beside the build-secret one.

docker 46/46; cargo test and clippy --all-targets --all-features clean under
-D warnings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
BuildKit finds `<dockerfile>.dockerignore` on its own; buildah reads only a
context-root ignore file. Without the flag every podman build streamed the whole
worktree -- `target/` included -- to the daemon, and a consumer repository
owning a root `.dockerignore` had that one obeyed instead, which can exclude
`justfiles/` and fail the build for a reason that names nothing relevant.

Named rather than probed, because the engine that does not take the flag
rejects it outright. This is a capability difference between the two engines,
in the same category as the uid mapping already handled, not a second code path
through the feature.

docker 46/46, podman 31/31.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Anvil spliced `[lints] workspace = true` into every workspace member.
A member that already declares its own `[lints.*]` then carries both,
which cargo rejects outright -- and because the failure is in `cargo
metadata`, it takes down every check in the workspace rather than only
the offending crate. The ox-tools dogfood hit exactly this: one migrated
crate keeps a deliberately lenient lint set, and generating made the
repository unbuildable.

Generalize the existing delta opt-out, which already solves the same
shape of problem for `trip_wire_patterns`, into one `region_body`
decision covering both regions. A crate that owns its lints now gets an
empty managed region: the region stays tracked, so dropping the crate's
own lints later adopts the catalog with no further gesture, and the plan
explains the deferral rather than leaving it to be discovered.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 72a310c4-f37b-497e-a10b-eb8d982d532d
…ve one

`anvil-container-tag` prints the reference for the current inputs and exits.
It is now the only place the content hash is computed -- the resolver asks it
rather than repeating the computation -- because a publisher needs the tag
before there is an image to inspect, and a published tag is only meaningful
while it is the same reference the consumer will later look up.

`Anvil-ResolveImage` is a third hook phase, offered the tag when nothing local
matches and before a build starts. It returns the reference it made available
rather than re-tagging to the local name: a local tag asserts "built here from
these inputs", and a fetched image only claims that, since the hash is over
source files and cannot be re-derived from layers. The returned reference is
inspected before use, because the run is `--pull=never` and a hook that
reported an image it never fetched would fail later and further from the
cause. Every failure falls through to a local build -- a publisher that has
not caught up must not block the change it has not caught up with.

Resolution sits inside the NO_CACHE guard, since ignoring the cache has to
mean the remote one too, and before the NO_REBUILD guard, since fetching is
not building. ANVIL_CONTAINER_NO_RESOLVE skips it, and `anvil-container-status`
sets it so a question about this machine cannot pull gigabytes to answer
itself.

The identity claim in the design doc is qualified accordingly: presence
implies the current inputs by construction only for an image built here; for
a resolved one the claim rests on the registry's tag immutability.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ae262693-67bf-46d4-a5af-df8da17c2553
clippy::needless_raw_string_hashes, denied workspace-wide. The assertion string
contains no quote, so a plain raw string is enough.

Mine for pushing without running clippy on that commit; the gate exists exactly
for this.
…al' into u/mhavelka/anvil-container-acr

# Conflicts:
#	.anvil.lock
The container documentation had drifted into narrating how the feature was built: it defended design choices against alternatives readers never saw, referenced a configuration file that was never shipped, and cited the end-to-end suite as evidence. Rewrite both the design doc and the crate docs around what the feature does and how to use it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ence

`containers.md` becomes a reference specification: purpose, command surface,
execution model, emitted artifacts, image identity, environment variables, the
hook contract, host requirements, customization, and limitations. Every claim
is stated against the implementation in `container.just`, `container.rs` and
the default `Dockerfile`.

Four inaccuracies are corrected:

- Process identity. A Linux host passes `--user <uid>:<gid>`; `--userns
  keep-id`, which rootless podman needs for bind-mount ownership, is not
  passed. The previous text asserted both that a fork inherits "the uid
  mapping" and that no mapping is applied.
- Podman is best-effort rather than fully supported, and all three known
  differences from Docker are listed together.
- A new recipe subdirectory needs no ignore-file override: the build context
  re-admits `justfiles/` as a directory and the identity hashes it
  recursively. Only a Dockerfile that copies something else must replace the
  ignore file with it.
- `hooks.ps1` is not emitted by default, which is what `local.md` already
  said.

Behaviour previously undocumented is now specified: the per-invocation engine
override, cache-volume sharing between checkouts of the same directory name,
a missing image input as a hard error, the `ANVIL_SECRET_<id>` mount naming,
`--pull=never` on run, and the ordinal-sort and LF-normalization properties
that make the digest stable across platforms.

The crate documentation in `lib.rs` keeps the short form and links out;
`README.md` is regenerated from it. `extensibility.md` states the
`justfiles/`-holds-recipes-only rule once and points at the customization
section, whose anchor had gone stale.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7cdda814-67ed-42b6-9c60-01149a70f78d
…rose

The `podman-docker` alias is removed as a justification for not probing
between engines, in the recipe comment and in `containers.md`. The behaviour
is unchanged: anvil still uses only the engine `ANVIL_CONTAINER_ENGINE` names.
The remaining reasons -- presence is not reachability, and a silent choice
between two engines splits the image cache -- carry the point on their own.

Several passages are rephrased as reference documentation rather than as
development notes. The engine-resolution rationale, the WSL path translation
and the podman limitations were written as defect narratives, reproducing the
diagnosis rather than stating the behaviour a reader needs. The podman
build-secret error text is kept, since it is what a user matches against.

Two further corrections: the artifact count is three, not two -- `container.just`,
the `Dockerfile` and its ignore file, as `artifacts::container::all()` returns
and §9 already said -- and `hooks.ps1` is described as one optional script that
may define up to three functions, rather than as a single hook.

`justfiles/anvil/container.just` and the three tree snapshots are regenerated
from the template. `cargo run -p cargo-anvil -- anvil --dry-run` reports 78
items, all unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7cdda814-67ed-42b6-9c60-01149a70f78d
`containers.md` is reordered so each concept is introduced once, at the level
a reader needs it, and detail follows the overview it depends on:

  interface -> artifacts -> identity -> runtime -> engines -> hook -> customization

Two sections move as a result. The environment variables join the recipe table
in "Command surface", since together they are the complete user-facing
interface, and engine resolution and path translation move out of the
execution model into "Engines and host setup", which previously repeated them
from the other side.

Duplication removed. The `just anvil-setup` install path was explained in the
artifacts section and again under hashed inputs; the build-context scope was
stated in artifacts and again under customization; the engine diagnostic
policy appeared in both engine resolution and host requirements; "all three
functions are optional" appeared twice in consecutive paragraphs; and the
limitations section restated the platform pin, the toolchain requirement and
the podman gap already covered above. Each now appears once, with a section
reference where a reader might look for it elsewhere.

The result is 10% shorter (3714 to 3338 words) while covering the same
surface.

`lib.rs` is corrected to match: it still described the feature as two
generated artifacts, which the previous commit fixed only in the design
document. `README.md` is regenerated from it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7cdda814-67ed-42b6-9c60-01149a70f78d
@wukchung martinhavelka (wukchung) changed the title feat(anvil): containerized execution as two artifacts and a hook feat(anvil): containerized execution as generated artifacts and an optional hook Aug 14, 2026
@wukchung martinhavelka (wukchung) changed the title feat(anvil): containerized execution as generated artifacts and an optional hook feat(anvil): containerized execution - simplified Aug 14, 2026

@wukchung martinhavelka (wukchung) left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated review pass over this branch: six independent models, then every finding checked against the code before it was written up. Eleven raw findings, eight survived. Both Gemini slots returned nothing, so this is a five-model pass; one slot was still running when the review was posted.

Dropped after checking:

  • Spaces in a repository path breaking the WSL bind mount. wsl.exe preserves a spaced argument; tested.
  • image inspect throwing under $ErrorActionPreference = 'Stop' instead of setting $LASTEXITCODE. $PSNativeCommandUseErrorActionPreference is False on pwsh 7.6.4.
  • -split '\s+' losing argument boundaries. Real limitation, but just flattens *target before this code sees it, so the information is already gone and the comment says so.

Of the eight, the first two are worth acting on before merge; the rest are smaller. The volume finding is the one that undercuts a stated design property, and it reproduces against a real daemon.

Comment thread crates/cargo-anvil/templates/justfiles/anvil/container.just Outdated
Comment thread crates/cargo-anvil/templates/justfiles/anvil/container.just Outdated
Comment thread crates/cargo-anvil/templates/justfiles/anvil/container.just Outdated
Comment thread crates/cargo-anvil/templates/justfiles/anvil/container.just Outdated
Comment thread crates/cargo-anvil/templates/justfiles/anvil/checks/aprz.just Outdated
Comment thread crates/cargo-anvil/templates/container/Dockerfile Outdated
Comment thread crates/cargo-anvil/src/anvil/artifacts/container.rs Outdated
Comment thread scripts/test-anvil-container.ps1

@wukchung martinhavelka (wukchung) left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Follow-up. The sixth reviewer in the pass finished well after the others and is reported separately so the existing threads stay stable.

It independently reached the cache-volume finding and the double-quoted interpolation, which is worth knowing: those two were single-model findings in the first batch and are now two-model. It added three further items, all verified here before posting, plus one point that changes the shape of the volume finding rather than standing alone, which is a reply on that thread instead of a new one.

One thing it raised is deliberately not posted. It flagged the whitespace re-split of *target as a defect and proposed passing the values as positional arguments; just flattens a variadic parameter into a single string before the recipe body can see it, and a [script] recipe receives no argv, so the boundaries are gone upstream of this code and the comment above the split already says so. Real limitation, not a defect, and the same conclusion the earlier batch reached.

Comment thread crates/cargo-anvil/templates/justfiles/anvil/container.just Outdated
Comment thread crates/cargo-anvil/templates/justfiles/anvil/container.just Outdated
Comment thread crates/cargo-anvil/templates/justfiles/anvil/container.just Outdated
Comment thread crates/cargo-anvil/templates/anvil/container/Dockerfile Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR rewrites cargo-anvil’s containerized execution to a much smaller, explicit surface: container runs are opt-in via just anvil-container …, the image is defined by a repository-owned .anvil/container/Dockerfile (+ Dockerfile.dockerignore), and the previous tier-routing seam (ANVIL_RUNNER, runner.just, tier-routing tests, and the large generated .anvil/container/ driver set) is removed. Documentation and templates are updated to match the new contract, including the optional PowerShell hook (hooks.ps1) for secrets/env/image resolution.

Changes:

  • Remove transparent tier routing (ANVIL_RUNNER, runner.just, routed tier recipes) so tiers always run natively and container use is explicit.
  • Introduce the simplified container backend: justfiles/anvil/container.just drives build/run and computes a content-based image tag; .anvil/container/Dockerfile* defines the image; hooks.ps1 is optional.
  • Update docs/tests/templates to reflect the new container contract and credential handling (including APRZ token behavior).

Reviewed changes

Copilot reviewed 53 out of 54 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
justfiles/anvil/tiers.just Removes routing via _anvil-run; tiers call their work directly.
justfiles/anvil/runner.just Removes legacy tier-routing recipe file.
justfiles/anvil/mod.just Drops import of runner.just.
justfiles/anvil/container.just Replaces old driver invocation with new PowerShell-based engine/tag/build/run implementation.
justfiles/anvil/checks/aprz.just Simplifies token acquisition to GITHUB_TOKEN or gh auth token, warns and continues unauthenticated otherwise.
Justfile Removes anvil-runner managed region and ANVIL_RUNNER default.
crates/cargo-coverage-gate/README.md Updates doc2readme dependency metadata blob.
crates/cargo-anvil/tests/tier_routing.rs Removes tests for tier-routing seam (ANVIL_RUNNER, _anvil-run).
crates/cargo-anvil/tests/extensibility.rs Updates container artifact expectations to Dockerfile/Dockerignore/hooks model and adjusts customization test.
crates/cargo-anvil/tests/container_upgrade.rs Removes upgrade-path tests tied to the previous .anvil/container/ asset set relocation.
crates/cargo-anvil/tests/container_customization_bash.rs Removes Bash driver contract tests for the old customization/driver system.
crates/cargo-anvil/templates/regions/justfile-runner.just Removes the template for the anvil-runner region.
crates/cargo-anvil/templates/justfiles/anvil/tiers.just Mirrors tier de-routing changes in templates.
crates/cargo-anvil/templates/justfiles/anvil/runner.just Removes template for legacy runner.just.
crates/cargo-anvil/templates/justfiles/anvil/mod.just Mirrors removal of runner.just import in templates.
crates/cargo-anvil/templates/justfiles/anvil/checks/aprz.just Mirrors APRZ token logic update in templates.
crates/cargo-anvil/templates/container/Dockerfile.dockerignore Adds new build-context scoping ignore file template for Dockerfile-based image.
crates/cargo-anvil/templates/container/Dockerfile Adds new default execution-image Dockerfile template (installs tools via just anvil-setup binstall).
crates/cargo-anvil/templates/anvil/container/run-in-container.sh Removes legacy generated Bash driver script template.
crates/cargo-anvil/templates/anvil/container/run-in-container.ps1 Removes legacy generated PowerShell driver script template.
crates/cargo-anvil/templates/anvil/container/README.md Removes legacy generated container backend README template.
crates/cargo-anvil/templates/anvil/container/image-id.sh Removes legacy image-id helper template (Bash).
crates/cargo-anvil/templates/anvil/container/image-id.ps1 Removes legacy image-id helper template (PowerShell).
crates/cargo-anvil/templates/anvil/container/entrypoint.sh Removes legacy entrypoint template.
crates/cargo-anvil/templates/anvil/container/Containerfile.dockerignore Removes legacy Containerfile ignore template.
crates/cargo-anvil/templates/anvil/container/Containerfile Removes legacy Containerfile template.
crates/cargo-anvil/src/lib.rs Updates crate docs to describe explicit container invocation, content-based image identity, and hooks.
crates/cargo-anvil/src/anvil/artifacts/region.rs Removes Justfile runner region artifact emission.
crates/cargo-anvil/src/anvil/artifacts/mod.rs Removes runner artifacts and adjusts owned-file marker expectations for editable Dockerfile artifacts.
crates/cargo-anvil/src/anvil/artifacts/justfile.rs Removes embedded runner.just and updates template assertions for tiers/APRZ.
crates/cargo-anvil/README.md Mirrors updated containerized execution documentation.
crates/cargo-anvil/docs/design/README.md Updates design-doc index and repository layout notes for the new container model.
crates/cargo-anvil/docs/design/local.md Updates emitted-tree documentation for .anvil/container/ and container invocation model.
crates/cargo-anvil/docs/design/extensibility.md Replaces the old “optional container runner” section with placement rules aligned to new content hashing model.
.spelling Adds new container-related terms and removes obsolete Containerfile.
.anvil/container/run-in-container.sh Removes legacy generated Bash driver file from this repo’s generated assets.
.anvil/container/run-in-container.ps1 Removes legacy generated PowerShell driver file from this repo’s generated assets.
.anvil/container/README.md Removes legacy generated README from this repo’s generated assets.
.anvil/container/image-id.sh Removes legacy generated image-id helper (Bash) from this repo’s generated assets.
.anvil/container/image-id.ps1 Removes legacy generated image-id helper (PowerShell) from this repo’s generated assets.
.anvil/container/entrypoint.sh Removes legacy generated entrypoint from this repo’s generated assets.
.anvil/container/Containerfile.dockerignore Removes legacy generated ignore file from this repo’s generated assets.
.anvil/container/Containerfile Removes legacy generated Containerfile from this repo’s generated assets.
.anvil/container/Dockerfile.dockerignore Adds the new generated Dockerfile ignore file to this repo’s generated assets.
.anvil/container/Dockerfile Adds the new generated Dockerfile to this repo’s generated assets.
.anvil.lock Updates tracked artifact set/checksums to the new container artifacts and removed runner region.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .anvil/container/Dockerfile.dockerignore
Comment thread crates/cargo-anvil/templates/container/Dockerfile.dockerignore
Comment thread justfiles/anvil/container.just
Comment thread crates/cargo-anvil/templates/justfiles/anvil/container.just
Comment thread justfiles/anvil/container.just
Comment thread crates/cargo-anvil/templates/justfiles/anvil/container.just
The run mounted named volumes over \ and \. An engine seeds a named volume from the image only when that volume is first created, so the first image's tools were pinned over every later tag: a tool bump renamed the tag, built a new image, and still ran the old binary. Both paths are also world-writable, so the shadowed copy was mutable and outlived the tag that named it. Cache only cargo's content-addressed registry and git downloads.

Engine calls in WSL used 'wsl.exe --', which hands the command line to the login shell. A repository path containing \$ was silently truncated -- wslpath still exited 0, so the guard never fired and the wrong directory was bind-mounted -- and a ';' in a recipe argument ran on the WSL host. Use --exec, which bypasses the shell.

Also: derive the image name so it is always a valid reference (a checkout in 'ox-tools (copy)' produced a trailing separator that the engine rejects); report a failed volume removal instead of always exiting 0; give a mapped uid a writable HOME; compute the tag before the presence query so status cannot report a fatal input error as 'not present'; correct the ANVIL_IN_CONTAINER comment and the documented engine override, neither of which matched the code.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
anvil-aprz runs in the pr-fast group and queries the GitHub advisory API, which allows 60 requests an hour unauthenticated -- fewer than a full tier needs. Inside the image it had no route to a token at all, so the flagship 'just anvil-container anvil-pr' degraded to warnings and rate limits instead of failing with instructions.

Forward GITHUB_TOKEN by name when the host already has it set, so a containerized tier authenticates exactly as the same recipe does natively and CI keeps working when it runs a tier this way. It is forwarded, never minted: running 'gh auth token' in the driver would hand a broadly-scoped credential to every recipe in the container, including the ones that never see it on the host, where anvil-aprz scopes it to itself.

Forwarding by name only works if the engine can see the name, so the WSLENV bridge now covers every forwarded variable rather than only the hook's -- without that, '-e NAME' reaches an engine that cannot see NAME and silently forwards nothing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The image copied all of justfiles/ but the tag hashed only justfiles/anvil/**/*.just. A file that is copied without being hashed can change what a build produces while naming a tag that already resolves -- and because presence of the tag is checked first, the existing image is reused and the change is never built.

Narrow the context to justfiles/anvil/ so the two sets agree by construction. The public catalog had no reachable symptom, since the synthetic Justfile imports only the anvil tree, but a fork that replaces the Dockerfile can copy anything it likes, and the hashed set is fixed and cannot be extended. Record that as a limitation rather than leaving it to be discovered.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…le's

The region removal loop resolved the host's casing for its liveness and
refusal checks, then read, spliced and wrote under the casing the lock
recorded. `HostTextCache` is keyed by the exact string and the writes of the
same pass seeded it under the resolved spelling, so after a case-only rename
the removal missed the cache, re-read the pre-pass file, and wrote that text
back. Removals apply after writes, so the stale text won: a host that gained
a region in the same pass lost it again, and for a composed Dockerfile that
is every region in the file.

The read, the cache update and the write now use the resolved spelling; the
manifest key stays as recorded so the retired entry is still purged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8e2a0c6a-d500-4acf-a91b-b443c029e866

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 78 out of 82 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

crates/cargo-anvil/src/plan.rs:493

  • The manifest-provided host is used to compute a filesystem path for rewriting during region removal without validating that it is a safe repo-root-relative path. If .anvil.lock is corrupted or malicious, this could overwrite files outside the repository root.
                (Target::Region { host, id }, Decision::Remove) => {
                    // Untouched orphan region: splice the markers + body
                    // out of the host file and drop the manifest entry.
                    // The host is resolved to its on-disk casing for the
                    // write, for the reason the File arm above gives; the
                    // manifest key stays as recorded so the entry is purged.
                    let spliced = item.spliced_host.as_ref().expect("region Remove must carry spliced host");
                    let abs = repo_root.join(resolve_existing_case_insensitive(repo_root, host));
                    write_file(&abs, spliced)?;

Comment thread crates/cargo-anvil/src/plan.rs Outdated
Every path in the manifest is joined to the repository root and then read,
written or deleted. `Path::join` discards the base when given an absolute or
drive-qualified path, and `..` climbs out of it, so a corrupted or
hand-edited `.anvil.lock` could direct any of those operations outside the
repository. Paths are always stored `/`-separated and repository-relative,
so the check costs nothing a valid manifest can trip on.

Validation goes at the load site rather than the delete sites, so it covers
every path the manifest yields, including the ones used for writes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8e2a0c6a-d500-4acf-a91b-b443c029e866

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 78 out of 82 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

crates/cargo-anvil/src/region.rs:234

  • RegionPlacement::At is documented as a byte offset, but this branch slices text[..offset] / text[offset..]. If a caller passes an offset that is not on a UTF-8 char boundary, these slices will panic at runtime. Since upsert_region_with_placement is public, it should defensively normalize the offset to a valid char boundary before slicing.

Comment thread crates/cargo-anvil/src/manifest.rs
Brings in cargo-gamma. The only conflict is `.spelling`, resolved as the
union both sides intend for a dictionary.

Carries one fix alongside the merge: `ensure_contained` admitted an empty
path and a bare `.`, neither of which has a component to reject, so both
resolved to the repository root and would send a read, write or delete at
a directory. A manifest path must name at least one segment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8e2a0c6a-d500-4acf-a91b-b443c029e866
Comment thread crates/cargo-anvil/src/manifest.rs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 78 out of 82 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

crates/cargo-anvil/templates/anvil/container/Dockerfile.dockerignore:4

  • This template is used for the repository-composed Dockerfile.dockerignore. Since consumers may need to adjust the build context when they add COPY statements in Dockerfile gaps, the “DO NOT EDIT DIRECTLY” wording conflicts with the intended customization story. Use a weaker provenance marker instead so edits are not discouraged.
    .anvil/container/Dockerfile.dockerignore:4
  • The Dockerfile allow-list is intended to be repository-composable (e.g., when adding COPY statements in the Dockerfile gaps), but this header says “DO NOT EDIT DIRECTLY”, which contradicts the documented customization model and may discourage required edits. Consider switching to the weaker provenance marker used for co-owned files (e.g. “Managed by cargo-anvil”) and remove the hard "do not edit" wording.
# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY.
# Update the corresponding template in the cargo-anvil crate.

Comment thread crates/cargo-anvil/src/lib.rs
…the Dockerfile header region

Five defects, each reachable from an ordinary setup:

- A wrapped tier ran unauthenticated. `just --dry-run` reports the bodies
  just runs itself, not the body of a recipe one of them launches as a
  child process, so planning `anvil-scheduled` showed the `_anvil-unscoped`
  wrapper and none of the checks under it. The driver read that as "needs
  no token" and `anvil-aprz` then slept on the advisory API's
  unauthenticated rate limit, inside an image with no `gh` of its own. The
  driver now follows each nested target a plan names.

- The boundary forwarded `ANVIL_INCLUDE_MODIFIED` / `_AFFECTED` /
  `_REQUIRED`, which nothing reads, and dropped `ANVIL_IMPACT`, which
  `anvil-impact` does. A CI group job exports `consume`; a container that
  did not inherit it recomputed scoping from a diff instead of trusting the
  artifact the group had downloaded.

- `--separate-git-dir` produced a container in which git could not resolve
  a single ref. The mount triggered on the git directory differing from the
  common one, but that redirect leaves them equal, so the checkout kept a
  `.git` file naming a host path. The predicate is now the shape of `.git`.
  A git directory outside its common directory is refused with a message
  rather than mounted at a path that climbs out of the mount.

- The recipes hard-coded `.anvil/container/Dockerfile` while the generator
  resolves that path against the casing already on disk. On a
  case-sensitive filesystem a repository carrying `dockerfile` had its
  regions maintained in a file the build then could not find.

- The manifest's containment guard was lexical, so a path whose components
  are all ordinary still left the repository when one of them was a symlink
  pointing out of it. Since the manifest is committed, one commit could add
  both the link and the entry naming a path through it. Writes, deletes and
  proposals now resolve before acting.

The Dockerfile loses its header region: it held only commentary, and a
managed region that contributes no build instruction is a section that
exists to hold prose. The guidance it carried is in containers.md, where it
does not have to be reconciled into every adopter's file. The copyright
notice moves to the scaffold, alongside the parser directive, so a fresh
file still carries it. Remaining region comments state the constraint that
is not visible in the instruction below them and nothing else.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8e2a0c6a-d500-4acf-a91b-b443c029e866

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 77 out of 81 changed files in this pull request and generated 3 comments.

Comment thread crates/cargo-anvil/src/manifest.rs
Comment thread justfiles/anvil/tiers.just Outdated
Comment thread crates/cargo-anvil/templates/justfiles/anvil/tiers.just Outdated
…he last commit dropped

The appended tests left the file without a final newline, which rustfmt
rejects, and the containment doc comment introduced a word the hunspell
dictionary does not carry.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8e2a0c6a-d500-4acf-a91b-b443c029e866
COPY carries a file's mode into the image, so a chmod +x with no content
change altered what the image contained while the tag kept resolving, and
the stale image was reused.

The bit comes from git's index rather than the filesystem: Windows has no
executable bit, so a filesystem read would make two checkouts of one commit
disagree on the tag. A path git cannot account for frames as
non-executable, which can cost a cache miss but never a stale reuse.

Every image reference changes once.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8e2a0c6a-d500-4acf-a91b-b443c029e866

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 77 out of 81 changed files in this pull request and generated no new comments.

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

crates/cargo-anvil/src/manifest.rs:91

  • ensure_contained claims manifest paths are “/”-separated, but it doesn’t currently reject backslash-separated inputs. On Unix, a path containing \ is treated as a literal character (not a separator), which can make the same .anvil.lock behave differently across platforms and break case/casing resolution logic that assumes / splitting.
/// target a directory. Paths are always stored `/`-separated and
/// repository-relative, so anything else is malformed.
///

.anvil/container/Dockerfile.dockerignore:4

  • This file is described in the design docs as intentionally customizable (“Managed by cargo-anvil.” rather than “DO NOT EDIT”), but the header still says “GENERATED … DO NOT EDIT DIRECTLY”, which is likely to mislead adopters who need to widen the build context (or derived catalogs that intentionally replace/adjust this file).
# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY.
# Update the corresponding template in the cargo-anvil crate.

…drop a duplicated tier comment

Path::components folds a trailing '.' away, so 'a/.' satisfied the
name-count check while naming a directory, against the contract the
function documents. The raw final segment now decides.

The full tier carried its summary line twice, once as prose and once as
the doc comment just --list reads.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8e2a0c6a-d500-4acf-a91b-b443c029e866

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 77 out of 81 changed files in this pull request and generated no new comments.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

crates/cargo-anvil/src/region.rs:234

  • RegionPlacement::At treats offset as a byte index, but the implementation slices text[..offset] / text[offset..] without ensuring offset is on a UTF-8 char boundary. If a caller ever passes a non-boundary byte offset (the API docs call it a byte offset), this will panic at runtime. Normalize offset to a char boundary before slicing.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 77 out of 81 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

crates/cargo-anvil/templates/anvil/container/Dockerfile.header:4

  • docs/design/updates.md states the container Dockerfile and its ignore file should use a weaker provenance marker (“Managed by cargo-anvil.”) rather than a “do not edit” header. The generated Dockerfile currently has no such marker (and the upgrade test fixture uses it), so consumers won’t get the intended guidance.

crates/cargo-anvil/templates/anvil/container/Dockerfile.dockerignore:4

  • This file is intended to be user-customizable (it’s explicitly called out as a “Managed by cargo-anvil” exception in docs/design/updates.md), but its header currently says “GENERATED … DO NOT EDIT DIRECTLY”, which contradicts the documented customization path for .anvil/container/Dockerfile* and discourages the intended edits.

Comment thread crates/cargo-anvil/src/manifest.rs
Windows treats the backslash as a separator and Unix as an ordinary
filename character, so a path carrying one denotes different things on
different machines and slips past whichever check is written in terms of
the other: 'a\\' counts a component on Windows yet ends no '/' segment,
and '..\\x' climbs on Windows while reading as a single filename on Unix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8e2a0c6a-d500-4acf-a91b-b443c029e866

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 77 out of 81 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

justfiles/anvil/container.just:135

  • anvil-container-tag assumes the build-context ignore file is named <DockerfileName>.dockerignore (derived from the resolved Dockerfile path). If a repo has a case-only renamed Dockerfile (e.g. .anvil/container/dockerfile), the generated ignore file is still .anvil/container/Dockerfile.dockerignore, so tag computation/build will look for .anvil/container/dockerfile.dockerignore and fail. Add an explicit check here so the failure is immediate and actionable (and consider aligning generation so the ignore file casing matches the Dockerfile casing).

Comment thread crates/cargo-anvil/src/manifest.rs Outdated
Comment thread crates/cargo-anvil/templates/justfiles/anvil/container.just Outdated
Comment thread crates/cargo-anvil/templates/justfiles/anvil/container.just
…mod naming the wrong image, and reject a drive-qualified manifest path

Resolving the Dockerfile to its on-disk casing broke the ignore file. The
engine derives that name from the Dockerfile's -- BuildKit reads
<dockerfile>.dockerignore and takes no flag pointing elsewhere -- while
anvil maintains the artifact at a fixed canonical path, so a repository
carrying 'dockerfile' would build with no ignore file at all and stream the
whole worktree into the context. The same derivation missed the owned file
in the text-normalized set, so CRLF and LF checkouts could disagree on the
tag. The two names must agree and only one can move, so a case variant is
now refused with the reason rather than accommodated.

The digest reads the index mode while the build copies the working tree, so
an unstaged chmod changed the image without renaming it. Framing the working
tree instead would break the property the index was chosen for: Windows has
no executable bit, so two checkouts of one commit would disagree and a
published image would stop resolving. The disagreement is refused instead,
naming the file and the recovery.

Path::components reports a drive qualifier as a Prefix on Windows and as an
ordinary name on Unix, so 'C:x.txt' was accepted there despite the
documented contract. Matched lexically now.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8e2a0c6a-d500-4acf-a91b-b443c029e866

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 77 out of 81 changed files in this pull request and generated no new comments.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

justfiles/anvil/checks/aprz.just:30

  • The gh auth token call doesn’t check $LASTEXITCODE. For external commands, a non-zero exit code won’t be caught by try/catch, so this can treat a failed gh invocation as success if it emits any output. Explicitly clear $tok when gh exits non-zero before using it as GITHUB_TOKEN.

Comment thread crates/cargo-anvil/templates/justfiles/anvil/container.just Outdated
git diff --raw encodes an unstaged deletion as a transition to mode 000000
and an intent-to-add as one from it, so the guard added for chmod drift
aborted the tag on both. Retiring a managed file and not yet staging the
deletion is the ordinary way to reach that. A path missing from the build
context is missing from the digest too, so only a transition between two
real modes is drift.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8e2a0c6a-d500-4acf-a91b-b443c029e866

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 77 out of 81 changed files in this pull request and generated no new comments.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

justfiles/anvil/checks/aprz.just:37

  • In a container run, this recipe cannot obtain a token via gh (the image intentionally has no gh CLI), so when GITHUB_TOKEN is missing it will always fall back to unauthenticated access and can block for up to an hour (as the comment above notes). Consider failing fast when ANVIL_IN_CONTAINER is set and no token is available, with actionable instructions to set host GITHUB_TOKEN or authenticate gh on the host so anvil-container can forward it.

…igest framed

The drift guard compared the two mode fields of git diff --raw, but the raw
index field is not what the digest uses. An intent-to-add entry reports zero
there while ls-files --stage reports a real placeholder mode, so an
executable file added with git add -N was exempted although it reaches the
build context with its mode, and an ordinary one would have been rejected.

Only a zero working-tree mode is exempt now, because that is a deletion and
the path is in neither the context nor the digest. Everything else is
compared against the framed bit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8e2a0c6a-d500-4acf-a91b-b443c029e866

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 77 out of 81 changed files in this pull request and generated no new comments.

Suppressed comments (2)

.anvil/container/Dockerfile.dockerignore:4

  • This file’s header says "GENERATED ... DO NOT EDIT DIRECTLY", but the design docs explicitly call out .anvil/container/Dockerfile* as an exception that should be edited in place when customizing container builds. The current wording is misleading for repository maintainers.
# GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY.
# Update the corresponding template in the cargo-anvil crate.

crates/cargo-anvil/templates/anvil/container/Dockerfile.dockerignore:4

  • This header says "GENERATED ... DO NOT EDIT DIRECTLY", but the design docs explicitly call out .anvil/container/Dockerfile* as an exception that should be editable in place (marked "Managed by cargo-anvil."). The current wording is misleading and contradicts the documented customization path.

Comment thread crates/cargo-anvil/templates/justfiles/anvil/container.just Outdated
COPY preserves a symlink as a symlink while the digest walk reads through
it, so replacing a regular file with a link to identical bytes changed the
image object without renaming the tag. The executable bit was the only part
of the mode being framed, and the drift guard compared the same single bit,
so a 100644 to 120000 transition passed both.

The digest now frames the index mode itself and the guard compares against
that mode, which subsumes the executable bit and covers every regular-file
transition git can record.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8e2a0c6a-d500-4acf-a91b-b443c029e866

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 77 out of 81 changed files in this pull request and generated no new comments.

Suppressed comments (4)

Previously missed (3) — in code that hasn't changed since the last review.

crates/cargo-anvil/templates/anvil/container/Dockerfile.header:4

  • This Dockerfile scaffold doesn’t include the "Managed by cargo-anvil." provenance marker, but docs/design/updates.md says .anvil/container/Dockerfile* should intentionally use that weaker marker (because repositories are expected to edit these files in place). Keeping the template aligned avoids confusing downstream users and keeps generated output consistent with the design docs.
    crates/cargo-anvil/src/lib.rs:244
  • The rustdoc says anvil owns "six regions" in .anvil/container/Dockerfile, but the generated Dockerfile/lock entries show five managed regions (base-image, base, tools, setup, entry). This mismatch makes the contract unclear for users adding content to the gaps.
//! `.anvil/container/Dockerfile` is a **user-composed file with managed
//! regions**: anvil owns six regions inside it and keeps them current, and the
//! gaps between them are the repository's. Add to the gap that matches when the
//! addition is needed -- re-declare `ARG BASE_IMAGE` to build on another base,

crates/cargo-anvil/docs/design/local.md:65

  • This sentence says the container Dockerfile is composed from "four managed regions", but the current composed Dockerfile contains five anvil-managed regions (base-image, base, tools, setup, entry). The count matters because it determines how many user-editable gaps exist.
container `Dockerfile` is composed the same way, from four managed regions with

crates/cargo-anvil/templates/anvil/container/Dockerfile.dockerignore:4

  • The header says "GENERATED BY cargo-anvil. DO NOT EDIT DIRECTLY", but docs/design/updates.md explicitly calls out .anvil/container/Dockerfile* as an exception that should be marked "Managed by cargo-anvil." (since repositories are expected to customize the Dockerfile and ignore file). This header currently contradicts that contract.

@github-actions

Copy link
Copy Markdown

⚠️ SemVer check advisory

Potential breaking changes

cargo semver-checks flagged the following on this PR. This is informational -- breaking changes between commits are expected; the major-version bump happens at release time, not on every PR.

cargo-anvil

     Cloning 95089c509ae50e028cb16406b07809f084150a42
    Building cargo-anvil v0.5.0 (current)
       Built [   5.128s] (current)
     Parsing cargo-anvil v0.5.0 (current)
      Parsed [   0.011s] (current)
    Building cargo-anvil v0.5.0 (baseline)
       Built [   5.092s] (baseline)
     Parsing cargo-anvil v0.5.0 (baseline)
      Parsed [   0.008s] (baseline)
    Checking cargo-anvil v0.5.0 -> v0.5.0 (no change; assume minor)
     Checked [   0.019s] 196 checks: 195 pass, 1 fail, 0 warn, 57 skip

--- failure function_missing: pub fn removed or renamed ---

Description:
A publicly-visible function cannot be imported by its prior path. A `pub use` may have been removed, or the function itself may have been renamed or removed entirely.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.49.0/src/lints/function_missing.ron

Failed in:
  function cargo_anvil::artifacts::container::image_id, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-95089c509ae50e028cb16406b07809f084150a42/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/container.rs:76
  function cargo_anvil::artifacts::container::powershell_driver, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-95089c509ae50e028cb16406b07809f084150a42/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/container.rs:94
  function cargo_anvil::artifacts::container::ignore_file, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-95089c509ae50e028cb16406b07809f084150a42/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/container.rs:64

  function cargo_anvil::artifacts::justfile::runner, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-95089c509ae50e028cb16406b07809f084150a42/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/justfile.rs:225
  function cargo_anvil::artifacts::container::shell_image_id, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-95089c509ae50e028cb16406b07809f084150a42/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/container.rs:82
  function cargo_anvil::artifacts::container::readme, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-95089c509ae50e028cb16406b07809f084150a42/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/container.rs:100
  function cargo_anvil::artifacts::container::customize_powershell, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-95089c509ae50e028cb16406b07809f084150a42/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/container.rs:122
  function cargo_anvil::artifacts::container::entrypoint, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-95089c509ae50e028cb16406b07809f084150a42/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/container.rs:70
  function cargo_anvil::artifacts::container::shell_driver, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-95089c509ae50e028cb16406b07809f084150a42/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/container.rs:88
  function cargo_anvil::artifacts::region::justfile_runner, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-95089c509ae50e028cb16406b07809f084150a42/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/region.rs:146
  function cargo_anvil::artifacts::container::customize_shell, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-95089c509ae50e028cb16406b07809f084150a42/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/container.rs:113
  function cargo_anvil::artifacts::container::containerfile, previously in file /home/runner/work/ox-tools/ox-tools/target/semver-checks/git-95089c509ae50e028cb16406b07809f084150a42/a79863149d5452f525905a3b1c5b761011f94d76/crates/cargo-anvil/src/anvil/artifacts/container.rs:58
     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  11.112s] cargo-anvil

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agency-rocket Touched by a rocket skill

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants