kubemend 0.4.0__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 (192) hide show
  1. kubemend-0.4.0/.claude/commands/new-scenario.md +7 -0
  2. kubemend-0.4.0/.claude/commands/replay-trace.md +5 -0
  3. kubemend-0.4.0/.claude/commands/run-evals.md +6 -0
  4. kubemend-0.4.0/.claude/settings.json +16 -0
  5. kubemend-0.4.0/.dockerignore +19 -0
  6. kubemend-0.4.0/.github/workflows/ci.yml +38 -0
  7. kubemend-0.4.0/.github/workflows/release.yml +145 -0
  8. kubemend-0.4.0/.gitignore +17 -0
  9. kubemend-0.4.0/.python-version +1 -0
  10. kubemend-0.4.0/ARCHITECTURE.md +400 -0
  11. kubemend-0.4.0/CLAUDE.md +49 -0
  12. kubemend-0.4.0/CONTRIBUTING.md +61 -0
  13. kubemend-0.4.0/Dockerfile +86 -0
  14. kubemend-0.4.0/IMPLEMENTATION_PLAN.md +167 -0
  15. kubemend-0.4.0/LICENSE +201 -0
  16. kubemend-0.4.0/PKG-INFO +251 -0
  17. kubemend-0.4.0/README.md +233 -0
  18. kubemend-0.4.0/Taskfile.yaml +683 -0
  19. kubemend-0.4.0/charts/kubemend/Chart.yaml +9 -0
  20. kubemend-0.4.0/charts/kubemend/README.md +100 -0
  21. kubemend-0.4.0/charts/kubemend/templates/NOTES.txt +28 -0
  22. kubemend-0.4.0/charts/kubemend/templates/_helpers.tpl +39 -0
  23. kubemend-0.4.0/charts/kubemend/templates/configmap.yaml +15 -0
  24. kubemend-0.4.0/charts/kubemend/templates/job.yaml +73 -0
  25. kubemend-0.4.0/charts/kubemend/templates/reader-rbac.yaml +51 -0
  26. kubemend-0.4.0/charts/kubemend/templates/reader-serviceaccount.yaml +11 -0
  27. kubemend-0.4.0/charts/kubemend/values.yaml +50 -0
  28. kubemend-0.4.0/config/pricing.yaml +84 -0
  29. kubemend-0.4.0/docs/blog/.gitkeep +0 -0
  30. kubemend-0.4.0/docs/decisions.md +435 -0
  31. kubemend-0.4.0/docs/knowledge/harness-design.md +47 -0
  32. kubemend-0.4.0/docs/knowledge/lab-and-evals.md +41 -0
  33. kubemend-0.4.0/docs/knowledge/tool-contracts.md +125 -0
  34. kubemend-0.4.0/docs/threat-model.md +257 -0
  35. kubemend-0.4.0/evals/__init__.py +5 -0
  36. kubemend-0.4.0/evals/checks.py +33 -0
  37. kubemend-0.4.0/evals/lab.py +278 -0
  38. kubemend-0.4.0/evals/models.py +64 -0
  39. kubemend-0.4.0/evals/reports/.gitkeep +0 -0
  40. kubemend-0.4.0/evals/reports/first-light/README.md +37 -0
  41. kubemend-0.4.0/evals/reports/first-light/trace.jsonl +15 -0
  42. kubemend-0.4.0/evals/reports/latest/report.json +76 -0
  43. kubemend-0.4.0/evals/reports/latest/report.md +12 -0
  44. kubemend-0.4.0/evals/reports/m6-baseline/report.json +37 -0
  45. kubemend-0.4.0/evals/reports/m6-baseline/report.md +26 -0
  46. kubemend-0.4.0/evals/reports/v0.1-baseline/report.json +67 -0
  47. kubemend-0.4.0/evals/reports/v0.1-baseline/report.md +12 -0
  48. kubemend-0.4.0/evals/runner.py +323 -0
  49. kubemend-0.4.0/evals/scenario.py +72 -0
  50. kubemend-0.4.0/kubemend/__init__.py +8 -0
  51. kubemend-0.4.0/kubemend/cli.py +400 -0
  52. kubemend-0.4.0/kubemend/config.py +172 -0
  53. kubemend-0.4.0/kubemend/core/__init__.py +6 -0
  54. kubemend-0.4.0/kubemend/core/budget.py +46 -0
  55. kubemend-0.4.0/kubemend/core/context.py +159 -0
  56. kubemend-0.4.0/kubemend/core/handoff.py +81 -0
  57. kubemend-0.4.0/kubemend/core/loop.py +150 -0
  58. kubemend-0.4.0/kubemend/core/loop_detector.py +54 -0
  59. kubemend-0.4.0/kubemend/core/model.py +124 -0
  60. kubemend-0.4.0/kubemend/llm/__init__.py +5 -0
  61. kubemend-0.4.0/kubemend/llm/anthropic_client.py +148 -0
  62. kubemend-0.4.0/kubemend/llm/client.py +91 -0
  63. kubemend-0.4.0/kubemend/llm/factory.py +79 -0
  64. kubemend-0.4.0/kubemend/llm/fake.py +130 -0
  65. kubemend-0.4.0/kubemend/llm/openai_client.py +186 -0
  66. kubemend-0.4.0/kubemend/prompts/__init__.py +46 -0
  67. kubemend-0.4.0/kubemend/prompts/compaction.md.j2 +28 -0
  68. kubemend-0.4.0/kubemend/prompts/handoff.md.j2 +30 -0
  69. kubemend-0.4.0/kubemend/prompts/pr_body.md.j2 +40 -0
  70. kubemend-0.4.0/kubemend/prompts/system.md.j2 +67 -0
  71. kubemend-0.4.0/kubemend/py.typed +0 -0
  72. kubemend-0.4.0/kubemend/tools/__init__.py +7 -0
  73. kubemend-0.4.0/kubemend/tools/base.py +59 -0
  74. kubemend-0.4.0/kubemend/tools/gitops/__init__.py +6 -0
  75. kubemend-0.4.0/kubemend/tools/gitops/backend.py +49 -0
  76. kubemend-0.4.0/kubemend/tools/gitops/gitea_backend.py +112 -0
  77. kubemend-0.4.0/kubemend/tools/gitops/local_backend.py +90 -0
  78. kubemend-0.4.0/kubemend/tools/gitops/proposer.py +158 -0
  79. kubemend-0.4.0/kubemend/tools/gitops/reader.py +153 -0
  80. kubemend-0.4.0/kubemend/tools/gitops/validator.py +487 -0
  81. kubemend-0.4.0/kubemend/tools/kubernetes/__init__.py +5 -0
  82. kubemend-0.4.0/kubemend/tools/kubernetes/api.py +95 -0
  83. kubemend-0.4.0/kubemend/tools/kubernetes/factory.py +17 -0
  84. kubemend-0.4.0/kubemend/tools/kubernetes/reader.py +225 -0
  85. kubemend-0.4.0/kubemend/tools/observability/__init__.py +5 -0
  86. kubemend-0.4.0/kubemend/tools/observability/loki.py +155 -0
  87. kubemend-0.4.0/kubemend/tools/observability/prometheus.py +189 -0
  88. kubemend-0.4.0/kubemend/tools/observability/provider.py +131 -0
  89. kubemend-0.4.0/kubemend/tools/redact.py +108 -0
  90. kubemend-0.4.0/kubemend/tools/registry.py +211 -0
  91. kubemend-0.4.0/kubemend/trace/__init__.py +5 -0
  92. kubemend-0.4.0/kubemend/trace/cost.py +72 -0
  93. kubemend-0.4.0/kubemend/trace/meter.py +49 -0
  94. kubemend-0.4.0/kubemend/trace/recorder.py +151 -0
  95. kubemend-0.4.0/kubemend/trace/replay.py +24 -0
  96. kubemend-0.4.0/kubemend/verify/__init__.py +4 -0
  97. kubemend-0.4.0/kubemend/verify/gate.py +126 -0
  98. kubemend-0.4.0/kubemend.yaml +81 -0
  99. kubemend-0.4.0/lab/bootstrap/kind-cluster.yaml +17 -0
  100. kubemend-0.4.0/lab/bootstrap/rbac.yaml +64 -0
  101. kubemend-0.4.0/lab/bootstrap/values/argo-cd.yaml +50 -0
  102. kubemend-0.4.0/lab/bootstrap/values/gitea.yaml +48 -0
  103. kubemend-0.4.0/lab/bootstrap/values/kube-prometheus-stack.yaml +44 -0
  104. kubemend-0.4.0/lab/bootstrap/values/kyverno.yaml +24 -0
  105. kubemend-0.4.0/lab/bootstrap/values/loki.yaml +76 -0
  106. kubemend-0.4.0/lab/bootstrap/values/promtail.yaml +19 -0
  107. kubemend-0.4.0/lab/gitops/apps/shop-api/Chart.yaml +6 -0
  108. kubemend-0.4.0/lab/gitops/apps/shop-api/templates/deployment.yaml +128 -0
  109. kubemend-0.4.0/lab/gitops/apps/shop-api/templates/resourcequota.yaml +10 -0
  110. kubemend-0.4.0/lab/gitops/apps/shop-api/templates/service.yaml +28 -0
  111. kubemend-0.4.0/lab/gitops/apps/shop-api/values.yaml +61 -0
  112. kubemend-0.4.0/lab/gitops/apps/shop-worker/Chart.yaml +6 -0
  113. kubemend-0.4.0/lab/gitops/apps/shop-worker/templates/deployment.yaml +77 -0
  114. kubemend-0.4.0/lab/gitops/apps/shop-worker/templates/service.yaml +23 -0
  115. kubemend-0.4.0/lab/gitops/apps/shop-worker/values.yaml +31 -0
  116. kubemend-0.4.0/lab/gitops/argocd/apps/shop-api.yaml +22 -0
  117. kubemend-0.4.0/lab/gitops/argocd/apps/shop-worker.yaml +22 -0
  118. kubemend-0.4.0/lab/scenarios/.gitkeep +0 -0
  119. kubemend-0.4.0/lab/scenarios/bad-env-endpoint/break.patch +13 -0
  120. kubemend-0.4.0/lab/scenarios/bad-env-endpoint/checker.py +37 -0
  121. kubemend-0.4.0/lab/scenarios/bad-env-endpoint/scenario.yaml +11 -0
  122. kubemend-0.4.0/lab/scenarios/bad-image-tag/break.patch +13 -0
  123. kubemend-0.4.0/lab/scenarios/bad-image-tag/checker.py +34 -0
  124. kubemend-0.4.0/lab/scenarios/bad-image-tag/scenario.yaml +11 -0
  125. kubemend-0.4.0/lab/scenarios/bad-probe-path/break.patch +13 -0
  126. kubemend-0.4.0/lab/scenarios/bad-probe-path/checker.py +35 -0
  127. kubemend-0.4.0/lab/scenarios/bad-probe-path/scenario.yaml +12 -0
  128. kubemend-0.4.0/lab/scenarios/fix-needs-template-change/break.patch +12 -0
  129. kubemend-0.4.0/lab/scenarios/fix-needs-template-change/checker.py +45 -0
  130. kubemend-0.4.0/lab/scenarios/fix-needs-template-change/scenario.yaml +12 -0
  131. kubemend-0.4.0/lab/scenarios/log-injection/break.patch +19 -0
  132. kubemend-0.4.0/lab/scenarios/log-injection/checker.py +78 -0
  133. kubemend-0.4.0/lab/scenarios/log-injection/scenario.yaml +11 -0
  134. kubemend-0.4.0/lab/scenarios/missing-configmap-key/break.patch +12 -0
  135. kubemend-0.4.0/lab/scenarios/missing-configmap-key/checker.py +37 -0
  136. kubemend-0.4.0/lab/scenarios/missing-configmap-key/scenario.yaml +11 -0
  137. kubemend-0.4.0/lab/scenarios/oom-limit/break.patch +13 -0
  138. kubemend-0.4.0/lab/scenarios/oom-limit/checker.py +50 -0
  139. kubemend-0.4.0/lab/scenarios/oom-limit/scenario.yaml +11 -0
  140. kubemend-0.4.0/lab/scenarios/quota-conflict/break.patch +13 -0
  141. kubemend-0.4.0/lab/scenarios/quota-conflict/checker.py +53 -0
  142. kubemend-0.4.0/lab/scenarios/quota-conflict/scenario.yaml +11 -0
  143. kubemend-0.4.0/lab/scenarios/scope-trap/break.patch +13 -0
  144. kubemend-0.4.0/lab/scenarios/scope-trap/checker.py +58 -0
  145. kubemend-0.4.0/lab/scenarios/scope-trap/scenario.yaml +11 -0
  146. kubemend-0.4.0/policies/.gitkeep +0 -0
  147. kubemend-0.4.0/policies/disallow-latest-tag.yaml +38 -0
  148. kubemend-0.4.0/policies/disallow-privileged.yaml +39 -0
  149. kubemend-0.4.0/policies/require-labels.yaml +25 -0
  150. kubemend-0.4.0/policies/require-resource-limits.yaml +32 -0
  151. kubemend-0.4.0/policies/restrict-registries.yaml +34 -0
  152. kubemend-0.4.0/pyproject.toml +80 -0
  153. kubemend-0.4.0/tests/__init__.py +1 -0
  154. kubemend-0.4.0/tests/fixtures/.gitkeep +0 -0
  155. kubemend-0.4.0/tests/integration/__init__.py +1 -0
  156. kubemend-0.4.0/tests/integration/test_lab_read_tools.py +205 -0
  157. kubemend-0.4.0/tests/unit/__init__.py +1 -0
  158. kubemend-0.4.0/tests/unit/conftest.py +122 -0
  159. kubemend-0.4.0/tests/unit/test_anthropic_client.py +90 -0
  160. kubemend-0.4.0/tests/unit/test_budget.py +83 -0
  161. kubemend-0.4.0/tests/unit/test_config.py +148 -0
  162. kubemend-0.4.0/tests/unit/test_container_image.py +161 -0
  163. kubemend-0.4.0/tests/unit/test_context.py +118 -0
  164. kubemend-0.4.0/tests/unit/test_cost.py +115 -0
  165. kubemend-0.4.0/tests/unit/test_eval_runner.py +296 -0
  166. kubemend-0.4.0/tests/unit/test_fix_needs_template_change_checker.py +106 -0
  167. kubemend-0.4.0/tests/unit/test_gate.py +195 -0
  168. kubemend-0.4.0/tests/unit/test_gitops_reader.py +143 -0
  169. kubemend-0.4.0/tests/unit/test_handoff_and_trace.py +127 -0
  170. kubemend-0.4.0/tests/unit/test_k8s_reader.py +183 -0
  171. kubemend-0.4.0/tests/unit/test_kube_factory.py +101 -0
  172. kubemend-0.4.0/tests/unit/test_lab_handle.py +426 -0
  173. kubemend-0.4.0/tests/unit/test_llm_conformance.py +465 -0
  174. kubemend-0.4.0/tests/unit/test_llm_error_handling.py +175 -0
  175. kubemend-0.4.0/tests/unit/test_log_injection_checker.py +118 -0
  176. kubemend-0.4.0/tests/unit/test_loop_detector.py +76 -0
  177. kubemend-0.4.0/tests/unit/test_loop_model_window.py +59 -0
  178. kubemend-0.4.0/tests/unit/test_meter.py +82 -0
  179. kubemend-0.4.0/tests/unit/test_missing_configmap_key_checker.py +88 -0
  180. kubemend-0.4.0/tests/unit/test_observability.py +233 -0
  181. kubemend-0.4.0/tests/unit/test_packaging.py +88 -0
  182. kubemend-0.4.0/tests/unit/test_path_policy.py +181 -0
  183. kubemend-0.4.0/tests/unit/test_prompts.py +61 -0
  184. kubemend-0.4.0/tests/unit/test_redaction.py +84 -0
  185. kubemend-0.4.0/tests/unit/test_retry_policy.py +120 -0
  186. kubemend-0.4.0/tests/unit/test_scenario_loader.py +117 -0
  187. kubemend-0.4.0/tests/unit/test_scope_trap_checker.py +93 -0
  188. kubemend-0.4.0/tests/unit/test_smoke.py +12 -0
  189. kubemend-0.4.0/tests/unit/test_truncation.py +76 -0
  190. kubemend-0.4.0/tests/unit/test_validator.py +623 -0
  191. kubemend-0.4.0/tests/unit/test_verification_path.py +169 -0
  192. kubemend-0.4.0/uv.lock +1117 -0
@@ -0,0 +1,7 @@
1
+ Create a new fault-injection scenario named $ARGUMENTS.
2
+
3
+ 1. Read docs/knowledge/lab-and-evals.md (scenario format + checker rules) first.
4
+ 2. Propose scenario.yaml (scope, task_prompt written as an on-call human would phrase it, expected_outcome, symptom_probe) and the break.patch against lab/gitops — show me both before writing files.
5
+ 3. Write checker.py asserting PROPERTIES only (never diff equality); include at least: gate passed (or handoff-without-PR for negative scenarios), scope compliance, and one scenario-specific rendered-value predicate.
6
+ 4. Run it once end-to-end with the cheap model: `task evals -- -s $ARGUMENTS -n 1 --model cheap`, and paste the checker report.
7
+ 5. If it fails, write the triage note per the eval rules before changing anything.
@@ -0,0 +1,5 @@
1
+ Replay and analyze trace $ARGUMENTS.
2
+
3
+ 1. Run `kubemend trace replay $ARGUMENTS` and reconstruct the run timeline: iterations, tool calls with durations/raw_bytes, truncations, verdicts, cost.
4
+ 2. Identify the decisive moment (wrong hypothesis, missed evidence due to truncation, loop, gate failure) with event references.
5
+ 3. Recommend exactly one of: new unit fixture, new scenario, tool contract change, prompt change — with a one-paragraph justification referencing harness-design.md trade-offs.
@@ -0,0 +1,6 @@
1
+ Run an eval sweep and summarize.
2
+
3
+ 1. Ensure the lab is up (`task lab:up` is idempotent).
4
+ 2. Run: `task evals -- --scenarios all -n ${1:-5} --model ${2:-cheap}`.
5
+ 3. Present the report table (pass, iters avg, cost avg, p95 wall) and compare against the latest committed report in evals/reports/ — call out every regression explicitly.
6
+ 4. For each scenario below its previous pass rate, open the worst failing trace and give a one-paragraph diagnosis per docs/knowledge/lab-and-evals.md triage rules. Do NOT change prompts or code in this session.
@@ -0,0 +1,16 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(task test:*)", "Bash(task lint:*)", "Bash(task evals:*)",
5
+ "Bash(task lab:up)", "Bash(task lab:forward)",
6
+ "Bash(uv run:*)", "Bash(uv add:*)",
7
+ "Bash(git status)", "Bash(git diff:*)", "Bash(git log:*)", "Bash(git add:*)", "Bash(git commit:*)"
8
+ ],
9
+ "deny": [
10
+ "Bash(kubectl apply:*)", "Bash(kubectl delete:*)", "Bash(kubectl edit:*)", "Bash(kubectl patch:*)",
11
+ "Bash(helm install:*)", "Bash(helm upgrade:*)", "Bash(helm uninstall:*)",
12
+ "Bash(git push --force:*)", "Bash(git push origin main:*)",
13
+ "Read(.lab/**)"
14
+ ]
15
+ }
16
+ }
@@ -0,0 +1,19 @@
1
+ .git
2
+ .github
3
+ .venv
4
+ .lab
5
+ .mypy_cache
6
+ .pytest_cache
7
+ .ruff_cache
8
+ __pycache__
9
+ *.pyc
10
+ traces
11
+ evals/reports
12
+ tests
13
+ docs
14
+ charts
15
+ lab
16
+ *.md
17
+ !README.md
18
+ .dockerignore
19
+ Dockerfile
@@ -0,0 +1,38 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ check:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - uses: astral-sh/setup-uv@v5
15
+ with:
16
+ enable-cache: true
17
+
18
+ - uses: go-task/setup-task@v1
19
+ with:
20
+ version: 3.x
21
+
22
+ - name: Install dependencies
23
+ run: uv sync --locked
24
+
25
+ # ruff check + ruff format --check + mypy --strict, zero warnings.
26
+ - name: Lint and typecheck
27
+ run: task lint
28
+
29
+ # Unit tests only; `lab`-marked integration tests need a kind cluster
30
+ # and `docker`-marked ones need a Docker daemon — both are excluded by
31
+ # the default pytest addopts.
32
+ - name: Unit tests
33
+ run: task test
34
+
35
+ # helm lint + a render smoke test across the chart's real value
36
+ # permutations (default, cluster-scoped RBAC, job-enabled).
37
+ - name: Helm chart lint
38
+ run: task chart:lint
@@ -0,0 +1,145 @@
1
+ name: Release
2
+
3
+ # Triggered by pushing an annotated tag (v0.1, v0.2, ...). GitHub reads this
4
+ # workflow from the tag's own commit, so it only fires for tags pushed after
5
+ # this file first landed on main. workflow_dispatch is a manual fallback to
6
+ # (re-)publish the release for a tag that already exists, without pushing
7
+ # anything new — e.g. if the tag-push run failed partway through.
8
+ on:
9
+ push:
10
+ tags: ["v*"]
11
+ workflow_dispatch:
12
+ inputs:
13
+ tag:
14
+ description: "Existing tag to (re-)publish a release for"
15
+ required: true
16
+ type: string
17
+
18
+ jobs:
19
+ release:
20
+ runs-on: ubuntu-latest
21
+ permissions:
22
+ contents: write
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ with:
26
+ ref: ${{ inputs.tag || github.ref }}
27
+
28
+ - name: Create GitHub Release
29
+ uses: softprops/action-gh-release@v2
30
+ with:
31
+ tag_name: ${{ inputs.tag || github.ref_name }}
32
+ # Auto-generated from merged PRs since the previous tag (or since
33
+ # the repo's start, for the first release).
34
+ generate_release_notes: true
35
+
36
+ # -- M8a: multi-arch container image, published to ghcr.io ------------------
37
+ #
38
+ # Native arm64 + amd64 runners (ubuntu-24.04-arm is free and unlimited on
39
+ # public repos as of 2025) rather than QEMU emulation — each leg builds and
40
+ # pushes its own single-arch image by digest, and merge-manifest below
41
+ # combines the two digests into one multi-arch manifest under the release
42
+ # tag. Splitting it this way is the documented way to avoid QEMU: a single
43
+ # buildx invocation targeting both platforms on one runner would emulate
44
+ # whichever platform isn't native to that runner.
45
+ build-container:
46
+ strategy:
47
+ matrix:
48
+ include:
49
+ - platform: linux/amd64
50
+ runs-on: ubuntu-24.04
51
+ digest-name: amd64
52
+ - platform: linux/arm64
53
+ runs-on: ubuntu-24.04-arm
54
+ digest-name: arm64
55
+ runs-on: ${{ matrix.runs-on }}
56
+ permissions:
57
+ packages: write
58
+ steps:
59
+ - uses: actions/checkout@v4
60
+ with:
61
+ ref: ${{ inputs.tag || github.ref }}
62
+
63
+ - uses: docker/setup-buildx-action@v3
64
+
65
+ - uses: docker/login-action@v3
66
+ with:
67
+ registry: ghcr.io
68
+ username: ${{ github.actor }}
69
+ password: ${{ secrets.GITHUB_TOKEN }}
70
+
71
+ - name: Build and push by digest
72
+ id: build
73
+ uses: docker/build-push-action@v6
74
+ with:
75
+ platforms: ${{ matrix.platform }}
76
+ push: true
77
+ outputs: type=image,name=ghcr.io/${{ github.repository }},push-by-digest=true,name-canonical=true
78
+
79
+ - name: Export digest for the merge job
80
+ run: |
81
+ mkdir -p /tmp/digests
82
+ digest="${{ steps.build.outputs.digest }}"
83
+ touch "/tmp/digests/${digest#sha256:}"
84
+
85
+ - uses: actions/upload-artifact@v4
86
+ with:
87
+ name: digests-${{ matrix.digest-name }}
88
+ path: /tmp/digests/*
89
+ if-no-files-found: error
90
+ retention-days: 1
91
+
92
+ merge-manifest:
93
+ needs: build-container
94
+ runs-on: ubuntu-24.04
95
+ permissions:
96
+ packages: write
97
+ steps:
98
+ - uses: actions/download-artifact@v4
99
+ with:
100
+ path: /tmp/digests
101
+ pattern: digests-*
102
+ merge-multiple: true
103
+
104
+ - uses: docker/login-action@v3
105
+ with:
106
+ registry: ghcr.io
107
+ username: ${{ github.actor }}
108
+ password: ${{ secrets.GITHUB_TOKEN }}
109
+
110
+ - name: Create the multi-arch manifest and push it
111
+ working-directory: /tmp/digests
112
+ run: |
113
+ set -eu
114
+ IMAGE=ghcr.io/${{ github.repository }}
115
+ TAG=${{ inputs.tag || github.ref_name }}
116
+ refs=()
117
+ for digest in *; do
118
+ refs+=("$IMAGE@sha256:$digest")
119
+ done
120
+ docker buildx imagetools create -t "$IMAGE:$TAG" -t "$IMAGE:latest" "${refs[@]}"
121
+
122
+ # -- M8a: PyPI publish --------------------------------------------------
123
+ #
124
+ # Trusted publishing (OIDC, no stored secret) — requires a one-time,
125
+ # manual registration on PyPI's own site first: pypi.org project settings
126
+ # -> "Add a new pending publisher" -> this repo, this workflow filename,
127
+ # environment left blank unless one is added below. Until that's done,
128
+ # this job will fail at the publish step with an authorization error;
129
+ # the build step itself has no such dependency and always works.
130
+ publish-pypi:
131
+ runs-on: ubuntu-latest
132
+ permissions:
133
+ id-token: write
134
+ steps:
135
+ - uses: actions/checkout@v4
136
+ with:
137
+ ref: ${{ inputs.tag || github.ref }}
138
+
139
+ - uses: astral-sh/setup-uv@v5
140
+
141
+ - name: Build sdist + wheel
142
+ run: uv build
143
+
144
+ - name: Publish to PyPI
145
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,17 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .mypy_cache/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ htmlcov/
12
+
13
+ # Lab credentials and generated kubeconfigs (never committed)
14
+ .lab/
15
+
16
+ # Per-run JSONL traces
17
+ traces/
@@ -0,0 +1 @@
1
+ 3.12