Skip to content

Keep a line continuation active across an empty line in the base-image preflight parser #393

Description

@swinney

Objective

Make _instruction_text in src/cli/managers/base_image_preflight.py keep a line
continuation active across an empty physical line, so a FROM that Docker joins into the
preceding RUN is no longer read as a build stage.

Context you need

_instruction_text blanks the lines of a Dockerfile that only look like instructions
(heredoc payloads, continuations, comments) before _final_stage_base scans what is left for
FROM stages. Its whole purpose is that the preflight must never conclude a template is
covered when the image that actually ships is third-party — the module docstring calls a
silent pass "the one thing this module may not do".

The defect. In the continuation branch, an empty line clears continued:

  • src/cli/managers/base_image_preflight.py:232continued = not pending and line.rstrip().endswith(escape)

An empty line does not end with the escape character, so continued becomes False and the
next line is read as an instruction. Docker does not agree: it joins across the empty line
and only emits a NoEmptyContinuation build check warning.

Measured, on 58fcdd74 (head of PR #387 at the time of filing):

from src.cli.managers.base_image_preflight import _final_stage_base
t = "FROM docker.io/library/debian:12\nRUN echo x \\\n\nFROM ghcr.io/fasrc/a2rchi-python-base:1.0\n"
_final_stage_base(t)   # -> 'ghcr.io/fasrc/a2rchi-python-base:1.0'

Docker's real final stage for that template is docker.io/library/debian:12. Verified with
docker build --check (Docker 29.5.1, docker/dockerfile:1 frontend) using a payload that
points at a nonexistent registry, so a stage Docker really parses shows up as a metadata
resolution and a stage it swallows does not:

# syntax=docker/dockerfile:1
FROM docker.io/library/busybox:latest
RUN echo x \

FROM nonexistent.invalid/definitely/not-real:1.0

Result: only docker.io/library/busybox:latest metadata is loaded, no error, plus
WARNING: NoEmptyContinuation. The bogus FROM was swallowed into the RUN. So the
preflight reports a supported base for a template whose shipped stage is third-party, and the
deployment proceeds without probing the image that actually ships.

Docker's own stance, which you must decide against explicitly: the
NoEmptyContinuation check
says empty continuation lines will become an error in a future release. Today they are
permitted. Target today's behavior — a preflight that models a build Docker will not accept
in future is still correct now, and failing closed on a form Docker currently builds would
refuse a working template.

Why it was deferred. Found by the async Codex reviewer at 2026-08-31T08:51:01Z on PR #387
head 58fcdd74 (inline thread id 3892983973). The 2026-08-31 nightly review had already
reached its NIGHTLY_REVIEW_MAX_ROUNDS bound after fixing three findings of the same class
in 90d95296 and 58fcdd74, so it was verified and handed off rather than fixed unattended.
It is a pre-existing gap, not a regression introduced by either of those commits.

Sibling case already fixed, and your model for this one: a comment line inside a
continuation had the identical defect and was fixed in 90d95296 at
src/cli/managers/base_image_preflight.py:229. Read that branch first; this fix belongs
beside it.

Constraints

  • Branch from origin/dev. origin is fasrc/archi; upstream is archi-physics/archi.
  • Open the PR with gh pr create --repo fasrc/archi --base dev. Never target upstream.
  • Never commit directly to dev.
  • TDD: write the failing test first, watch it fail, then the minimum code, then refactor.
  • Gate must pass before every commit: bash scripts/gate.sh (black 24.10.0 + isort 6.0.1,
    then pytest tests/unit/ with diff-cover patch coverage --fail-under=80 vs origin/dev).
    Never bypass with --no-verify.
  • No Co-Authored-By or session trailers on commits in this repo.
  • If PR fix(#382): require a service template's final-stage base to be one the preflight can probe #387 is still open when you start, the code lives on branch
    fix/issue-382-placeable-base and not yet on dev — branch from that PR's head instead of
    origin/dev, and say so in the PR body. If fix(#382): require a service template's final-stage base to be one the preflight can probe #387 has merged, branch from origin/dev as
    normal. Check first: gh pr view 387 --repo fasrc/archi --json state,headRefName,mergedAt.

Plan

Single PR — this is one behavior change, no mechanical churn to separate.

  1. Add a failing test in tests/unit/test_base_image_preflight.py next to
    test_a_comment_inside_a_continuation_does_not_end_the_continuation, asserting that a
    template whose final stage is third-party, followed by RUN echo x \, an empty line, and
    a FROM naming a placeable a2rchi base, is reported by
    templates_missing_base_reference(). Run it and watch it fail.
  2. In _instruction_text, in the if continued: branch, treat an empty (whitespace-only)
    line the way the comment case at line 229 is treated: append the blank, continue, and
    leave continued untouched.
  3. Decide and encode one edge case deliberately, with a test either way: does an empty line
    inside a heredoc payload still terminate anything? It must not — the if pending: branch
    runs before the continuation branch and an empty line is only a terminator if the delimiter
    is itself empty. Add a regression test proving a heredoc payload containing an empty line
    is still fully blanked.
  4. Update the _instruction_text docstring. It currently documents the comment case as
    handled; add the empty-line case to the same paragraph so the next reader does not have to
    re-derive it. Cite the NoEmptyContinuation decision from "Context you need".
  5. Run the gate, commit, push, open the PR against fasrc/archi:dev.

Commands

# 0. Where does the code live right now?
gh pr view 387 --repo fasrc/archi --json state,headRefName,mergedAt

# 1. Reproduce the defect (run from the repo root; pipe via stdin so you measure the
#    working tree and not an installed copy of the package)
python - <<'PY'
import subprocess
print("SHA:", subprocess.run(["git","rev-parse","HEAD"],capture_output=True,text=True).stdout.strip())
from src.cli.managers.base_image_preflight import _final_stage_base
t = "FROM docker.io/library/debian:12\nRUN echo x \\\n\nFROM ghcr.io/fasrc/a2rchi-python-base:1.0\n"
print("got     :", _final_stage_base(t))
print("expected: docker.io/library/debian:12")
PY

# 2. Confirm Docker's behavior yourself rather than trusting this issue
D=$(mktemp -d); mkdir -p "$D/empty"
printf '# syntax=docker/dockerfile:1\nFROM docker.io/library/busybox:latest\nRUN echo x \\\n\nFROM nonexistent.invalid/definitely/not-real:1.0\n' > "$D/empty/Dockerfile"
docker build --check "$D/empty"
# expect: only busybox metadata loaded, no error, WARNING: NoEmptyContinuation

# 3. Red test, then fix, then the gate
python -m pytest tests/unit/test_base_image_preflight.py -q
bash scripts/gate.sh

# 4. Ship
git push -u origin HEAD:<your-branch>
gh pr create --repo fasrc/archi --base dev

Acceptance criteria

  • _final_stage_base("FROM docker.io/library/debian:12\nRUN echo x \\\n\nFROM ghcr.io/fasrc/a2rchi-python-base:1.0\n") returns docker.io/library/debian:12.
  • A new test asserting the above fails on the parent commit and passes on yours. State
    both results in the PR body.
  • A regression test proves an empty line inside a heredoc payload does not end the
    payload.
  • test_a_comment_inside_a_continuation_does_not_end_the_continuation,
    test_a_comment_inside_a_continuation_cannot_open_a_heredoc,
    test_a_multi_line_continuation_is_skipped_to_its_end, and
    test_a_real_stage_after_a_continuation_is_still_read all still pass — an empty-line
    fix must not change how a non-empty continuation ends.
  • bash scripts/gate.sh exits 0, with patch coverage 100% on the changed file.
  • The _instruction_text docstring names the empty-line case and the
    NoEmptyContinuation decision.
  • PR is open against fasrc/archi:dev, no Co-Authored-By trailer, and references
    PR fix(#382): require a service template's final-stage base to be one the preflight can probe #387 and this issue.

Start here

Run command 0 to find out whether the code is on dev or still only on
fix/issue-382-placeable-base, branch accordingly, then run command 1 and confirm you see
ghcr.io/fasrc/a2rchi-python-base:1.0 — that is the defect. Then read
src/cli/managers/base_image_preflight.py:225-242 and the comment-case fix at line 229,
which is the shape your fix should take.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions