From 587a1aa74ed597aba539d41175beb39e291e6ec4 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Fri, 17 Jul 2026 13:44:06 +0300 Subject: [PATCH] docs: curate 0.3.0 release notes --- planning/releases/0.3.0.md | 114 +++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 planning/releases/0.3.0.md diff --git a/planning/releases/0.3.0.md b/planning/releases/0.3.0.md new file mode 100644 index 0000000..d0cb599 --- /dev/null +++ b/planning/releases/0.3.0.md @@ -0,0 +1,114 @@ +# compose2pod 0.3.0 — measured parity with Docker, and a much wider accepted subset + +This release makes the subset's boundary a *measured* one. compose2pod now +refuses every document `docker compose config` refuses — verified continuously +by a differential conformance harness that runs both the real Docker CLI and the +real compose2pod pipeline over the same YAML and asserts the rule. On top of that +hard floor, the accepted subset grew substantially: quoted booleans, `env_file` +and `volumes` long-form (with `--mount`), the full Compose duration grammar, and +the volume mount option maps. Where compose2pod still refuses a form Docker +accepts, that refusal is now measured, documented, and — for the permanent ones — +recorded as a decision with the exact Podman limitation behind it. + +## The parity thesis + +The governing rule (`decisions/2026-07-14-docker-rejection-parity.md`): +**`accepted(compose2pod) ⊆ accepted(docker)`** — compose2pod must reject every +document Docker rejects, so a file that compiles is a file Docker would run. + +- **Reject every document `docker compose config` refuses.** The value grammars + (`size`, `number`, `integer`, `count`, `duration`, `port`) now match Docker's + own — `mem_limit: ""`, `cpus: somevalue`, a unitless healthcheck duration, an + out-of-range port, a whitespace-padded scalar, all refused where Docker refuses + them, instead of being silently accepted and compiled into a script Docker + itself would not run. +- **Read YAML the way Docker reads it (1.2).** A bare `on`/`off`/`yes`/`no` stays + an ordinary string (not a boolean), and a bare `1e3` is the float `1000.0` — + matching Docker's YAML-1.2 parser, so `SSL: on` reaches the container as + `SSL=on` and an `on:` key no longer resolves to `True`. +- **Match Docker's null-value policy.** A bare `key:` (null) is refused exactly + where Docker refuses one, and treated as "unset" exactly where Docker does. +- **Close the structural-key and reference gates.** Non-string mapping keys, + malformed nested shapes, undefined named-volume/network references, and + strict long-form schemas (`build`, `depends_on`, `networks`, `volumes`, + `secrets`/`configs` refs) are validated to Docker's own grammar. +- **A differential conformance harness** (`tests/conformance/`, CI-only) probes + every registry key × every hostile shape plus a hand-authored corpus, running + Docker and compose2pod over identical YAML. A document Docker rejects that + compose2pod accepts fails the build; the reverse (an over-rejection) is + reported and catalogued, never hidden. + +## Feature — a wider accepted subset + +- **Quoted booleans on every boolean field.** `read_only: "yes"`, `tty: "true"`, + `init: "on"`, and the same on `build`, network/volume definitions, and + `depends_on` — the YAML-1.1 boolean spellings Docker casts a string field + through — are now accepted, via one shared `values.is_bool_like`/`as_bool` + seam, coercing before emit so a quoted `"false"` never leaks a set flag. +- **`env_file` long-form.** `env_file: [{path, required, format}]` is accepted; + `required: false` is honored with a run-time `[ -f path ]` guard so an absent + optional file is skipped, `format: raw` is accepted, and `path` resolves as + before. +- **`volumes` long-form (`--mount`).** The mapping entry + `{type, source, target, read_only, consistency}` for `type` in + `bind`/`volume`/`tmpfs` compiles to `podman run --mount`, and the nested option + maps — `bind: {propagation, selinux}`, `volume: {subpath}`, + `tmpfs: {size, mode}` — map to the corresponding `--mount` options (`selinux` + `z`/`Z` → `relabel=shared`/`private`). +- **The full Compose duration grammar for healthcheck `interval`.** Compound and + larger units — `1h`, `1h30m`, `1d`, `1w`, `1.5d`, `-1h` — are accepted (the + interval paces the polling loop; Podman never sees it), overflow- and + whitespace-safe. +- **Podman-version guard.** Generated scripts warn at run time when Podman is + below the version with the `/etc/hosts` pod-wide fix, so a silent name- + resolution failure surfaces as an explicit warning. + +## Fix + +- **`--add-host` scoped to the target's dependency closure** — a host entry for a + service outside the run set no longer lands on the pod-create line. +- **Two hard-rule false greens, caught by the differential harness's own + adversarial review and closed:** a trailing newline slipping past a `$`-anchored + value grammar (now `\Z`, reachable via a YAML block scalar), and a padded / + negative / float value on the `tmpfs` mount sub-schema (Docker validates it as + unsigned; Podman's `crun` rejects a float mode). + +## Internals + +- **Registry unification.** `environment` and `tmpfs` moved into the + `SERVICE_KEYS` registry (one validate + emit + merge spec per key); the compose + reader (the YAML-1.2 SafeLoader and format dispatch) was extracted into + `read.py` with its own test surface. `volumes` was deliberately left + hand-rolled — recorded as a decision (its emit needs `project_dir`, its + references are document-level). +- **A decisions log** now records the rulings that shape the boundary — the + docker-rejection-parity rule, the `list-of-str` (`sysctls`/`volumes: ["a"]`) + legitimate refusals, the `volumes`-stays-hand-rolled call, and the + measured-negative-numerics finding — each with the exact Docker/Podman behavior + and a revisit trigger, so none is re-litigated. +- Tests at 100% line coverage (enforced); `ruff select=ALL`, `ty`, `eof-fixer` + clean; the CI-only integration harness (real Podman) and conformance harness + (real Docker CLI) both green. + +## Why + +0.2.0 rounded out the common compose keys. This release answers a sharper +question — *which documents does compose2pod's "yes" actually mean?* — with a +measured answer: exactly the ones Docker would run, minus a small, catalogued set +of forms Podman genuinely cannot express. The differential harness makes that a +property the build enforces, not a claim in the README, and it repeatedly caught +false greens that unit tests (which assert on generated *text*) could not. + +## Downstream + +- **Stricter where Docker is strict.** A document that previously compiled but + that `docker compose config` rejects — a native number on a duration field, a + malformed size, a null where Docker refuses one, a bare `on`/`yes` you relied on + being a boolean — now raises `UnsupportedComposeError`. This is the point: the + script you get is one Docker would run. +- **Wider where Podman can express it.** Quoted booleans, `env_file`/`volumes` + long-form, and compound durations mean fewer documents need pre-editing. +- **The honest boundary.** Three forms Docker accepts stay refused, each measured + and documented: the `image` mount type and a Windows drive-letter source + (`planning/deferred.md`, genuine parser gaps), and `sysctls`/`volumes: ["a"]` + (`decisions/`, permanent — Podman cannot form the flag).