feat(5): overleveringspakka er KUN kjørbar Python — container-innpakningen fjernet
Operatørdirektiv 14.08 etter ekstern test: mottakeren skal få kjørbar Python, ikke en Docker-innpakking. `Dockerfile` og `azure.yaml` er SLETTET fra treet. Sømmen er valgt av den eksisterende invarianten, ikke av smak: pakka ER `git archive HEAD`, så å ekskludere filene fra arkivet ville krevd en kurerings-mekanisme — den andre kopien av «hva mottakeren får», fri til å drifte fra HEAD (kø-(p)). Fjerning holder arkivet ukurert og gjør fraværet til en egenskap ved HEAD, som er det eneste en gate kan måle. De to gatene som pinnet flaten er håndtert bevisst: - 4e-rå-tekst-gaten (`--platform linux/amd64` + ÉN kopi av startkommandoen) er SLETTET, med et notat der den sto. En gate som pinner en fjernet flate kan bare bli grønn. - handover-gatens `_REQUIRED_MEMBERS` er ikke bare fratatt de to navnene, men erstattet av en POSITIV fraværs-assert pluss en dokument-gate. Å kun slutte å KREVE dem ville gitt en gate som ikke kan skille «fjernet» fra «shippes fortsatt». Startkommandoen har nå ÉN kopi igjen: DEPLOY.md-ens `python main.py`, som navngir inngangen subprosess-testen faktisk kjører.
This commit is contained in:
parent
642ce8ae9a
commit
605c02aa62
10 changed files with 215 additions and 205 deletions
|
|
@ -2,15 +2,24 @@
|
|||
Microsoft Foundry with minimal friction.
|
||||
|
||||
**Why an archive and not "clone the repo".** The receiving party is not a contributor: they get a
|
||||
tree, set two environment variables and deploy it. The measured docker build context has always been
|
||||
``git archive HEAD`` (Dockerfile header), so the package is that SAME tree — never a hand-curated
|
||||
copy, which would be the second copy that drifts (the kø-(p) rule applied to a deliverable).
|
||||
tree, set two environment variables and run it. The package is ``git archive HEAD`` itself — never a
|
||||
hand-curated copy, which would be the second copy that drifts (the kø-(p) rule applied to a
|
||||
deliverable).
|
||||
|
||||
Three seams are gated here, and each one is a way the handover fails in the receiver's hands rather
|
||||
**Runnable Python, no container wrapper (operator directive 14.08, from an external trial).** What a
|
||||
receiver gets is a Python tree they install and start themselves — ``uv sync --frozen`` +
|
||||
``python main.py`` — and the ``Dockerfile``/``azure.yaml`` pair that used to ride along was removed
|
||||
from the tree rather than filtered out of the archive. Filtering would have meant a curation
|
||||
mechanism deciding what a receiver sees, i.e. a SECOND copy of "what is delivered" free to drift
|
||||
from HEAD (the kø-(p) rule, which is the very reason this package is ``git archive HEAD``). Removing
|
||||
the files keeps the archive uncurated and makes the absence a property of HEAD — which is what the
|
||||
gate below can actually measure.
|
||||
|
||||
Four seams are gated here, and each one is a way the handover fails in the receiver's hands rather
|
||||
than in ours:
|
||||
|
||||
1. **Completeness** — a tree missing ``uv.lock`` resolves different versions than every measurement
|
||||
in this repo ran against; missing ``shared/`` gives a container with no example knowledge base.
|
||||
in this repo ran against; missing ``shared/`` gives a receiver with no example knowledge base.
|
||||
2. **Exposure** — ``STATE.md``, ``*.local.md`` and ``.env`` must never leave this machine. The
|
||||
archive is built from tracked files only, so this is a property of the BUILDER; the control below
|
||||
proves the check looks for names that could actually appear.
|
||||
|
|
@ -19,6 +28,10 @@ than in ours:
|
|||
only an endpoint gets a container that answers ``/readiness`` and fails every ``/invocations``.
|
||||
4e measured that requirement and called it "et deploy-krav ingen rad hadde skrevet ned"; DEPLOY.md
|
||||
is that row, and this test is what keeps it written.
|
||||
4. **Python-only delivery** — the archive carries no container/azd wrapper, and DEPLOY.md starts the
|
||||
service the way the tree actually supports: as a Python process. A gate that merely stopped
|
||||
REQUIRING ``Dockerfile`` could not tell "removed" from "still shipped", so the check is positive
|
||||
(absence, asserted) rather than an omission.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -33,10 +46,10 @@ _REPO_ROOT = Path(__file__).resolve().parents[1]
|
|||
_SCRIPT = _REPO_ROOT / "scripts" / "make-handover-package.sh"
|
||||
|
||||
# Deploy-critical members. Each one is load-bearing for a receiver, not decoration:
|
||||
# the two manifests, the entry point, the locked resolution, the packaged data and the example base.
|
||||
# the entry point, the locked resolution, the packaged data and the example base. `Dockerfile` and
|
||||
# `azure.yaml` were REQUIRED here until 14.08; they are gone from the tree, and their absence is now
|
||||
# asserted below instead of their presence.
|
||||
_REQUIRED_MEMBERS = (
|
||||
"Dockerfile",
|
||||
"azure.yaml",
|
||||
"main.py",
|
||||
"pyproject.toml",
|
||||
"uv.lock",
|
||||
|
|
@ -51,6 +64,25 @@ _REQUIRED_MEMBERS = (
|
|||
_FORBIDDEN_SUFFIXES = (".local.md", ".env")
|
||||
_FORBIDDEN_NAMES = ("STATE.md",)
|
||||
|
||||
# The container/azd wrapper. Matched on archive MEMBER NAMES, never on prose: the documents may
|
||||
# explain that no image is shipped, and a gate that read the word out of a sentence would be red on
|
||||
# exactly the prose it protects (this repo's 08-09 defect class).
|
||||
_CONTAINER_WRAPPER_NAMES = ("Dockerfile", ".dockerignore", "docker-compose.yml", "azure.yaml")
|
||||
|
||||
# The start command a receiver is told to run. With no image CMD left, the document IS the one copy —
|
||||
# and it names the entry point whose serve/SIGTERM behaviour test_hosting_loadbearing measures.
|
||||
_PYTHON_START = "python main.py"
|
||||
|
||||
# Command invocations that would put a container step back into the documented path. Fragments, not
|
||||
# the bare word: "no container image is shipped" must stay sayable.
|
||||
_WRAPPER_COMMANDS = ("docker build", "docker run", "azd up", "azd deploy", "azd provision")
|
||||
|
||||
|
||||
def _members_named(names: list[str], wanted: tuple[str, ...]) -> list[str]:
|
||||
"""Archive members whose basename is one of ``wanted`` — the matcher both the assertion and its
|
||||
control run through, so a matcher that silently matches nothing cannot pass unnoticed."""
|
||||
return [n for n in names if Path(n).name in wanted]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def package(tmp_path_factory: pytest.TempPathFactory) -> zipfile.ZipFile:
|
||||
|
|
@ -129,3 +161,63 @@ def test_deploy_doc_states_the_placeholder_requirement(package: zipfile.ZipFile)
|
|||
assert "REPLACE-WITH-" in doc, (
|
||||
"DEPLOY.md must state that the packaged deployment ids are placeholders"
|
||||
)
|
||||
|
||||
|
||||
def test_package_ships_runnable_python_and_no_container_wrapper(package: zipfile.ZipFile) -> None:
|
||||
"""Detach point: put ``Dockerfile`` or ``azure.yaml`` back into HEAD → RED.
|
||||
|
||||
The 14.08 operator directive is that the deliverable is runnable Python. Removing the wrapper
|
||||
from the TREE (rather than filtering it out of the archive) is what makes that measurable here:
|
||||
the package is ``git archive HEAD``, so absence in the archive IS absence in what we ship.
|
||||
|
||||
Two controls, because a name-matcher that matches nothing would make this green forever:
|
||||
the run path a receiver actually needs must be present, and the matcher must be shown to match
|
||||
a member this archive really has."""
|
||||
names = package.namelist()
|
||||
|
||||
# Control 1 — the Python run path is what replaces the image. If these are missing, "no
|
||||
# container" would just mean "nothing to run".
|
||||
for member in ("main.py", "pyproject.toml", "uv.lock"):
|
||||
assert member in names, f"the runnable-Python path is incomplete: {member} is not packaged"
|
||||
|
||||
# Control 2 — the matcher matches by basename against THIS archive, proven on a member we know
|
||||
# is there. Without it, a matcher comparing full paths would find nothing and pass silently.
|
||||
assert _members_named(names, ("main.py",)) == ["main.py"], (
|
||||
"control: the member matcher found nothing for a member the archive demonstrably has"
|
||||
)
|
||||
|
||||
wrappers = _members_named(names, _CONTAINER_WRAPPER_NAMES)
|
||||
assert not wrappers, (
|
||||
f"the handover package ships a container/azd wrapper: {wrappers}. The delivery is runnable "
|
||||
"Python (operator directive 14.08); a wrapper here is a second, unmeasured way to start it."
|
||||
)
|
||||
|
||||
|
||||
def test_receiver_documents_start_the_service_as_a_python_process(package: zipfile.ZipFile) -> None:
|
||||
"""Detach point: tell the receiver to build an image again in either document → RED.
|
||||
|
||||
Both documents ride inside the archive, so both are the receiver's instructions. Each is checked
|
||||
LINE-ANCHORED with its own positive control first: a negative assertion on a document the
|
||||
extractor failed to read is a gate that can only be green."""
|
||||
deploy = package.read("DEPLOY.md").decode("utf-8").splitlines()
|
||||
readme = package.read("README.md").decode("utf-8").splitlines()
|
||||
|
||||
# Positive control on DEPLOY.md: with no image CMD left, this document carries the ONE copy of
|
||||
# the start command, and it must name the entry point the hosting tests actually exercise.
|
||||
assert [line for line in deploy if _PYTHON_START in line], (
|
||||
f"DEPLOY.md no longer prints the start command ({_PYTHON_START!r}) — the receiver has "
|
||||
"nothing to run, and the one copy of the start command is gone"
|
||||
)
|
||||
# Positive control on README.md: proves this document was read and searched at all.
|
||||
assert [line for line in readme if "uv sync" in line], (
|
||||
"control: README.md has no install line — the check below would be searching nothing"
|
||||
)
|
||||
|
||||
for label, lines in (("DEPLOY.md", deploy), ("README.md", readme)):
|
||||
offenders = [
|
||||
line for line in lines if any(fragment in line for fragment in _WRAPPER_COMMANDS)
|
||||
]
|
||||
assert not offenders, (
|
||||
f"{label} instructs a container/azd build step that this package no longer ships: "
|
||||
f"{offenders}"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue