Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/scripts/report-scheduled-failure.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail

LABEL="scheduled-failure"
TITLE="Scheduled dependency check failed"

# Ensure the label exists. --force makes this idempotent: creates if absent,
# updates color/description without error if present.
gh label create "$LABEL" \
--color "FBCA04" \
--description "Weekly dependency check failures" \
--force

# Find an open issue with our label, if any. --jq '.[0].number // empty'
# yields the first number or an empty string when there are no matches.
existing=$(gh issue list --label "$LABEL" --state open --json number --jq '.[0].number // empty')

if [ -z "$existing" ]; then
body=$(printf '%s\n\n%s\n\n%s\n\n%s' \
"The weekly scheduled dependency check failed." \
"First failing run: ${RUN_URL}" \
"Likely cause: a transitive dev or lint dependency (ruff, ty, eof-fixer, typing-extensions, types-PyYAML) released a breaking change. Reproduce locally with \`just install\` then \`just lint\` and \`just test\`. The integration and conformance jobs additionally need podman and docker." \
"Close this issue once fixed. The next scheduled failure will open a fresh issue.")
gh issue create --title "$TITLE" --label "$LABEL" --body "$body"
else
gh issue comment "$existing" --body "Failed again: ${RUN_URL}"
fi
28 changes: 28 additions & 0 deletions .github/workflows/scheduled.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: scheduled-dep-check
on:
schedule:
- cron: "0 6 * * 1" # Mondays 06:00 UTC
workflow_dispatch: {}

concurrency:
group: scheduled-dep-check
cancel-in-progress: false

jobs:
checks:
uses: ./.github/workflows/_checks.yml

report-failure:
needs: checks
if: failure() && github.event_name == 'schedule'
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v6
- name: Open or update tracking issue
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: bash .github/scripts/report-scheduled-failure.sh
6 changes: 3 additions & 3 deletions planning/changes/2026-07-09.08-service-key-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ to the key surface.
```python
@dataclass(frozen=True)
class KeySpec:
validate: Callable[[str, str, Any], None] # (service_name, key, value) -> raises
emit: Callable[[Any], list[Token]] # (value) -> tokens
validate: Callable[[str, str, Any], None] # (service_name, key, value) -> raises
emit: Callable[[Any], list[Token]] # (value) -> tokens
```

`SERVICE_KEYS: dict[str, KeySpec]` holds the ~16 declarative-flag keys (`user`,
Expand All @@ -55,7 +55,7 @@ one loop:
```python
for key, spec in SERVICE_KEYS.items():
if key in svc:
flags += spec.emit(svc[key]) # emit; or spec.validate(name, key, svc[key])
flags += spec.emit(svc[key]) # emit; or spec.validate(name, key, svc[key])
```

This retires `_add_declarative_flags`, the three `_add_*_flags` bespoke helpers,
Expand Down
32 changes: 21 additions & 11 deletions planning/changes/2026-07-10.03-configs.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ machinery, parameterized by a frozen `StoreKind`:
```python
@dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
class StoreKind:
label: str # "secret" | "config" — error wording
top_key: str # "secrets" | "configs"
prefix: str # "" | "config-" → store = f"{pod}-{prefix}{name}"
sources: frozenset[str] # {file,environment} | {file,environment,content}
label: str # "secret" | "config" — error wording
top_key: str # "secrets" | "configs"
prefix: str # "" | "config-" → store = f"{pod}-{prefix}{name}"
sources: frozenset[str] # {file,environment} | {file,environment,content}
default_target: Callable[[str], str] # secret: name ; config: "/"+name
require_absolute_target: bool # secret: False ; config: True
require_absolute_target: bool # secret: False ; config: True
```

It exposes kind-parameterized primitives (`validate_store`, `referenced_names`,
Expand All @@ -53,13 +53,23 @@ the union entry points that iterate a `kinds` tuple — emit's single seam:

```python
# secrets.py — prefix="" preserves the exact current store names
SECRET = StoreKind(label="secret", top_key="secrets", prefix="",
sources=frozenset({"file", "environment"}),
default_target=lambda n: n, require_absolute_target=False)
SECRET = StoreKind(
label="secret",
top_key="secrets",
prefix="",
sources=frozenset({"file", "environment"}),
default_target=lambda n: n,
require_absolute_target=False,
)
# configs.py
CONFIG = StoreKind(label="config", top_key="configs", prefix="config-",
sources=frozenset({"file", "environment", "content"}),
default_target=lambda n: f"/{n}", require_absolute_target=True)
CONFIG = StoreKind(
label="config",
top_key="configs",
prefix="config-",
sources=frozenset({"file", "environment", "content"}),
default_target=lambda n: f"/{n}",
require_absolute_target=True,
)
```

A small `STORE_KINDS = (SECRET, CONFIG)` aggregator (importing both specs — no
Expand Down
4 changes: 2 additions & 2 deletions planning/changes/2026-07-11.01-extends.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ filesystem access and no `--project-dir` dependency. Import direction: `cli` ->

```python
compose = _read_compose(text, args.format)
compose = resolve_extends(compose) # flatten inheritance
warnings = validate(compose) # never sees `extends`
compose = resolve_extends(compose) # flatten inheritance
warnings = validate(compose) # never sees `extends`
script = emit_script(compose=compose, options=options)
```

Expand Down
7 changes: 5 additions & 2 deletions planning/changes/2026-07-12.03-one-script-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,19 @@ Introduce a single internal traversal and make the two readers project from it:
```python
@dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
class PlannedScript:
script: str # finished: joined lines + trailing newline
variables: list[str] # finished: sorted, unique
script: str # finished: joined lines + trailing newline
variables: list[str] # finished: sorted, unique


def _plan(compose: dict[str, Any], options: EmitOptions) -> PlannedScript: ...


def emit_script(compose, options) -> str:
if not POD_NAME_PATTERN.fullmatch(options.pod):
raise UnsupportedComposeError(...)
return _plan(compose, options).script


def referenced_variables(compose, options) -> list[str]:
return _plan(compose, options).variables
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def _list(flag: str) -> KeySpec:
...
return KeySpec(validate=_validate_list, emit=emit, merge=_concat_list)


def _map(flag: str) -> KeySpec:
...
return KeySpec(validate=_validate_map, emit=emit, merge=_merge_map)
Expand All @@ -82,9 +83,9 @@ def _merge(base: dict[str, Any], local: dict[str, Any], name: str) -> dict[str,
spec = SERVICE_KEYS.get(key)
if key in base and spec is not None and spec.merge is not None:
merged[key] = spec.merge(name, key, base[key], local_val)
elif key in base and key in _MERGE_KEYS: # structural keys only
elif key in base and key in _MERGE_KEYS: # structural keys only
...
elif key in base and key in _CONCAT_KEYS: # structural keys only
elif key in base and key in _CONCAT_KEYS: # structural keys only
...
else:
merged[key] = local_val
Expand Down
2 changes: 1 addition & 1 deletion planning/changes/2026-07-13.09-uniform-flag-composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ accumulating into a local list and returning it:
```python
def _health_flags(healthcheck: dict[str, Any]) -> list[Token]:
flags: list[Token] = []
... # body unchanged
... # body unchanged
return flags
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ every site that reads an entry:

```python
def split_extra_host(entry: str) -> tuple[str, str]:
if "=" in entry: # Compose's documented separator
if "=" in entry: # Compose's documented separator
host, _sep, address = entry.partition("=")
else: # legacy 'host:ip'
else: # legacy 'host:ip'
host, _sep, address = entry.partition(":")
return host, address
```
Expand Down
10 changes: 6 additions & 4 deletions planning/changes/2026-07-16.01-quoted-boolean-coercion.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ strings; this field-level cast is what re-accepts them, matching Docker.
**Primitive (`values.py`)**, beside `has_variable`/`is_number`:

```python
_BOOL_TRUE = frozenset({"y","Y","yes","Yes","YES","true","True","TRUE","on","On","ON"})
_BOOL_FALSE = frozenset({"n","N","no","No","NO","false","False","FALSE","off","Off","OFF"})
_BOOL_TRUE = frozenset({"y", "Y", "yes", "Yes", "YES", "true", "True", "TRUE", "on", "On", "ON"})
_BOOL_FALSE = frozenset({"n", "N", "no", "No", "NO", "false", "False", "FALSE", "off", "Off", "OFF"})

def is_bool_like(value): # the accept-test every bool validator uses

def is_bool_like(value): # the accept-test every bool validator uses
return isinstance(value, bool) or (isinstance(value, str) and (value in _BOOL_TRUE or value in _BOOL_FALSE))

def as_bool(value): # coerce a validated bool-or-bool-string to a real bool

def as_bool(value): # coerce a validated bool-or-bool-string to a real bool
return value if isinstance(value, bool) else value in _BOOL_TRUE
```

Expand Down
13 changes: 11 additions & 2 deletions planning/changes/2026-07-16.03-healthcheck-duration-grammar.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,17 @@ component parser, beside a `unit → seconds` map:
_UNITS = "ns|us|µs|ms|s|m|h|d|w"
_INTERVAL_DURATION = re.compile(rf"^[+-]?(?:[0-9]+(?:\.[0-9]+)?(?:{_UNITS}))+$")
_DURATION_COMPONENT = re.compile(rf"([0-9]+(?:\.[0-9]+)?)({_UNITS})")
_UNIT_SECONDS = {"ns": 1e-9, "us": 1e-6, "µs": 1e-6, "ms": 1e-3,
"s": 1.0, "m": 60.0, "h": 3600.0, "d": 86400.0, "w": 604800.0}
_UNIT_SECONDS = {
"ns": 1e-9,
"us": 1e-6,
"µs": 1e-6,
"ms": 1e-3,
"s": 1.0,
"m": 60.0,
"h": 3600.0,
"d": 86400.0,
"w": 604800.0,
}
```

`interval_seconds`:
Expand Down
4 changes: 2 additions & 2 deletions planning/changes/2026-07-16.04-grammar-trailing-newline.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ probe (`" 512m "`) tests spaces, not newlines, so the class went uncaught.
regexes:

```python
_SIZE = re.compile(r"...\Z", re.VERBOSE) # was ...$
_SIZE = re.compile(r"...\Z", re.VERBOSE) # was ...$
_DURATION = re.compile(r"^[+-]?(?:[0-9]+(?:\.[0-9]+)?(?:ns|us|µs|ms|s|m|h))+\Z")
_STRICT_INT_STRING = re.compile(r"^[+-]?[0-9]+\Z")
_PORT = re.compile(r"...\Z", re.VERBOSE) # was ...$
_PORT = re.compile(r"...\Z", re.VERBOSE) # was ...$
```

`\Z` matches only the true end of the string, so a trailing `\n` no longer
Expand Down
4 changes: 3 additions & 1 deletion planning/changes/2026-07-17.01-tmpfs-into-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,23 @@ shares one uniform shape, not a structural-key registry).
**`keys.py`** — add a scalar-or-list factory and the registry entry:

```python
def _validate_string_or_string_list(name, key, value): # moved from parsing.py, None-guard dropped
def _validate_string_or_string_list(name, key, value): # moved from parsing.py, None-guard dropped
if not isinstance(value, str | list):
raise UnsupportedComposeError(f"service {name!r}: '{key}' must be a string or list")
if isinstance(value, list):
for entry in value:
if not isinstance(entry, str):
raise UnsupportedComposeError(f"service {name!r}: '{key}' entry must be a string")


def _scalar_or_list(flag):
def emit(value):
items = [value] if isinstance(value, str) else value
tokens = []
for item in items:
tokens += [flag, Expand(value=str(item))]
return tokens

return KeySpec(validate=_validate_string_or_string_list, emit=emit, merge=concat_list)
```

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ ignore = [
"D213", # "multi-line-summary-second-line" conflicting with D212
"COM812", # flake8-commas "Trailing comma missing"
"ISC001", # flake8-implicit-str-concat
"CPY001", # no per-file copyright header
]
isort.lines-after-imports = 2
isort.no-lines-before = ["standard-library", "local-folder"]
Expand Down
10 changes: 6 additions & 4 deletions tests/integration/test_pod_level_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ def test_pod_level_options_land_on_the_pod(run_pod: Callable[..., PodRun]) -> No
"command": [
"sh",
"-c",
"grep 9.9.9.9 /etc/resolv.conf "
"&& grep 1024 /proc/sys/net/core/somaxconn "
"&& grep 10.0.0.9 /etc/hosts "
"&& grep external-svc /etc/hosts",
(
"grep 9.9.9.9 /etc/resolv.conf "
"&& grep 1024 /proc/sys/net/core/somaxconn "
"&& grep 10.0.0.9 /etc/hosts "
"&& grep external-svc /etc/hosts"
),
],
},
},
Expand Down
Loading