Skip to content

Releases: modern-python/compose2pod

0.4.2

Choose a tag to compare

@github-actions github-actions released this 22 Jul 12:28
0.4.2
00b5db8

compose2pod 0.4.2 — a container can resolve its own hostname again

A second follow-up to the 0.4.0 owned-/etc/hosts work: restore resolution of
a container's own hostname inside the pod, which --no-hosts silently
dropped. Images whose startup resolves their own hostname — a KeyDB-sentinel
image, anything running hostname -f — no longer crash-loop.

Fix

  • The pod gets a resolvable hostname. 0.4.0 moved name resolution into a
    script-owned /etc/hosts under --no-hosts to kill the pre-6.0.0 stop-wipe
    bug — but --no-hosts also stops Podman from adding each container's own
    hostname line. The hostname is then the random container ID, absent from the
    owned file, so hostname -f fails with Temporary failure in name resolution and an image whose startup resolves its own hostname crash-loops
    and never goes healthy (a depends_on: service_healthy on it then times
    out). The generated script now passes podman pod create --uts private --hostname <pod-name> and adds 127.0.0.1 <pod-name> to the hosts file, so
    the hostname resolves. --uts private is required — in the host UTS
    namespace Podman refuses --hostname. Verified end-to-end on real Podman
    5.8.1 and 6.0.1.

Why

This is a straight regression from 0.1.8, which used no --no-hosts and let
Podman populate /etc/hosts — including the own-hostname line — so a
self-referential image worked. 0.4.0 owned the file to gain version-independent
resolution and non-root readability (0.4.1), but did not re-add the one line
Podman contributed beyond the alias set. A pod shares a single UTS namespace,
so per-service hostnames are physically impossible; the pod name is one shared,
resolvable hostname, which is all a hostname -f lookup needs (0.1.8 worked
with a random container-ID hostname, so the specific value never mattered).

Downstream

  • No action needed beyond re-running compose2pod at 0.4.2 (the fix is in
    the emitted script). Compose files with a self-referential image that hung on
    0.4.0/0.4.1 now start. Per-service hostname: keys behave exactly as before:
    resolvable by peers via /etc/hosts, not set as a container's own name.

Internals

  • Tests at 100% line coverage (enforced); ruff select=ALL, ty, eof-fixer
    clean; conformance (real Docker CLI) and integration (real Podman) harnesses
    green. Reproduced on Podman 5.8.1 via quay.io/podman/stable:v5.8.1 and on
    6.0.1: a service whose healthcheck is hostname -f goes healthy, and a peer
    resolves its own hostname. Confirmed in passing that the JSON-array
    --health-cmd compose2pod emits for a ["CMD", ...] healthcheck is accepted
    by Podman 5.8.1 — that rendering was never the bug.

0.4.1

Choose a tag to compare

@github-actions github-actions released this 22 Jul 11:43
0.4.1
24cd8d5

compose2pod 0.4.1 — non-root images can read the owned /etc/hosts

A focused fix for the owned-/etc/hosts mechanism 0.4.0 introduced: the
generated hosts file was created 0600, so a service image that runs as a
non-root user could not read it, and pod-internal name resolution silently
failed. This restores resolution for every image regardless of its USER.

Fix

  • World-readable owned /etc/hosts. The script writes the hosts file to a
    mktemp path, which mktemp creates mode 0600, owned by the invoking user.
    Bind-mounted read-only as /etc/hosts into every container, a process running
    as a non-root user could not read it — glibc then skips /etc/hosts, falls
    through to DNS, and every lookup fails with could not translate host name "<svc>": Temporary failure in name resolution, deep inside an otherwise
    healthy service. The script now chmod 644s the file right after writing it
    and before the first podman run mounts it. The file holds only
    hostname → 127.0.0.1 and extra_hosts (hostname → ip) lines — nothing
    secret — so world-readable is correct; umask cannot substitute, as mktemp
    forces 0600. Reproduced and fixed against real Podman 5.8.1: a non-root pod
    member goes from /etc/hosts: Permission denied to resolving its peers.

Why

0.4.0 traded Podman's managed /etc/hosts (world-readable 0644) for a
script-owned file, closing the pre-6.0.0 stop-wipe bug — but the mktemp
default mode reintroduced a resolution failure on the axis the earlier work
never exercised: every 0.4.0 validation ran its containers as root. Any image
with a USER directive (the common case in CI) hit the regression on first
run.

Downstream

  • No action needed. Regenerate nothing beyond re-running compose2pod at
    0.4.1 (the fix is in the emitted script, not the compose input). Compose files
    that lost name resolution under 0.4.0 on non-root images now work unchanged.
    Root-user images were never affected and are unchanged.

Internals

  • Tests at 100% line coverage (enforced); ruff select=ALL, ty, eof-fixer
    clean; conformance (real Docker CLI) and integration (real Podman) harnesses
    green. The fix was reproduced end-to-end on Podman 5.8.1 via
    quay.io/podman/stable:v5.8.1: without the chmod a --user 1000 member
    fails to read /etc/hosts; with it, it resolves db → 127.0.0.1.
  • require_string_keys returns the validated keys as set[str]. ty 0.0.62
    tightened sorted()'s bound, rejecting the 11 sorted(set(mapping) - KNOWN)
    unsupported-key error paths (an untyped mapping infers set[object], which is
    not sortable). The shared key-string check now returns the keys it validated,
    so each check consumes a statically sortable set[str] and drops a redundant
    re-iteration. No behavior change.

0.4.0

Choose a tag to compare

@github-actions github-actions released this 21 Jul 10:52
0.4.0
3961b7d

compose2pod 0.4.0 — Podman 5.x support: the script owns /etc/hosts

Generated scripts now own /etc/hosts outright instead of asking Podman to
populate it with pod-level --add-host. Every container runs under --no-hosts
and bind-mounts a hosts file compose2pod writes at run time. Because Podman no
longer manages the file, the pre-6.0.0 bug that wiped it can no longer fire —
pod-internal name resolution is now correct on every Podman version,
including the 5.x line that has no fix for that bug. The Podman >= 6.0.0
requirement is gone.

Why

Before 6.0.0, a container stopping inside a multi-container pod made Podman
regenerate the pod's shared /etc/hosts and delete every entry — wiping the
--add-host set that was the pod's only source of name resolution. A
service_completed_successfully dependency (a migration that runs and exits)
triggered it, producing a could not translate host name failure deep inside an
unrelated service, minutes after the script appeared to succeed.

The fix (containers/podman#28494)
first shipped in Podman 6.0.0 and was never backported to any 5.x — verified
against the v5.8 release notes and tag containment for 5.8.0–5.8.5. Anyone
pinned to a Fedora-based Podman 5.x image could not get a fixed Podman, and the
0.3.0 run-time version guard could only warn, not fix. This release removes the
cause instead of warning about it.

Change

  • The script owns /etc/hosts. podman pod create and every podman run
    now carry --no-hosts; no --add-host is emitted. The script writes a full
    hosts file to a mktemp path — 127.0.0.1 localhost, ::1 localhost, each
    service alias/hostname → 127.0.0.1, each extra_hosts host → its address —
    and bind-mounts it read-only into every container as
    -v "$hostsfile":/etc/hosts:ro,z, removing it on teardown. The z relabel is
    mandatory on SELinux-enforcing hosts (Fedora) and a no-op elsewhere.
  • Works on any Podman. With --no-hosts, Podman never rewrites the file, so
    the stop-wipe cannot occur regardless of version. Behavior is identical on 5.x
    and 6.x.
  • The Podman-version guard is removed. The run-time warning 0.3.0 added
    existed only to flag the now-impossible bug, so it is deleted along with the
    documented version floor.

Downstream

  • No pre-editing needed on Podman 5.x. Compose files with a run-to-completion
    dependency that previously lost name resolution now work unchanged.
  • host.containers.internal / host.docker.internal are no longer provided.
    Under --no-hosts, compose2pod cannot resolve Podman's gateway IP to synthesize
    them (Docker Compose on Linux does not add them by default either). A service
    that needs to reach the host adds an explicit extra_hosts entry with the
    address it wants.
  • Everything else is unchanged. The alias/hostname resolution set, the
    extra_hosts merge, and the conflict rule (a name landing on two addresses is
    refused) behave exactly as before — only the mechanism that carries them to the
    container changed.

Internals

  • Tests at 100% line coverage (enforced); ruff select=ALL, ty, eof-fixer
    clean; the conformance harness (real Docker CLI) and integration harness (real
    Podman) both green — the integration suite exercises name resolution surviving a
    completion container's stop.
  • The mechanism was validated end-to-end in a Fedora CoreOS VM before it was
    written: with --no-hosts and the bind-mounted file, a container's stop leaves
    the hosts file byte-identical, and the missing :z relabel was caught there
    (an un-relabeled mount is unreadable inside the container). See
    planning/changes/2026-07-20.01-own-etc-hosts-via-no-hosts.md for the full
    rationale, the upstream research, and the rejected alternatives.

0.3.1

Choose a tag to compare

@github-actions github-actions released this 17 Jul 13:07
0339e8a

compose2pod 0.3.1 — the image mount type

A focused follow-up to 0.3.0: the long-form volumes type: image mount, which
closes the last long-form volume form Podman can express but compose2pod did not
yet parse.

Feature

  • type: image volume mounts. A long-form entry {type: image, source, target, read_only, image: {subpath}} compiles to podman run --mount type=image,source=<ref>,target=<path>[,subpath=<abs>], mounting an image's
    rootfs into the container. Measured against docker compose config v5.1.2 and
    Podman 6.0.1:
    • source (the image reference) is required — Podman refuses a sourceless
      image mount, so compose2pod does too (a rule-two narrowing; Docker allows it
      optional at config).
    • read_only is accepted but inert — an image mount is always read-only,
      and Podman rejects both a readonly and an rw option on it, so no ro is
      emitted (matching what Docker does with the field).
    • image: {subpath} must be an absolute path — Podman requires it for an
      image mount (a relative subpath is rejected at run time), unlike a volume
      subpath, which may be relative. A ${VAR} is carved out.

Docs

  • The README's supported-subset section, stale since before the 0.3.0 parity
    work, now leads with the measured parity property (compose2pod refuses every
    document docker compose config refuses, verified by the differential
    conformance harness) and reflects the full 0.3.0 subset.

Downstream

  • type: image compose files that previously raised now compile. Nothing else
    changes — the short form, the other long-form types, and every existing key
    behave identically.
  • With image landed, the honest subset is complete for its scope: every
    remaining refusal of a form Docker accepts is either a permanent rule-two limit
    (Podman cannot form the flag) or a genuinely niche gap (a Windows drive-letter
    volume source on a Linux-targeting tool), each measured and documented in
    architecture/supported-subset.md, planning/deferred.md, and
    planning/decisions/.

Internals

  • Tests at 100% line coverage (enforced); ruff select=ALL, ty, eof-fixer
    clean; the conformance harness (real Docker CLI) and integration harness (real
    Podman) both green — the integration suite mounts an image's rootfs on real
    Podman, and the final review re-measured every emitted --mount option against
    Podman 6.0.1, catching (and closing) a relative-subpath value Podman rejects
    at run time.

0.3.0

Choose a tag to compare

@github-actions github-actions released this 17 Jul 11:43
b80f0b6

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/Zrelabel=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).

0.2.0

Choose a tag to compare

@github-actions github-actions released this 13 Jul 11:05
1fcb527

compose2pod 0.2.0 — most of the common compose subset, and a real-podman test harness

This release rounds out the per-container and pod-level keys most compose
files actually use — extends, secrets/configs, resource limits,
ulimits, and pod-wide dns/sysctls/extra_hosts — and adds a CI-only
integration harness that runs generated scripts against real Podman. That
harness caught a real bug on its first run, fixed in this same release: see
Fix, below.

Fix

  • --add-host moved from podman run to podman pod create. Current
    Podman rejects --add-host on a container joining a pod ("network cannot
    be configured when it is shared with a pod") — so any generated script for
    a pod with more than one named service failed at run time. All
    --add-host emission (service-name/hostname/container_name/network
    aliases, and extra_hosts) now lands on the single podman pod create
    line instead, alongside dns/sysctls. extra_hosts is consequently now
    genuinely pod-wide (previously per-service); a host name landing on two
    different addresses — across services' extra_hosts, or against an
    alias's fixed 127.0.0.1 — is refused (UnsupportedComposeError) rather
    than resolved by guessing, matching the existing sysctls conflict rule.
  • validate()/emit_script() reject malformed input cleanly instead of
    crashing.
    A malformed depends_on, healthcheck, hostname/
    container_name, tmpfs, or per-service networks shape now raises
    UnsupportedComposeError at the validation/emit boundary instead of an
    uncaught AttributeError/ValueError deeper in the pipeline.
  • emit_script validates the pod name itself, not only the CLI — an
    adversarial EmitOptions.pod (quotes, spaces, $(...)) now raises
    cleanly instead of producing a malformed shell trap.

Feature

  • extends. Same-file Compose extends: {service: <name>} is resolved
    before validation (transitive, cycle-checked, per-key merge — concat for
    list keys like cap_add/volumes, local-wins merge for environment/
    labels/healthcheck, override for scalars and command/entrypoint).
    Cross-file extends: {file: ...} stays refused, keeping the
    single-document input model.
  • secrets and configs. Top-level secrets:/configs: (file:,
    environment:, and — configs only — inline content: sources) compile to
    podman secret create, mounted via --secret at /run/secrets/<name>
    (secrets) or the container root /<name> (configs, absolute target:
    only). external: true is refused; a secret and same-named config don't
    collide (distinct store names).
  • Resource limits. Both the legacy scalar keys (mem_limit, cpus,
    pids_limit, cpu_shares, cpuset, shm_size, oom_score_adj,
    oom_kill_disable, ...) and the modern deploy.resources.limits/
    .reservations block map onto --memory/--cpus/--pids-limit/
    --memory-reservation. A legacy key and its deploy.resources counterpart
    targeting the same flag is refused rather than picking an undefined
    precedence.
  • ulimits. A mapping of limit name to a scalar (nproc: 65535) or a
    {soft, hard} pair, emitted as --ulimit.
  • Pod-level dns/sysctls/extra_hosts. These apply to every
    container in the pod (one shared /etc/resolv.conf, sysctl set, and
    /etc/hosts) — dns/dns_search/dns_opt are unioned across the
    target's dependency closure; sysctls union by key, refusing a same-key
    conflict; extra_hosts merges the same way (see Fix, above). validate()
    warns whenever any service in the document declares one of these, even
    outside the target's closure.
  • Core process, confinement, and metadata keys: entrypoint, user,
    working_dir, group_add, labels, read_only, init, privileged,
    cap_add, cap_drop, security_opt, platform, devices,
    annotations, pull_policy. Each is an honest, validated passthrough to
    its podman run flag.
  • profiles and stop_signal/stop_grace_period are now accepted and
    ignored (with a warning) rather than rejected — compose2pod's run set is
    always --target plus its depends_on closure, and the generated script
    always force-removes the pod, so neither key changes anything it can act
    on. A target depends_on reaching a service outside its declared profile
    still runs it (the closure is authoritative — more permissive than
    Compose, never a silent drop).

Packaging

  • resolve_extends is exported from compose2pod.__all__, alongside
    the existing EmitOptions, UnsupportedComposeError, emit_script,
    to_shell, and validate.

Why

Prior releases built the core single-pod model and its interpolation
semantics; this one rounds out the compose keys real-world compose files
actually use, so fewer documents need pre-editing before they run.
Discovering the --add-host regression only on a real Podman run — not from
any of the 363 existing unit tests, all of which assert on generated
script text — is exactly why the integration harness (see Internals) was
worth building: some regressions are only visible to the runtime it targets.

Downstream

  • Compose files using extra_hosts for a service-specific override: it
    is now pod-wide. A host name your extra_hosts maps to one address must
    not conflict with another service's mapping, or with an alias's fixed
    127.0.0.1 — compose2pod now refuses rather than silently picking one.
  • Any pod that previously failed with network cannot be configured when it is shared with a pod now works without changes to your compose file.
  • API consumers: compose2pod.resolve_extends(compose) is now
    available for flattening extends outside the CLI pipeline.

Internals

  • 363 tests at 100% line coverage (enforced); ruff select=ALL, ty, and
    eof-fixer clean.
  • A new CI-only integration harness (tests/integration/, 10 scenarios)
    renders real compose documents and runs the generated script against real
    Podman on ubuntu-latest, asserting on exit code and output — it caught
    the --add-host regression above on its first run. Kept out of the
    100%-coverage gate and the fast suite via a location-based marker.
  • compose2pod/keys.py's SERVICE_KEYS registry unifies what was
    previously three hand-synced tables (validation, emission, and the
    supported-key set) into one (validate, emit) pair per key.

0.1.8

Choose a tag to compare

@github-actions github-actions released this 09 Jul 05:08
ca24f96

compose2pod 0.1.8 — interpolation moves to run time

This release reverses 0.1.7: Compose ${VAR} interpolation now happens when
the generated script runs, not when compose2pod generates it. compose2pod
emits a script that runs later — typically in CI, where the real secrets and
config live — so resolving against compose2pod's own environment at generation
time baked any variable that was unset then to an empty string. References are
now emitted as live POSIX-shell fragments that the runtime shell expands.

Change

  • ${VAR} is interpolated at script-run time, not generation time.
    to_shell() (new compose2pod/shell.py) re-encodes every compose-derived
    value (environment, image, command, volumes, tmpfs, env_file,
    healthcheck test) into a double-quoted shell fragment whose variable
    references stay live, so the generated script's own shell resolves them
    against its runtime environment. All the 0.1.7 forms still work — $VAR,
    ${VAR}, ${VAR:-default}, ${VAR-default}, ${VAR:?msg}, ${VAR?msg},
    ${VAR:+alt}, ${VAR+alt}, $$ — mapping onto identical POSIX sh
    parameter expansion. A bare $VAR/${VAR} is emitted as ${VAR-} so an
    unset variable expands to empty under the script's set -eu instead of
    aborting; ${VAR:?msg} now fails the script at run time (not at generation).
    Command substitution and other shell metacharacters in literal text are
    escaped, so nothing but a variable reference is ever live. The CLI prints one
    informational note listing the variables the script references at run time.

Fix

  • environment null value is host passthrough. A null mapping value
    (KEY:) now emits a bare -e KEY — passing KEY through from the host,
    identical to the list form - KEY — instead of the literal -e KEY=None.
  • Malformed braced references are rejected. A braced reference whose text
    after the name is not a valid operator (e.g. ${FOO!bar}) now raises a clear
    UnsupportedComposeError instead of silently dropping the trailing text.

Packaging

  • Public API: the generation-time interpolate function is removed;
    to_shell is exported in its place. to_shell(value) returns a
    runtime-expandable shell fragment, not a resolved value.

Why

The 0.1.7 model resolved ${VAR} against os.environ at generation time. For
compose2pod's actual use — generate a script now, run it later in CI — that was
the wrong moment: the variables are supposed to be unset at generation and set
at run time, so 0.1.7 baked them to empty and emitted a spurious "variable not
set" warning. Deferring to the runtime shell resolves them where they exist.

Downstream

  • Compose files: set ${VAR} values in the runtime environment of the
    generated script (e.g. your CI job), not in the environment that runs
    compose2pod. An unset bare ${VAR} now expands to empty at run time; write
    ${VAR:?message} where you want a missing variable to fail the run.
  • API consumers: compose2pod.interpolate is gone. Use
    compose2pod.to_shell(value), which emits a runtime-expandable shell fragment
    rather than resolving the value in-process.

Internals

  • architecture/supported-subset.md rewritten to describe run-time expansion,
    the ${VAR-}/$$/${VAR:?} semantics, env_file interpolation, the
    reference note, null-value passthrough, and malformed-reference rejection.
  • 123 tests at 100% line coverage (enforced); ruff select=ALL, ty, and
    eof-fixer clean. test_shell.py executes emitted fragments under real
    sh -euc to prove run-time expansion, the ${VAR:?} abort, and
    command-substitution inertness.

0.1.7

Choose a tag to compare

@github-actions github-actions released this 08 Jul 19:57
869ba8b

compose2pod 0.1.7 — variable interpolation

A patch release resolving Compose-spec ${VAR} variable interpolation
against the process environment. Previously, a compose file using ${VAR}
reached the container as the literal placeholder text — shlex.quote
correctly single-quoted the value for safe script embedding, which also
meant the shell never expanded it, so nothing in the pipeline ever
substituted the real value. Compose files that lean on host/CI environment
values now convert and run correctly, matching docker compose.

Fix

  • ${VAR}-style interpolation is resolved. Every string leaf of the
    compose document is checked against os.environ before validation: $VAR,
    ${VAR}, ${VAR:-default}, ${VAR-default}, ${VAR:?msg}, ${VAR?msg},
    ${VAR:+alt}, ${VAR+alt}, and $$ (literal $) are all supported. An
    unset $VAR/${VAR} with no default resolves to an empty string and
    prints a warning rather than failing; ${VAR:?msg}/${VAR?msg} raises a
    clear error instead of silently defaulting.

Downstream

No action needed — additive and backward compatible. A document with no
$-prefixed values is unaffected; one that already relied on the previous
literal pass-through (unlikely, since it's not useful behavior) would now see
the resolved value instead.

Internals

  • architecture/supported-subset.md documents the supported interpolation
    forms and the explicit non-goal: no .env file loading, only the caller's
    existing environment is consulted.
  • 123 tests at 100% line coverage (enforced); ruff select=ALL, ty, and
    eof-fixer clean.

0.1.6

Choose a tag to compare

@github-actions github-actions released this 08 Jul 19:36
392c22f

compose2pod 0.1.6 — accept the service tmpfs key

A patch release that stops rejecting valid compose documents which set a
service tmpfs. The validator was over-strict: a service carrying tmpfs:
a string or list of <path>[:<options>] entries, e.g. /tmp:mode=1777
raised unsupported key 'tmpfs'. Such documents now convert.

Fix

  • The service tmpfs key is accepted. validate() no longer rejects a
    service that sets tmpfs:. Each entry is emitted verbatim as podman run --tmpfs <value> — Compose's short syntax maps directly onto podman's own
    --tmpfs CONTAINER-DIR[:OPTIONS] flag, so no translation is needed. No
    format validation; a malformed option string surfaces as a podman error at
    run time.

Downstream

No action needed — additive and backward compatible. Documents that previously
failed with unsupported key 'tmpfs' now emit a pod script; nothing that
converted before changes.

Internals

  • architecture/supported-subset.md documents tmpfs in the Service keys
    matrix.
  • 98 tests at 100% line coverage (enforced); ruff select=ALL, ty, and
    eof-fixer clean.

0.1.5

Choose a tag to compare

@github-actions github-actions released this 08 Jul 15:16
744a484

compose2pod 0.1.5 — named volumes

A patch release accepting Compose named volumes, previously a hard error.
Compose files that declare a top-level volumes: block or reference a named
volume in a service (e.g. pgdata:/var/lib/postgresql/data for database data
persistence) now convert without editing the compose file.

Fix

  • Named volumes are accepted. A service volume whose source is a bare
    identifier rather than a host path (a Compose named volume) is emitted
    verbatim as -v <name>:<target>. Podman creates the volume implicitly on
    first reference — the same behavior as plain podman run -v — so no
    explicit podman volume create step is needed. The volume persists on the
    host after the pod is removed, identical to docker compose down without
    -v.
  • Top-level volumes: is accepted (ignored). Mirrors the existing
    top-level networks treatment: the block's driver/driver_opts and
    external settings are never read, since implicit creation with default
    options is what podman does regardless. A non-default driver or an
    external: true volume that should pre-exist has no special handling.

Downstream

No action needed — additive and backward compatible. Documents that previously
failed with unsupported top-level keys: ['volumes'] or "named volume ... is
not supported" now emit a pod script; nothing that converted before changes.

Internals

  • architecture/supported-subset.md documents the bind-mount vs named-volume
    disambiguation (source starts with .// vs bare identifier) and the
    default-driver-only limitation.
  • 95 tests at 100% line coverage (enforced); ruff select=ALL, ty, and
    eof-fixer clean.