Skip to content

test(deploy): require every compile-time include to land in the Docker build context - #306

Merged
argszero merged 1 commit into
mainfrom
fix/deploy-guard-the-release-build-context
Sep 27, 2026
Merged

argszero merged 1 commit into
mainfrom
fix/deploy-guard-the-release-build-context

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

include_str! reads its target at compile time, so a target that is not in the Docker build context makes docker build fail outright (couldn't read ...). The context has two deciders — the Dockerfile's COPY sources (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 reason COPY config ./config exists — and that reason is written only in a Dockerfile comment, and a comment is not an executor). The seven that point outside the context are safe solely because their modules are declared #[cfg(test)] in src/main.rs:

src/deploy_gate.rs:97    ../docker-compose.yml
src/deploy_gate.rs:102   ../Dockerfile
src/deploy_gate.rs:103   ../README.md
src/deploy_gate.rs:104   ../README.en.md
src/deploy_gate.rs:632   ../CHANGELOG.md
src/deploy_gate.rs:761   ../.dockerignore
src/state_gate.rs:3403   ../docs/prototype/aitokenpool-console.html

That is the same shape as R76's MSRV, R80's release tag and R82's CHANGELOG.md heading — a declaration with consumers and no executor — and its failure point sits on the release chain: the publish job in .github/workflows/docker-publish.yml runs a real docker build (a mis-scoped include turns that job red at tag time), the README's first recommended command docker compose up -d --build fails the same way, and CI never runs docker 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 a COPY source and matched by no .dockerignore pattern — or the site must be unreachable in a release build (its module is a #[cfg(test)] module of src/main.rs, or the site sits after the file's own #[cfg(test)]).

    • Both deciders are read from the tree: copy_sources() parses Dockerfile (COPY --from=… is skipped — a cross-stage copy reads no context), ignore_patterns()/pattern_matches() read .dockerignore with 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 and CHANGELOG.md are excluded by *.md, while docker-compose.yml / Dockerfile have no COPY source at all.
    • The #[cfg(test)] roster is derived, not snapshotted: test_only_modules() reads the mod X; lines of src/main.rs whose preceding line is #[cfg(test)] (mod tests { block modules are ignored — they have no file).
    • The corpus is a runtime walk of src/**/*.rs via CARGO_MANIFEST_DIR, following body_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.
    • Zero new files, zero new dependencies, and it is #[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. .dockerignore and Dockerfile are 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 --check rc=0, cargo clippy --all-targets -- -D warnings rc=0.

  • 新增/更新了单元测试(如适用)

    Three tests, each owning one claim:

    • the axis — the live tree: no release-reachable include points outside the context.
    • positive control — the scanner really sees the corpus and the context it judges by: ≥20 source files, ≥40 sites, the parsed COPY sources and .dockerignore patterns, 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.
    • teeth — on synthetic input, so that "the rule works" and "the tree happens to be compliant" stay two separate readings: an out-of-context embed in a release module is reported with its file:line, the same site in a #[cfg(test)] module passes, an in-context target passes, a site after the file's own #[cfg(test)] passes, a commented-out include_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.rs md5 ddbc4bf2…, .dockerignore md5 87d51bdc…):

    1. Removing the #[cfg(test)] that precedes mod deploy_gate; in src/main.rs turns 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:

      src/deploy_gate.rs:97: ../docker-compose.yml → docker-compose.yml 不在 Docker 构建上下文里, …
      src/deploy_gate.rs:102: ../Dockerfile → Dockerfile 不在 Docker 构建上下文里, …
      src/deploy_gate.rs:103: ../README.md → README.md 不在 Docker 构建上下文里, …
      src/deploy_gate.rs:104: ../README.en.md → README.en.md 不在 Docker 构建上下文里, …
      src/deploy_gate.rs:632: ../CHANGELOG.md → CHANGELOG.md 不在 Docker 构建上下文里, …
      src/deploy_gate.rs:761: ../.dockerignore → .dockerignore 不在 Docker 构建上下文里, …
      
    2. Appending config/config.example.toml to .dockerignore turns 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:

      src/main.rs:126: ../config/config.example.toml → config/config.example.toml 不在 Docker 构建上下文里, …
      
  • Scope, honestly recorded in the module doc: the rule is lexical. It does not prove docker build actually runs (that needs a Linux container — the same gap R76 recorded and measured), it does not check that a COPY destination is correct, and it does not implement .dockerignore's ** or negation (!) semantics — the ten lines currently in the repository use neither.

Checklist

  • 分支命名符合约定(feat/ /fix/ /docs/ ...) — fix/deploy-guard-the-release-build-context
  • Commit message 使用 Conventional Commits 格式 — test(deploy): require every compile-time include to land in the Docker build context
  • 单一职责,改动最小化 — one file, test-only, no production code path changed

…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).
@argszero
argszero merged commit 0cd878c into main Sep 27, 2026
2 checks passed
@argszero
argszero deleted the fix/deploy-guard-the-release-build-context branch September 27, 2026 03:18
@argszero argszero mentioned this pull request Sep 30, 2026
12 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant