docker-devkit 0.2.1.dev37490360141__tar.gz → 0.3.0.dev113__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/.github/actions/setup-release-devkit/action.yml +1 -1
  2. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/.github/workflows/integrate.yml +21 -0
  3. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/.github/workflows/merge-gate.yml +2 -3
  4. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/.github/workflows/release.yml +2 -3
  5. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/AGENTS.md +7 -7
  6. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/PKG-INFO +2 -2
  7. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/pyproject.toml +3 -4
  8. docker_devkit-0.3.0.dev113/release-devkit.yaml +6 -0
  9. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/src/docker_devkit/build_docker.py +51 -53
  10. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/src/docker_devkit/matrix.py +31 -14
  11. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/tests/test_build_docker.py +16 -18
  12. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/tests/test_matrix.py +57 -6
  13. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/uv.lock +10 -11
  14. docker_devkit-0.2.1.dev37490360141/release-devkit.yaml +0 -7
  15. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/.gitattributes +0 -0
  16. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/.gitignore +0 -0
  17. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/.python-version +0 -0
  18. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/CLAUDE.md +0 -0
  19. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/LICENSE +0 -0
  20. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/NOTICE +0 -0
  21. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/README.md +0 -0
  22. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/ruff.base.toml +0 -0
  23. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/ruff.toml +0 -0
  24. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/src/docker_devkit/__init__.py +0 -0
  25. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/src/docker_devkit/config.py +0 -0
  26. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/src/docker_devkit/context_sha.py +0 -0
  27. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/src/docker_devkit/detect_gpu.py +0 -0
  28. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/src/docker_devkit/documents.py +0 -0
  29. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/src/docker_devkit/down.py +0 -0
  30. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/src/docker_devkit/image_refs.py +0 -0
  31. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/src/docker_devkit/lifecycle.py +0 -0
  32. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/src/docker_devkit/mirror.py +0 -0
  33. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/src/docker_devkit/modes.py +0 -0
  34. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/src/docker_devkit/py.typed +0 -0
  35. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/src/docker_devkit/score_generate.py +0 -0
  36. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/src/docker_devkit/up.py +0 -0
  37. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/tests/test_config.py +0 -0
  38. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/tests/test_context_sha.py +0 -0
  39. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/tests/test_default_targets.py +0 -0
  40. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/tests/test_documents.py +0 -0
  41. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/tests/test_image_refs.py +0 -0
  42. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/tests/test_lifecycle.py +0 -0
  43. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/tests/test_mirror.py +0 -0
  44. {docker_devkit-0.2.1.dev37490360141 → docker_devkit-0.3.0.dev113}/tests/test_score_generate.py +0 -0
@@ -5,7 +5,7 @@ runs:
5
5
  steps:
6
6
  - shell: bash
7
7
  env:
8
- RELEASE_DEVKIT_COMMIT: ed406a0d88445dd601b7cbe47decbb6371694e3a
8
+ RELEASE_DEVKIT_COMMIT: 84afa7a39ba21ab8971014dbefe130e3b9df8ded
9
9
  run: |
10
10
  git clone https://github.com/outernet-foundation/release-devkit.git "$RUNNER_TEMP/release-devkit"
11
11
  git -C "$RUNNER_TEMP/release-devkit" checkout "$RELEASE_DEVKIT_COMMIT"
@@ -54,3 +54,24 @@ jobs:
54
54
  - *uv-restore
55
55
  - *setup-release-devkit
56
56
  - run: uv run --project "$RUNNER_TEMP/release-devkit" --locked --no-dev validate-release-plan
57
+
58
+ update-pr-draft-release:
59
+ if: github.event.pull_request
60
+ needs: [validate-release-plan]
61
+ permissions:
62
+ contents: write
63
+ packages: read
64
+ runs-on: ubuntu-latest
65
+ steps:
66
+ - uses: actions/checkout@v5
67
+ with:
68
+ ref: ${{ github.event.pull_request.head.sha }}
69
+ # Perform a full clone, required for tags
70
+ fetch-depth: 0
71
+ fetch-tags: true
72
+ persist-credentials: false
73
+ - *uv-restore
74
+ - *setup-release-devkit
75
+ - run: uv run --project "$RUNNER_TEMP/release-devkit" --locked --no-dev release --channel pr
76
+ env:
77
+ GITHUB_TOKEN: ${{ github.token }}
@@ -22,7 +22,7 @@ jobs:
22
22
  - uses: actions/checkout@v5
23
23
  with:
24
24
  ref: ${{ github.event.pull_request.head.sha || github.event.workflow_run.head_sha }}
25
- # Perform a full clone, required so merge-gate can validate that this PR is a fast forward from dev
25
+ # Perform a full clone, required so merge-gate can validate that this PR is a semi-linear merge into dev
26
26
  fetch-depth: 0
27
27
  persist-credentials: false
28
28
  - uses: astral-sh/setup-uv@v7
@@ -35,7 +35,6 @@ jobs:
35
35
  with:
36
36
  app-id: ${{ vars.MERGE_BOT_APP_ID }}
37
37
  private-key: ${{ secrets.MERGE_BOT_APP_PRIVATE_KEY }}
38
- - run: uv run --project "$RUNNER_TEMP/release-devkit" --locked --no-dev merge-gate
38
+ - run: uv run --project "$RUNNER_TEMP/release-devkit" --locked --no-dev merge-gate --head-sha ${{ github.event.pull_request.head.sha || github.event.workflow_run.head_sha }}
39
39
  env:
40
40
  GITHUB_TOKEN: ${{ steps.mint.outputs.token }}
41
- HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.workflow_run.head_sha }}
@@ -18,7 +18,6 @@ jobs:
18
18
  contents: write
19
19
  id-token: write
20
20
  packages: read
21
- actions: read
22
21
  steps:
23
22
  - uses: actions/checkout@v5
24
23
  with:
@@ -33,7 +32,7 @@ jobs:
33
32
  save-cache: "false"
34
33
  - &setup-release-devkit
35
34
  uses: ./.github/actions/setup-release-devkit
36
- - run: uv run --project "$RUNNER_TEMP/release-devkit" --locked --no-dev prerelease
35
+ - run: uv run --project "$RUNNER_TEMP/release-devkit" --locked --no-dev release --channel dev
37
36
  env:
38
37
  GITHUB_TOKEN: ${{ github.token }}
39
38
 
@@ -53,6 +52,6 @@ jobs:
53
52
  persist-credentials: true
54
53
  - *uv-restore
55
54
  - *setup-release-devkit
56
- - run: uv run --project "$RUNNER_TEMP/release-devkit" --locked --no-dev release
55
+ - run: uv run --project "$RUNNER_TEMP/release-devkit" --locked --no-dev release --channel stable
57
56
  env:
58
57
  GITHUB_TOKEN: ${{ github.token }}
@@ -4,13 +4,13 @@
4
4
 
5
5
  `docker-devkit` owns the Docker-stack lifecycle commands — `up`, `down`, `build`, `mirror`, `generate-score`, `docker-build-matrix` — plus the helpers they share (`detect_gpu`, `modes`, `context_sha`). A small, generic package that any repo shipping a compose graph can depend on to get the same bring-up/tear-down / cross-build flow, with the compose assembly declared per consumer in the `lifecycle:` section of `docker-devkit.yaml`. The PyPI identity starts its own version tags at `0.1.0`; the terminal `stack-toolkit` distributions (≤0.1.1) are deprecation signposts pointing here, not this package's history.
6
6
 
7
- The package is `docker_devkit` (src-layout under `src/docker_devkit/`); all dependencies resolve from PyPI — `bashrun` (shell-outs), `ci-devkit` (builds-shelf push for CI lock artifacts), `packaging` (`requires` specifier parsing), `pathspec`, `pydantic`/`pydantic-settings`, `pyyaml` (score-document parsing), `strictyaml` (config loading), `typer` (CLIs) — git-source pins only in scratch branches testing unreleased changes.
7
+ The package is `docker_devkit` (src-layout under `src/docker_devkit/`); all dependencies resolve from PyPI — `bashrun` (shell-outs), `build-artifact-registry` (builds-shelf push and ORAS provisioning for CI artifacts), `packaging` (`requires` specifier parsing), `pathspec`, `pydantic`/`pydantic-settings`, `pyyaml` (score-document parsing), `strictyaml` (config loading), `typer` (CLIs) — git-source pins only in scratch branches testing unreleased changes.
8
8
 
9
- Config follows the org law (home: release-devkit's `AGENTS.md`): this is a root-`docker-devkit.yaml` devkit. The schema (`config.DockerDevkitConfig`, strictyaml-loaded, `extra="forbid"` on every model) carries a required `requires` version stamp plus optional `lifecycle` / `mirror` / `generate-score` sections; `config.load_config` self-checks the installed docker-devkit version against `requires` and refuses loudly outside the range (the `0.0.0.dev0` dev sentinel bypasses the check, not the field).
9
+ Config follows the org law (home: github-actions-toolkit's `AGENTS.md`): this is a root-`docker-devkit.yaml` devkit. The schema (`config.DockerDevkitConfig`, strictyaml-loaded, `extra="forbid"` on every model) carries a required `requires` version stamp plus optional `lifecycle` / `mirror` / `generate-score` sections; `config.load_config` self-checks the installed docker-devkit version against `requires` and refuses loudly outside the range (the `0.0.0.dev0` dev sentinel bypasses the check, not the field).
10
10
 
11
11
  ## Release flow
12
12
 
13
- release-devkit's `AGENTS.md` owns the three-workflow contract; this repo follows it unchanged. Repo-specific facts: release-devkit is never a project dependency (docker-devkit sits inside its own dependency graph; a project-level edge is a resolver cycle), and from 1.0 on API-breaking changes ship with a manually bumped `major_minor` (pre-1.0, breaking changes ride the patch flow; patch-auto assumes additive changes).
13
+ github-actions-toolkit's `AGENTS.md` owns the three-workflow contract; this repo follows it unchanged. Repo-specific facts: github-actions-toolkit is never a project dependency (docker-devkit sits inside its own dependency graph; a project-level edge is a resolver cycle), and from 1.0 on API-breaking changes ship with a manually bumped `major_minor` (pre-1.0, breaking changes ride the patch flow; patch-auto assumes additive changes).
14
14
 
15
15
  ## Mirror consumption
16
16
 
@@ -24,12 +24,12 @@ Entry points (`[project.scripts]`): `up` → `up.py:app`, `down` → `down.py:ap
24
24
  |---|---|
25
25
  | `up.py` | `docker compose up`. Flags: `--attached`/`-a`, `--quiet-pull`/`-q`, `--build`, `--gpu auto\|cuda\|rocm\|none`, `--dev`, `--allow-upstream-fallback`. |
26
26
  | `down.py` | `docker compose down`. Flags: `--volumes`/`-v` (also removes named volumes), `--gpu`. |
27
- | `build_docker.py` | `run_build()` + the `build` command — cross-builds all images per the resolved image manifest, writing the paired lock (one `NAME=path:tag@sha256:…` entry per `x-base-images` declaration; `@digest`-pinned declarations pass through unresolved; mirror-prefixed declarations resolve their digest against the upstream reference). `--mode ci` hard-requires `x-registry-cache` in the manifest. |
27
+ | `build_docker.py` | `run_build()` + the `build` command — cross-builds all images per the resolved image manifest, writing the paired lock (one `NAME=path:tag@sha256:…` entry per `x-base-images` declaration; `@digest`-pinned declarations pass through unresolved; mirror-prefixed declarations resolve their digest against the upstream reference). `--push` is the CI shape (push images + registry caches + digest-manifest shelving) and hard-requires `x-registry-cache`; the flag order mirrors the binding's run line (`--push`, `--targets`, `--gpu`), and `--targets` accepts both repeated flags and one space-joined value. Push mode reads its registry facts from the neutral `REGISTRY`/`REGISTRY_TOKEN`/`REGISTRY_USERNAME` environment — no `GITHUB_*` reads. |
28
28
  | `score_generate.py` | `generate-score` — deterministic Score-manifest generation from a consumer's `stack/score/` sources: `${ALL_CAPS}` placeholder expansion into tempdir-rendered workload copies, idempotent `score-compose`/`score-k8s` init (state preserved across runs), and a determinism suite (document sort, state-path normalisation, 0644 modes, LF newlines). Consumer policy — workloads, provisioners, patch templates, output paths, storage classes, publishes, tool versions — is declared in the consumer's root `docker-devkit.yaml` under `generate-score:` (`ScoreConfig`, model in `config.py`); `load_score_config()` reads it, `ensure_score_tools()` fetches the pinned score-compose/score-k8s release binaries (for CI staleness gates). Output and state paths resolve against the repo root: the compose half runs in `score_dir` (its `.score-compose` state stays there, gitignored), the k8s half runs in the configured `k8s_state` parent so the committed `.score-k8s` state lands in the generated tier. `--local` writes the k8s artifact to the configured gitignored local path with the local storage class. |
29
29
  | `config.py` | `DockerDevkitConfig` — the root `docker-devkit.yaml` schema: a required `requires` PEP 440 specifier plus optional `lifecycle` / `mirror` / `generate-score` sections, `extra="forbid"` on every model. `load_config(root)` is the single strictyaml entry point: parses the file (returns `None` when absent — the no-config → single-file-default path for `up`/`down`), then self-checks the installed docker-devkit version against `requires` (refuses loudly outside range; the `0.0.0.dev0` dev sentinel bypasses the check, not the field). The section models (`LifecycleConfig`, `MirrorConfig`, `ScoreConfig`) live here; `load_lifecycle_config` / `load_mirror_config` / `load_score_config` in their modules are thin wrappers over it. |
30
30
  | `lifecycle.py` | `LifecycleConfig` (model in `config.py`) — the `lifecycle:` section declaration (`files`: compose files in layering order with `{gpu}` templating; `dev_file`: optional dev overlay); `load_lifecycle_config()` (reads the `lifecycle:` section via `config.load_config`, `None` when the section or file is absent), `resolve_manifest()` / `require_manifest()` (the raising form) / `resolve_lock()` (the image manifest and its lock are a named pair — `workloads/images.yml` + `workloads/images.lock`, with the legacy root `compose.bake.yml` + `.env.lock` pair still resolved so repos can upgrade before moving files), `enforce_bake_declaration()` (a manifest under either name requires a declaration), `expand_compose_files()` (assembles the `-f` list; a `{gpu}` entry is dropped when the GPU resolves to `none`; `dev_file` appended when a dev overlay applies). |
31
31
  | `detect_gpu.py` | `detect_gpu()` → `cuda`/`rocm`/`none` from host devices; the `Gpu` literal. |
32
- | `matrix.py` | `docker-build-matrix` — emit the CI build matrix as one `matrix=<JSON>` `$GITHUB_OUTPUT` line: one entry per variant (`common`/`cuda`/`rocm`, derived from the service-name suffix convention), `targets` = that variant's explicitly-buildable services with dependents grouped with the base their `additional_contexts: target:` references pull in (one leg builds a whole dependency-closure group in a single bake invocation — the safe default over hoping registry cache makes flat legs suffice). Tagless services never become explicit targets (bake rejects a tagless target under `--push`) — they ride as in-invocation dependencies; `x-cross-compile-targets` are excluded; a group spanning variants is a loud error. |
32
+ | `matrix.py` | `docker-build-matrix` — emit the CI build leg list as a bare JSON array on stdout (the binding's getter pipes it through its `matrix` verb, which composes the envelope and guards the empty case): one entry per variant × cohort cell, `targets` = that cell's explicitly-buildable services with dependents grouped with the base their `additional_contexts: target:` references pull in (one leg builds a whole dependency-closure group in a single bake invocation). The leg schema — `targets` (space-joined), `variant` (`common`/`cuda`/`rocm`, from the service-name suffix) — is named contract with the binding; renaming a key breaks its fanout YAML. The cross-compile cohort (services in `x-cross-compile-targets` or declaring `platforms:`) gets its own legs — per-arch builds are the QEMU-slow ones and parallelize away from native builds; a group spanning cohort membership is a loud error. Tagless services never become explicit targets (bake rejects a tagless target under `--push`) — they ride as in-invocation dependencies; a group spanning gpu variants is a loud error. |
33
33
  | `modes.py` | `resolve_auth_mode()` — validates `PUBLIC_URL` + `AUTH_MODE` from `.env`, rejecting `keycloak` over cleartext `http://`. |
34
34
  | `context_sha.py` | `compute_service_shas(repo_root, bake: BakeDocument)` — per-service `tree-<hash>` image tags over the `.dockerignore`-allowlisted git tree. The workload-tree root is derived as the single top-level directory containing every tagged service's Dockerfile (a bake graph spanning several top-level directories is a loud error); files under it that belong to no tagged service are excluded from every SHA rather than shared into all of them. |
35
35
  | `documents.py` | `BakeDocument` + `parse_bake(path)` — the typed bake-file model every consumer of bake data goes through (build, up, down, generate-score, the reference collectors). A field validator enforces at parse time that every `x-base-images` value is `path:tag` or `path@digest` (a `${…}` hole or a bare path is a validation error, killing the silent-unpinned `FROM` → `:latest` failure mode). `digest_pinned(reference)` is the shared tail test. |
@@ -62,10 +62,10 @@ The mirror is declared, not inferred: the `mirror:` section with `prefix` in the
62
62
 
63
63
  - `build` resolves lock digests for mirror-prefixed declarations against the upstream reference (prefix stripped, tag/digest tail preserved) and records it under the mirror name. Valid because `crane copy` produces byte-identical manifests — upstream and mirror digests are the same digest; a manifest re-wrap (`docker buildx imagetools`-shaped tooling) would change the top-level digest and break lock equality. This makes declare → re-lock walkable before CI has mirrored anything; lock lines are never hand-authored.
64
64
  - `up` fails closed: after lock resolution it probes each mirror-prefixed lock entry; a missing mirror image is a hard `RuntimeError`, not a warning — people ignore warnings, and warnings are errors someone was too lazy to fix. The only escape is the per-run `--allow-upstream-fallback` flag. Under the flag, missing entries become digest-preserving address substitutions (same bytes from the upstream host), printed as plain statements — the operator asked for fallback, the run reports what it did. The rewrite is authoritative on both consumption paths: an appended `--env-file` overlay (compose's later env files win) plus `os.environ` before `build`'s bake (shell env wins over env files; `run_build` re-applies the overlay after its own lock update so bake is never clobbered). CI never passes the flag — the mirror job runs before anything needs the images.
65
- - The `mirror` verb (crane install from a pinned release, per-target `crane copy`, count footer) uses bashrun + plain prints, deliberately not ci-devkit's `ci_step` — it needs nothing from the floor.
65
+ - The `mirror` verb (crane install from a pinned release, per-target `crane copy`, count footer) uses bashrun + plain prints — it needs nothing from any runner floor.
66
66
  - Non-mirror lock entries (e.g. `placeframe/*` publishes) are not probed and keep failing naturally at pull. There is no config-level ban on the flag: an air-gap fails closed anyway (no route to upstream — the pull dies with or without it).
67
67
 
68
- The ci-devkit edge exists for the builds shelf: a CI `build` run distills bake's `--metadata-file` into a digest manifest (`target → {ref, digest, tags[]}` JSON) and pushes it to the repo's builds registry at the conventional `images-digests/all` project/platform (`--builds-registry` + `--run-number`) through ci-devkit's `push_build` — one `images-digests-all` artifact tagged `run-{N}` per run, consumed by release-devkit's release surfaces for the built-image digest table. `workloads/images.lock` stays in source control (the stapler that consumed the shelf copy is gone). ORAS CLI knowledge lives only in ci-devkit (org law); local builds (`--mode local`, no `--builds-registry`) never touch it.
68
+ The build-artifact-registry edge exists for the builds shelf: a `build --push` run distills bake's `--metadata-file` into a digest manifest (`target → {ref, digest, tags[]}` JSON) and pushes it through build-artifact-registry's `push_build` — one **shard** per matrix leg, tagged `sha-{full}-{key}` where the key is the sha256-8 of the leg's sorted targets (legs run in parallel, so a shared tag would be last-writer-wins; the pull side enumerates a SHA's shards and merges, and consumption only ever runs after every leg finished). The shelf root comes from the neutral `REGISTRY` environment and the SHA from the checkout HEAD (`git rev-parse HEAD`), stamping the certified PR head the release channels query back at; the keying law is stated in github-actions-toolkit's `AGENTS.md`. `workloads/images.lock` stays in source control (the stapler that consumed the shelf copy is gone). ORAS CLI knowledge lives only in build-artifact-registry (org law); local builds (no `--push`) never touch the shelf.
69
69
 
70
70
  Lifecycle stays a separate package from codegen (client generation, datamodel generation, workspace locking) because their dependency surfaces are disjoint — bundling them would force a consumer that only brings a stack up to install `sqlacodegen`, `psycopg`, `datamodel-code-generator`.
71
71
 
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: docker-devkit
3
- Version: 0.2.1.dev37490360141
3
+ Version: 0.3.0.dev113
4
4
  Summary: Docker-stack lifecycle commands (up/down/build) driven by a pyproject-declared compose assembly
5
5
  License-File: LICENSE
6
6
  License-File: NOTICE
7
7
  Requires-Python: >=3.13
8
8
  Requires-Dist: bashrun>=0.1.0
9
- Requires-Dist: ci-devkit>=0.1.15
9
+ Requires-Dist: build-artifact-registry>=0.2.0.dev0
10
10
  Requires-Dist: packaging>=24.1
11
11
  Requires-Dist: pathspec>=1.0.4
12
12
  Requires-Dist: pydantic-settings>=2.9.1
@@ -1,11 +1,11 @@
1
1
  [project]
2
2
  name = "docker-devkit"
3
- version = "0.2.1.dev37490360141"
3
+ version = "0.3.0.dev113"
4
4
  description = "Docker-stack lifecycle commands (up/down/build) driven by a pyproject-declared compose assembly"
5
5
  requires-python = ">=3.13"
6
6
  dependencies = [
7
7
  "bashrun>=0.1.0",
8
- "ci-devkit>=0.1.15",
8
+ "build-artifact-registry>=0.2.0.dev0",
9
9
  "packaging>=24.1",
10
10
  "pathspec>=1.0.4",
11
11
  "pydantic>=2.11.7",
@@ -20,7 +20,7 @@ dev = [
20
20
  "basedpyright>=1.39.10",
21
21
  "ruff>=0.14.11",
22
22
  "pytest>=8.0.0",
23
- "python-devkit>=0.1.0",
23
+ "python-devkit>=0.3.0.dev0",
24
24
  "types-pyyaml>=6.0.12.20260906",
25
25
  "types-strictyaml>=1.7.3.3",
26
26
  ]
@@ -39,7 +39,6 @@ docker-build-matrix = "docker_devkit.matrix:app"
39
39
 
40
40
  [tool.hatch.build.targets.wheel]
41
41
  packages = ["src/docker_devkit"]
42
- include = ["src/docker_devkit/py.typed"]
43
42
 
44
43
  [tool.uv.sources]
45
44
 
@@ -0,0 +1,6 @@
1
+ packages:
2
+ docker-devkit:
3
+ path: .
4
+ major_minor: "0.3"
5
+ registry: pypi
6
+ identity: docker-devkit
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import hashlib
3
4
  import json
4
5
  import os
5
6
  import platform
@@ -7,12 +8,12 @@ import re
7
8
  from pathlib import Path
8
9
  from subprocess import CalledProcessError
9
10
  from tempfile import TemporaryDirectory
10
- from typing import Annotated, Any, Literal, TypedDict
11
+ from typing import Annotated, Any, TypedDict
11
12
 
12
13
  import typer
13
14
  from bashrun.bash import bash, bash_output
14
- from ci_devkit.builds import push_build
15
- from ci_devkit.setup_oras import install_oras
15
+ from build_artifact_registry.builds import push_build
16
+ from build_artifact_registry.setup_oras import install_oras
16
17
  from .detect_gpu import GPU_TYPES, Gpu, detect_gpu
17
18
  from .documents import BakeDocument, digest_pinned, parse_bake
18
19
  from pydantic_settings import BaseSettings
@@ -32,8 +33,6 @@ settings = Settings.model_validate({})
32
33
 
33
34
  METADATA_PATH = Path("metadata.json")
34
35
 
35
- Mode = Literal["local", "ci"]
36
-
37
36
  DIGEST_PROJECT = "images-digests"
38
37
  DIGEST_PLATFORM = "all"
39
38
  DIGEST_FILE_NAME = "images-digests.json"
@@ -50,55 +49,43 @@ app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
50
49
 
51
50
  @app.command()
52
51
  def build(
52
+ push: Annotated[
53
+ bool, typer.Option("--push", help="Push images (+ registry caches) and shelve the digest manifest.")
54
+ ] = False,
55
+ targets_opt: Annotated[
56
+ list[str] | None,
57
+ typer.Option("--targets", "-t", help="Build only these services (from the image manifest)."),
58
+ ] = None,
59
+ gpu: Annotated[Gpu, typer.Option("--gpu", help="auto|cuda|rocm|none")] = "auto",
53
60
  upgrade: bool = typer.Option(False, "--upgrade", "-u", help="Re-resolve and rewrite base digests."),
54
61
  lock_only: bool = typer.Option(False, "--lock-only", help="Update lock file without building images."),
55
- mode: Annotated[
56
- Mode, typer.Option("--mode", help="local: --load images; ci: --push images + registry caches.")
57
- ] = "local",
58
- gpu: Annotated[Gpu, typer.Option("--gpu", help="auto|cuda|rocm|none")] = "auto",
59
62
  gpu_only: bool = typer.Option(
60
63
  False, "--gpu-only", help="Build only the services suffixed for this gpu (requires a concrete --gpu)."
61
64
  ),
62
65
  no_cache: bool = typer.Option(False, "--no-cache", help="Force rebuild by disabling cache usage."),
63
- targets_opt: Annotated[
64
- list[str] | None,
65
- typer.Option("--targets", "-t", help="Build only these services (from the image manifest)."),
66
- ] = None,
67
- builds_registry: Annotated[
68
- str | None,
69
- typer.Option(
70
- "--builds-registry", help="OCI registry path to push the digest manifest to (CI build artifacts)."
71
- ),
72
- ] = None,
73
- run_number: Annotated[
74
- int, typer.Option("--run-number", help="CI run number baked into the digest manifest's builds-shelf tag.")
75
- ] = 0,
76
66
  ) -> None:
67
+ targets = [service for value in targets_opt or [] for service in value.split()] or None
77
68
  run_build(
69
+ push=push,
70
+ targets_opt=targets,
71
+ gpu=gpu,
78
72
  upgrade=upgrade,
79
73
  lock_only=lock_only,
80
- mode=mode,
81
- gpu=gpu,
82
74
  gpu_only=gpu_only,
83
75
  no_cache=no_cache,
84
- targets_opt=targets_opt,
85
- builds_registry=builds_registry,
86
- run_number=run_number,
87
76
  )
88
77
 
89
78
 
90
79
  def run_build(
91
80
  *,
81
+ push: bool = False,
82
+ targets_opt: list[str] | None = None,
83
+ gpu: Gpu = "auto",
92
84
  upgrade: bool = False,
93
85
  lock_only: bool = False,
94
- mode: Mode = "local",
95
- gpu: Gpu = "auto",
96
86
  gpu_only: bool = False,
97
87
  no_cache: bool = False,
98
- targets_opt: list[str] | None = None,
99
88
  env_overlay: dict[str, str] | None = None,
100
- builds_registry: str | None = None,
101
- run_number: int = 0,
102
89
  ) -> None:
103
90
  root = Path.cwd()
104
91
  manifest = require_manifest(root)
@@ -113,18 +100,17 @@ def run_build(
113
100
  # Load any existing lock
114
101
  lock_data = parse_env_file(lock_file) if lock_file.exists() else {}
115
102
 
116
- # TOOD: Create separate commands for ci and local modes so typer can do this validation instead of us
117
- if mode == "ci" and gpu == "auto":
118
- raise typer.BadParameter("In CI mode, --gpu cannot be 'auto'; specify 'cuda' or 'rocm'.")
103
+ if push and gpu == "auto":
104
+ raise typer.BadParameter("With --push, --gpu cannot be 'auto'; specify 'cuda' or 'rocm'.")
119
105
 
120
- if mode == "ci" and bake.registry_cache is None:
121
- raise typer.BadParameter("In CI mode, the bake file must declare x-registry-cache.")
106
+ if push and bake.registry_cache is None:
107
+ raise typer.BadParameter("With --push, the bake file must declare x-registry-cache.")
122
108
 
123
109
  if gpu_only and gpu not in GPU_TYPES:
124
110
  raise typer.BadParameter("--gpu-only requires a concrete gpu (cuda or rocm), not 'auto' or 'none'.")
125
111
 
126
112
  # For local builds, ensure Docker GC limits are high enough that GPU builds don't cause cache evictions
127
- if mode == "local" and not lock_only and not targets_opt:
113
+ if not push and not lock_only and not targets_opt:
128
114
  _check_gc_limits(min_gb=60)
129
115
 
130
116
  # Resolve gpu
@@ -176,8 +162,8 @@ def run_build(
176
162
  else:
177
163
  targets = compute_default_targets(bake, gpu, gpu_only)
178
164
 
179
- # Configure registry caches in CI mode
180
- if mode == "ci":
165
+ # Configure registry caches for push builds
166
+ if push:
181
167
  for target in targets:
182
168
  target_cache = f"{bake.registry_cache}:{target}"
183
169
  command_arguments.append(
@@ -187,9 +173,9 @@ def run_build(
187
173
 
188
174
  # `docker buildx --load` only writes a single arch to the host image store,
189
175
  # so bake targets declared multi-platform get overridden to host arch in
190
- # local builds. CI uses `--push` and produces the full manifest list.
176
+ # local builds. --push produces the full manifest list.
191
177
  # Override key is `platform` (singular) even though the bake field is plural.
192
- if mode == "local":
178
+ if not push:
193
179
  machine = platform.machine().lower()
194
180
  if machine in ("x86_64", "amd64"):
195
181
  host_platform = "linux/amd64"
@@ -201,8 +187,8 @@ def run_build(
201
187
  if len(bake.services[target].build.platforms) > 1:
202
188
  command_arguments.append(f"--set {target}.platform={host_platform}")
203
189
 
204
- # Load or push images based on mode
205
- command_arguments.append("--load" if mode == "local" else "--push")
190
+ # Load or push images
191
+ command_arguments.append("--push" if push else "--load")
206
192
 
207
193
  # Handle no-cache option
208
194
  if no_cache:
@@ -230,29 +216,39 @@ def run_build(
230
216
  if not set(targets) <= baked_images.keys():
231
217
  raise RuntimeError("Bake output does not cover the requested targets")
232
218
 
233
- push_image_digests(builds_registry, run_number, baked_images, targets)
219
+ # Push the digest manifest to the repo's builds shelf (registry supplied explicitly, SHA from the checkout)
220
+ if push:
221
+ registry = os.environ.get("REGISTRY", "")
222
+ if not registry:
223
+ raise SystemExit("REGISTRY not set — --push shelves the digest manifest under it")
224
+ shelf = f"{registry}/builds"
225
+ sha = bash_output("git rev-parse HEAD").strip()
226
+ push_image_digests(shelf, sha, baked_images, targets)
227
+
234
228
 
229
+ def leg_key(targets: list[str]) -> str:
230
+ return hashlib.sha256(",".join(sorted(targets)).encode("utf-8")).hexdigest()[:8]
235
231
 
232
+
233
+ # Each matrix leg pushes its own manifest shard under sha-{sha}-{key}: legs run in
234
+ # parallel, so a shared tag would be last-writer-wins. The pull side enumerates a
235
+ # SHA's shards and merges — consumption happens only after every leg finished.
236
236
  def push_image_digests(
237
- builds_registry: str | None,
238
- run_number: int,
237
+ shelf: str,
238
+ sha: str,
239
239
  baked_images: dict[str, Any],
240
240
  targets: list[str],
241
241
  ) -> None:
242
- if builds_registry is None:
243
- return
244
- if run_number <= 0:
245
- raise typer.BadParameter("--builds-registry requires a positive --run-number")
246
242
  install_oras()
247
243
  manifest = distill_digest_manifest(baked_images, targets)
248
244
  with TemporaryDirectory(prefix="digest-manifest-") as staging:
249
245
  manifest_path = Path(staging) / DIGEST_FILE_NAME
250
246
  manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
251
247
  push_build(
252
- builds_registry,
248
+ shelf,
253
249
  DIGEST_PROJECT,
254
250
  DIGEST_PLATFORM,
255
- f"run-{run_number}",
251
+ f"sha-{sha}-{leg_key(targets)}",
256
252
  Path(staging),
257
253
  [DIGEST_FILE_NAME],
258
254
  )
@@ -273,6 +269,8 @@ def distill_digest_manifest(baked_images: dict[str, Any], targets: list[str]) ->
273
269
  if separator:
274
270
  image_ref = repository
275
271
  tags.append(tag)
272
+ if not tags:
273
+ raise ValueError(f"target '{target}' produced no tagged image references from '{image_name}'")
276
274
  manifest[target] = {"ref": image_ref, "digest": digest, "tags": tags}
277
275
  return manifest
278
276
 
@@ -18,32 +18,49 @@ PLAIN_VARIANT = "common"
18
18
  @app.command()
19
19
  def matrix_main() -> None:
20
20
  bake = parse_bake(require_manifest(Path.cwd()))
21
- print(f"matrix={json.dumps({'include': compute_matrix_entries(bake)})}")
21
+ print(json.dumps(compute_matrix_entries(bake)))
22
22
 
23
23
 
24
- # One entry per variant: targets are the dependency-closure groups of that variant's
25
- # explicitly-buildable services, coalesced into a single leg — the shape a single
26
- # gpu-resolved build invocation would produce, with dependents grouped with the base
27
- # their additional_contexts `target:` references pull in.
24
+ # One entry per variant and cohort cell: targets are the dependency-closure groups
25
+ # of that cell's explicitly-buildable services, coalesced into a single leg — the
26
+ # shape a single gpu-resolved build invocation would produce, with dependents
27
+ # grouped with the base their additional_contexts `target:` references pull in.
28
+ # The cross-compile cohort gets its own legs: per-arch builds are the QEMU-slow
29
+ # ones, so they parallelize away from native builds instead of serializing them
30
+ # into one invocation.
28
31
  def compute_matrix_entries(bake: BakeDocument) -> list[dict[str, str]]:
29
- entries_by_variant: dict[str, list[str]] = {}
32
+ cohort = cross_compile_cohort(bake)
33
+ entries_by_cell: dict[tuple[str, bool], list[str]] = {}
30
34
  for group in dependency_groups(bake):
31
- explicit = sorted(
32
- service
33
- for service in group
34
- if bake.services[service].build.tags and service not in set(bake.cross_compile_targets)
35
- )
35
+ explicit = sorted(service for service in group if bake.services[service].build.tags)
36
36
  if not explicit:
37
37
  continue
38
38
  variant = group_variant(bake, explicit)
39
- entries_by_variant.setdefault(variant, []).extend(explicit)
39
+ memberships = {service in cohort for service in explicit}
40
+ if len(memberships) > 1:
41
+ raise RuntimeError(
42
+ f"dependency group spans cross-compile cohort membership ({', '.join(explicit)}) — "
43
+ "additional_contexts must stay within one cohort"
44
+ )
45
+ entries_by_cell.setdefault((variant, memberships.pop()), []).extend(explicit)
40
46
 
41
47
  return [
42
- {"targets": " ".join(sorted(entries_by_variant[variant])), "variant": variant}
43
- for variant in sorted(entries_by_variant)
48
+ {"targets": " ".join(sorted(targets)), "variant": variant}
49
+ for (variant, _), targets in sorted(entries_by_cell.items())
44
50
  ]
45
51
 
46
52
 
53
+ # The cross-compile cohort: every service declaring `platforms:` (per-arch
54
+ # treatment is a manifest fact, not an operator judgment) plus the hand-listed
55
+ # x-cross-compile-targets members.
56
+ def cross_compile_cohort(bake: BakeDocument) -> set[str]:
57
+ unknown = set(bake.cross_compile_targets) - set(bake.services)
58
+ if unknown:
59
+ raise RuntimeError(f"x-cross-compile-targets names unknown services: {', '.join(sorted(unknown))}")
60
+ pinned = {service for service, config in bake.services.items() if config.build.platforms}
61
+ return pinned | set(bake.cross_compile_targets)
62
+
63
+
47
64
  def dependency_groups(bake: BakeDocument) -> list[list[str]]:
48
65
  parent = {service: service for service in bake.services}
49
66
 
@@ -3,13 +3,13 @@ from pathlib import Path
3
3
  from typing import Any
4
4
 
5
5
  import pytest
6
- import typer
7
6
 
8
7
  from docker_devkit import build_docker
9
8
  from docker_devkit.build_docker import DIGEST_FILE_NAME, distill_digest_manifest, push_image_digests
10
9
 
11
10
  DIGEST_A = "sha256:" + "a" * 64
12
11
  DIGEST_B = "sha256:" + "b" * 64
12
+ BUILD_SHA = "c" * 40
13
13
  REPO_ZED = "ghcr.io/outernet-foundation/placeframe-capture-tool/zed-capture"
14
14
  REPO_AOA = "ghcr.io/outernet-foundation/placeframe-capture-tool/aoa-bridge"
15
15
 
@@ -45,9 +45,9 @@ def test_distills_single_target_single_tag() -> None:
45
45
 
46
46
 
47
47
  def test_distills_multiple_tags_from_comma_separated_refs() -> None:
48
- baked = _baked("zed-capture", refs=f"{REPO_ZED}:tree-abc,{REPO_ZED}:run-42", digest=DIGEST_A)
48
+ baked = _baked("zed-capture", refs=f"{REPO_ZED}:tree-abc,{REPO_ZED}:latest", digest=DIGEST_A)
49
49
  assert distill_digest_manifest(baked, ["zed-capture"]) == {
50
- "zed-capture": {"ref": REPO_ZED, "digest": DIGEST_A, "tags": ["tree-abc", "run-42"]}
50
+ "zed-capture": {"ref": REPO_ZED, "digest": DIGEST_A, "tags": ["tree-abc", "latest"]}
51
51
  }
52
52
 
53
53
 
@@ -69,31 +69,29 @@ def test_strips_digest_suffix_from_refs() -> None:
69
69
  assert entry["tags"] == ["tree-abc"]
70
70
 
71
71
 
72
- def test_push_image_digests_is_noop_without_builds_registry(monkeypatch: pytest.MonkeyPatch) -> None:
73
- recorder = PushBuildRecorder()
74
- monkeypatch.setattr(build_docker, "push_build", recorder)
75
- baked = _baked("zed-capture", refs=f"{REPO_ZED}:tree-abc", digest=DIGEST_A)
76
- push_image_digests(None, 42, baked, ["zed-capture"])
77
- assert recorder.calls == []
78
-
79
-
80
- def test_push_image_digests_rejects_zero_run_number() -> None:
81
- with pytest.raises(typer.BadParameter):
82
- push_image_digests("ghcr.io/owner/builds", 0, {}, [])
72
+ def test_refuses_target_with_no_tagged_references() -> None:
73
+ baked = _baked("zed-capture", refs=f"{REPO_ZED}@{DIGEST_A}", digest=DIGEST_A)
74
+ with pytest.raises(ValueError, match="zed-capture"):
75
+ distill_digest_manifest(baked, ["zed-capture"])
83
76
 
84
77
 
85
78
  def test_push_image_digests_pushes_manifest_to_images_digests_all(monkeypatch: pytest.MonkeyPatch) -> None:
86
79
  recorder = PushBuildRecorder()
87
80
  monkeypatch.setattr(build_docker, "push_build", recorder)
88
- baked = _baked("zed-capture", refs=f"{REPO_ZED}:tree-abc,{REPO_ZED}:run-42", digest=DIGEST_A)
89
- push_image_digests("ghcr.io/owner/builds", 42, baked, ["zed-capture"])
81
+ baked = _baked("zed-capture", refs=f"{REPO_ZED}:tree-abc,{REPO_ZED}:latest", digest=DIGEST_A)
82
+ push_image_digests("ghcr.io/owner/builds", BUILD_SHA, baked, ["zed-capture"])
90
83
  assert len(recorder.calls) == 1
91
84
  registry, project, platform, tag, _source_directory, paths = recorder.calls[0]
92
85
  assert registry == "ghcr.io/owner/builds"
93
86
  assert project == "images-digests"
94
87
  assert platform == "all"
95
- assert tag == "run-42"
88
+ assert tag == f"sha-{BUILD_SHA}-{build_docker.leg_key(['zed-capture'])}"
96
89
  assert paths == [DIGEST_FILE_NAME]
97
90
  assert recorder.manifests == [
98
- {"zed-capture": {"ref": REPO_ZED, "digest": DIGEST_A, "tags": ["tree-abc", "run-42"]}}
91
+ {"zed-capture": {"ref": REPO_ZED, "digest": DIGEST_A, "tags": ["tree-abc", "latest"]}}
99
92
  ]
93
+
94
+
95
+ def test_leg_key_is_order_insensitive() -> None:
96
+ assert build_docker.leg_key(["zed-capture", "aoa-bridge"]) == build_docker.leg_key(["aoa-bridge", "zed-capture"])
97
+ assert build_docker.leg_key(["zed-capture"]) != build_docker.leg_key(["aoa-bridge"])
@@ -7,12 +7,20 @@ from docker_devkit.documents import BakeDocument
7
7
  from docker_devkit.matrix import compute_matrix_entries
8
8
 
9
9
 
10
- def service(dockerfile: str, *, tags: bool = True, contexts: dict[str, str] | None = None) -> dict[str, object]:
10
+ def service(
11
+ dockerfile: str,
12
+ *,
13
+ tags: bool = True,
14
+ contexts: dict[str, str] | None = None,
15
+ platforms: list[str] | None = None,
16
+ ) -> dict[str, object]:
11
17
  build: dict[str, object] = {"dockerfile": dockerfile}
12
18
  if tags:
13
19
  build["tags"] = [f"registry/{dockerfile}:latest"]
14
20
  if contexts is not None:
15
21
  build["additional_contexts"] = contexts
22
+ if platforms is not None:
23
+ build["platforms"] = platforms
16
24
  return {"build": build}
17
25
 
18
26
 
@@ -65,16 +73,59 @@ def test_single_service_repo_emits_matrix_of_one() -> None:
65
73
  assert compute_matrix_entries(bake) == [{"targets": "livekit-token", "variant": "common"}]
66
74
 
67
75
 
68
- def test_cross_compile_targets_are_excluded() -> None:
76
+ def test_cross_compile_targets_get_their_own_legs() -> None:
69
77
  bake = BakeDocument.model_validate({
70
78
  "services": {
71
79
  "api": service("docker/api/Dockerfile"),
80
+ "postgres": service("docker/postgres/Dockerfile"),
72
81
  "zed-capture": service("docker/zed-capture/Dockerfile"),
73
82
  },
74
83
  "x-cross-compile-targets": ["zed-capture"],
75
84
  })
76
85
 
77
- assert compute_matrix_entries(bake) == [{"targets": "api", "variant": "common"}]
86
+ assert compute_matrix_entries(bake) == [
87
+ {"targets": "api postgres", "variant": "common"},
88
+ {"targets": "zed-capture", "variant": "common"},
89
+ ]
90
+
91
+
92
+ def test_platform_pinned_services_join_the_cross_compile_cohort() -> None:
93
+ bake = BakeDocument.model_validate({
94
+ "services": {
95
+ "api": service("docker/api/Dockerfile"),
96
+ "zed-capture": service("docker/zed-capture/Dockerfile", platforms=["linux/arm64"]),
97
+ "aoa-bridge": service("docker/aoa-bridge/Dockerfile", platforms=["linux/arm64"]),
98
+ }
99
+ })
100
+
101
+ assert compute_matrix_entries(bake) == [
102
+ {"targets": "api", "variant": "common"},
103
+ {"targets": "aoa-bridge zed-capture", "variant": "common"},
104
+ ]
105
+
106
+
107
+ def test_unknown_cross_compile_target_fails_loudly() -> None:
108
+ bake = BakeDocument.model_validate({
109
+ "services": {"api": service("docker/api/Dockerfile")},
110
+ "x-cross-compile-targets": ["missing"],
111
+ })
112
+
113
+ with pytest.raises(RuntimeError, match="unknown services"):
114
+ compute_matrix_entries(bake)
115
+
116
+
117
+ def test_group_spanning_cohort_membership_fails_loudly() -> None:
118
+ bake = BakeDocument.model_validate({
119
+ "services": {
120
+ "api": service("docker/api/Dockerfile"),
121
+ "zed-capture": service(
122
+ "docker/zed-capture/Dockerfile", contexts={"api": "target:api"}, platforms=["linux/arm64"]
123
+ ),
124
+ }
125
+ })
126
+
127
+ with pytest.raises(RuntimeError, match="spans cross-compile cohort"):
128
+ compute_matrix_entries(bake)
78
129
 
79
130
 
80
131
  def test_non_target_additional_contexts_do_not_group() -> None:
@@ -111,7 +162,7 @@ def test_unknown_target_reference_fails_loudly() -> None:
111
162
  compute_matrix_entries(bake)
112
163
 
113
164
 
114
- def test_matrix_main_prints_github_output_line(
165
+ def test_matrix_main_prints_bare_leg_list(
115
166
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
116
167
  ) -> None:
117
168
  (tmp_path / "workloads").mkdir()
@@ -125,5 +176,5 @@ def test_matrix_main_prints_github_output_line(
125
176
 
126
177
  matrix_main()
127
178
 
128
- payload = json.loads(capsys.readouterr().out.splitlines()[0][len("matrix=") :])
129
- assert payload == {"include": [{"targets": "livekit-token", "variant": "common"}]}
179
+ payload = json.loads(capsys.readouterr().out)
180
+ assert payload == [{"targets": "livekit-token", "variant": "common"}]
@@ -42,17 +42,17 @@ wheels = [
42
42
  ]
43
43
 
44
44
  [[package]]
45
- name = "ci-devkit"
46
- version = "0.2.0"
45
+ name = "build-artifact-registry"
46
+ version = "0.2.0.dev38101394871"
47
47
  source = { registry = "https://pypi.org/simple" }
48
48
  dependencies = [
49
49
  { name = "bashrun" },
50
50
  { name = "pydantic" },
51
51
  { name = "pydantic-settings" },
52
52
  ]
53
- sdist = { url = "https://files.pythonhosted.org/packages/fd/c6/94c2a4a406b593ca3a1140151c0cb2c1367b30bf295f32a4bf9c92c100ac/ci_devkit-0.2.0.tar.gz", hash = "sha256:a16e0a91771a5b50205646577e8d56198c7c3262a063d7ebcf9a82fc26a2e3e0", size = 53288, upload-time = "2026-10-05T15:02:35.926Z" }
53
+ sdist = { url = "https://files.pythonhosted.org/packages/d2/35/7bd270fb56bbbcee40ee2f3834b07e912fb69ff62f84560f7a2e08a3759a/build_artifact_registry-0.2.0.dev38101394871.tar.gz", hash = "sha256:d19575c8315f808cbd5d6e6011dbe2eb03a2b1bc25865482ddbf14acccb1dace", size = 36173, upload-time = "2026-10-11T01:21:07.61Z" }
54
54
  wheels = [
55
- { url = "https://files.pythonhosted.org/packages/ce/d7/f7b91b7e62ed5cd7864f4ddbf84851ad5a31938342cb00e28bf15a7064c2/ci_devkit-0.2.0-py3-none-any.whl", hash = "sha256:d86403effbe36563b0a471d860e5e1e46ae7793d7d203a2dcc5561e91d1163f6", size = 28022, upload-time = "2026-10-05T15:02:34.654Z" },
55
+ { url = "https://files.pythonhosted.org/packages/41/e4/4cb0cc61d42f3822aec3f66b27490e3dfa44ecc26109093236c868528121/build_artifact_registry-0.2.0.dev38101394871-py3-none-any.whl", hash = "sha256:b124930a6b565ce0861d3bc6f680cbca92bf81334caef077b5b00666cee9257f", size = 10745, upload-time = "2026-10-11T01:21:06.485Z" },
56
56
  ]
57
57
 
58
58
  [[package]]
@@ -70,7 +70,7 @@ version = "0.0.0.dev0"
70
70
  source = { editable = "." }
71
71
  dependencies = [
72
72
  { name = "bashrun" },
73
- { name = "ci-devkit" },
73
+ { name = "build-artifact-registry" },
74
74
  { name = "packaging" },
75
75
  { name = "pathspec" },
76
76
  { name = "pydantic" },
@@ -93,7 +93,7 @@ dev = [
93
93
  [package.metadata]
94
94
  requires-dist = [
95
95
  { name = "bashrun", specifier = ">=0.1.0" },
96
- { name = "ci-devkit", specifier = ">=0.1.15" },
96
+ { name = "build-artifact-registry", specifier = ">=0.2.0.dev0" },
97
97
  { name = "packaging", specifier = ">=24.1" },
98
98
  { name = "pathspec", specifier = ">=1.0.4" },
99
99
  { name = "pydantic", specifier = ">=2.11.7" },
@@ -107,7 +107,7 @@ requires-dist = [
107
107
  dev = [
108
108
  { name = "basedpyright", specifier = ">=1.39.10" },
109
109
  { name = "pytest", specifier = ">=8.0.0" },
110
- { name = "python-devkit", specifier = ">=0.1.0" },
110
+ { name = "python-devkit", specifier = ">=0.3.0.dev0" },
111
111
  { name = "ruff", specifier = ">=0.14.11" },
112
112
  { name = "types-pyyaml", specifier = ">=6.0.12.20260906" },
113
113
  { name = "types-strictyaml", specifier = ">=1.7.3.3" },
@@ -310,17 +310,16 @@ wheels = [
310
310
 
311
311
  [[package]]
312
312
  name = "python-devkit"
313
- version = "0.1.15"
313
+ version = "0.3.0.dev38096025533"
314
314
  source = { registry = "https://pypi.org/simple" }
315
315
  dependencies = [
316
316
  { name = "bashrun" },
317
- { name = "ci-devkit" },
318
317
  { name = "pydantic" },
319
318
  { name = "typer" },
320
319
  ]
321
- sdist = { url = "https://files.pythonhosted.org/packages/82/53/c3b37f69c35475b09cdf1c18deb193238d7ab5143ee6bc97e5c22c48fe87/python_devkit-0.1.15.tar.gz", hash = "sha256:ac1ef0f2a4622e9c97763f4e353005c41164e2d6d4b130cdb709e811947f253d", size = 34651, upload-time = "2026-09-30T04:12:18.88Z" }
320
+ sdist = { url = "https://files.pythonhosted.org/packages/85/07/160d8d87f184c1f9a32e2dd64a413695d10dad275d1684f89728d28c543c/python_devkit-0.3.0.dev38096025533.tar.gz", hash = "sha256:94b6f3d63b9192ede7d4bff2a3fabcc714a202807e2a7468c0047920837bb657", size = 33112, upload-time = "2026-10-10T23:47:13.476Z" }
322
321
  wheels = [
323
- { url = "https://files.pythonhosted.org/packages/b6/81/da4f76cbfccb5a4d5a58a9f51d3e2b7d40f6dc9edd931d7891b2789adeae/python_devkit-0.1.15-py3-none-any.whl", hash = "sha256:0c13684cc780e3ba9b3e25feed61065b355e8fc7436bee043e7c6adf71fa40e7", size = 14537, upload-time = "2026-09-30T04:12:17.918Z" },
322
+ { url = "https://files.pythonhosted.org/packages/12/66/c87d7ab58ba1ded58dfb21fe0c9783231d7a67514fd335239bd95df58414/python_devkit-0.3.0.dev38096025533-py3-none-any.whl", hash = "sha256:605aeabb81770f6393e771518639512fd6a7c39b5a22554dfe747fd9de3a0205", size = 14633, upload-time = "2026-10-10T23:47:12.49Z" },
324
323
  ]
325
324
 
326
325
  [[package]]
@@ -1,7 +0,0 @@
1
- ci_workflow: release.yml
2
- packages:
3
- docker-devkit:
4
- path: .
5
- major_minor: "0.2"
6
- registries:
7
- pypi: docker-devkit