test(deploy): require every compile-time include to land in the Docker build context - #306
Merged
Merged
Conversation
…r build context `include_str!` reads a file at compile time, so a target that is not in the build context makes `docker build` fail outright (`couldn't read ...`). The context has two deciders -- the Dockerfile's `COPY` sources and `.dockerignore` -- and the property had no executor: of the 47 compile-time includes under `src/`, only one (`main.rs` -> `config/config.example.toml`, the reason `COPY config ./config` exists) is release-reachable; the seven that do point outside the context are safe solely because their modules are `#[cfg(test)]`. CI never runs `docker build`, so this is invisible until a tag is pushed or `docker compose up -d --build` is run. The fourth rule in `src/deploy_gate.rs` asserts: every include target is either inside the context (covered by a `COPY` source, matched by no `.dockerignore` pattern) or the site is unreachable in a release build. - The `#[cfg(test)]` roster is **derived** from `src/main.rs`; the corpus is a runtime walk of `src/**/*.rs` (`CARGO_MANIFEST_DIR`), so no roster or snapshot needs syncing. - The extractor masks comments and string bodies in one state machine, so the string fixtures in this module's own tests are not read as sites (the naive "truncate the line at `//`" version read them, and produced targets like `../CHANGELOG.md\`). - Scope is lexical, as the module doc records: it does not prove that `docker build` runs, and it does not implement `.dockerignore`'s `**` or negation semantics (the current ten lines use neither).
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.
Summary
include_str!reads its target at compile time, so a target that is not in the Docker build context makesdocker buildfail outright (couldn't read ...). The context has two deciders — the Dockerfile'sCOPYsources (what reaches the builder) and.dockerignore(what is discarded before it gets there) — and this property had no executor at all.Measured on this tree: of the 47 compile-time includes under
src/, exactly one is release-reachable (src/main.rs:126→config/config.example.toml, which is the reasonCOPY config ./configexists — and that reason is written only in aDockerfilecomment, and a comment is not an executor). The seven that point outside the context are safe solely because their modules are declared#[cfg(test)]insrc/main.rs:That is the same shape as R76's MSRV, R80's release tag and R82's
CHANGELOG.mdheading — a declaration with consumers and no executor — and its failure point sits on the release chain: thepublishjob in.github/workflows/docker-publish.ymlruns a realdocker build(a mis-scoped include turns that job red at tag time), the README's first recommended commanddocker compose up -d --buildfails the same way, and CI never runsdocker build, so the tree stays green until a release or a deploy.So this adds the missing executor.
Related Issue
None — no tracking issue exists for this; it was found while auditing the "declaration with no executor" family after #303 / #304 / #305.
Changes
src/deploy_gate.rs(fourth rule): every compile-time include target must be either in the build context — covered by aCOPYsource and matched by no.dockerignorepattern — or the site must be unreachable in a release build (its module is a#[cfg(test)]module ofsrc/main.rs, or the site sits after the file's own#[cfg(test)]).copy_sources()parsesDockerfile(COPY --from=…is skipped — a cross-stage copy reads no context),ignore_patterns()/pattern_matches()read.dockerignorewith Docker's semantics (*and?do not cross/, a directory hit covers everything under it). Judging with only one of the two would have missed an entire class: the two READMEs andCHANGELOG.mdare excluded by*.md, whiledocker-compose.yml/Dockerfilehave noCOPYsource at all.#[cfg(test)]roster is derived, not snapshotted:test_only_modules()reads themod X;lines ofsrc/main.rswhose preceding line is#[cfg(test)](mod tests {block modules are ignored — they have no file).src/**/*.rsviaCARGO_MANIFEST_DIR, followingbody_limit_gate.rs::source_files, so no hand-written include roster needs a completeness guard of its own and tomorrow's new source file is in scope automatically.#[cfg(test)]-gated like the rest of the module — deliberate, as the module header records.The extractor masks comments and string bodies in one state machine (nested
/* */, raw strings, char literals, and//inside a string literal such as a URL). Truncating each line at the first//is not equivalent: this module's own tests pass fixture sources as string literals, and the naive version read those fixtures as sites and produced targets such as../CHANGELOG.md\. The positive control now also asserts that every target it reads is a clean path literal, which is the regression assertion for exactly that defect.No config / data-structure change.
.dockerignoreandDockerfileare only read, never modified.Tests
cargo test全部通过 — 405 passed / 0 failed (402 → 405; the three new tests are the rule plus its two companions).cargo fmt --checkrc=0,cargo clippy --all-targets -- -D warningsrc=0.新增/更新了单元测试(如适用)
Three tests, each owning one claim:
COPYsources and.dockerignorepatterns, the*-does-not-cross-/matching semantics, a non-empty derived#[cfg(test)]roster whose every name has a real file, and two derived (not snapshotted) cross-checks: the raw textual occurrence count is strictly greater than the code-position site count (the masker is actually filtering something — it currently filters 16 comment lines plus the string fixtures), and no target contains a quote, backslash, semicolon or space.#[cfg(test)]module passes, an in-context target passes, a site after the file's own#[cfg(test)]passes, a commented-outinclude_str!is not a site, and a target under an ignored directory (docs/) is reported.Verified by mutation, both legs reverted byte-identically afterwards (
src/main.rsmd5ddbc4bf2…,.dockerignoremd587d51bdc…):Removing the
#[cfg(test)]that precedesmod deploy_gate;insrc/main.rsturns the axis red and names exactly the six real out-of-context sites in that file — no garbage entries, which is the check that the extractor is faithful:Appending
config/config.example.tomlto.dockerignoreturns the axis red on the one release-reachable embed — i.e. the rule catches the real at-tag-time failure, not just a hypothetical one:Scope, honestly recorded in the module doc: the rule is lexical. It does not prove
docker buildactually runs (that needs a Linux container — the same gap R76 recorded and measured), it does not check that aCOPYdestination is correct, and it does not implement.dockerignore's**or negation (!) semantics — the ten lines currently in the repository use neither.Checklist
fix/deploy-guard-the-release-build-contexttest(deploy): require every compile-time include to land in the Docker build context