quoll-cli 0.2.0__py3-none-any.whl

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 (191) hide show
  1. quoll/__init__.py +0 -0
  2. quoll/cd/__init__.py +1 -0
  3. quoll/cd/config.py +75 -0
  4. quoll/ci/__init__.py +0 -0
  5. quoll/ci/config.py +213 -0
  6. quoll/ci/custom.py +32 -0
  7. quoll/ci/detect.py +139 -0
  8. quoll/ci/steps.py +85 -0
  9. quoll/ci/tools.py +104 -0
  10. quoll/commands/__init__.py +0 -0
  11. quoll/commands/_common.py +145 -0
  12. quoll/commands/_workspace.py +83 -0
  13. quoll/commands/add.py +104 -0
  14. quoll/commands/add_secret.py +75 -0
  15. quoll/commands/cd.py +214 -0
  16. quoll/commands/ci.py +521 -0
  17. quoll/commands/docker.py +244 -0
  18. quoll/commands/k8s.py +211 -0
  19. quoll/commands/new.py +337 -0
  20. quoll/commands/remove.py +79 -0
  21. quoll/config/__init__.py +0 -0
  22. quoll/config/quoll_toml.py +196 -0
  23. quoll/config/settings.py +22 -0
  24. quoll/config/store.py +39 -0
  25. quoll/console.py +3 -0
  26. quoll/docker/__init__.py +1 -0
  27. quoll/docker/config.py +64 -0
  28. quoll/docker/detect.py +41 -0
  29. quoll/docker/render.py +162 -0
  30. quoll/files.py +64 -0
  31. quoll/k8s/__init__.py +0 -0
  32. quoll/k8s/chart/templates/_helpers.tpl +38 -0
  33. quoll/k8s/chart/templates/deployment.yaml +93 -0
  34. quoll/k8s/chart/templates/hpa.yaml +24 -0
  35. quoll/k8s/chart/templates/ingress.yaml +35 -0
  36. quoll/k8s/chart/templates/pdb.yaml +16 -0
  37. quoll/k8s/chart/templates/service.yaml +18 -0
  38. quoll/k8s/chart/templates/serviceaccount.yaml +10 -0
  39. quoll/k8s/ci.py +39 -0
  40. quoll/k8s/config.py +97 -0
  41. quoll/k8s/deploy.py +22 -0
  42. quoll/k8s/detect.py +32 -0
  43. quoll/k8s/render.py +94 -0
  44. quoll/languages/__init__.py +121 -0
  45. quoll/languages/base.py +350 -0
  46. quoll/languages/cpp/Dockerfile.j2 +37 -0
  47. quoll/languages/cpp/__init__.py +17 -0
  48. quoll/languages/cpp/ci.py +192 -0
  49. quoll/languages/cpp/detect.py +84 -0
  50. quoll/languages/cpp/docker.py +47 -0
  51. quoll/languages/dotnet/Dockerfile.j2 +21 -0
  52. quoll/languages/dotnet/__init__.py +17 -0
  53. quoll/languages/dotnet/ci.py +88 -0
  54. quoll/languages/dotnet/detect.py +53 -0
  55. quoll/languages/dotnet/docker.py +47 -0
  56. quoll/languages/go/Dockerfile.j2 +25 -0
  57. quoll/languages/go/__init__.py +17 -0
  58. quoll/languages/go/ci.py +126 -0
  59. quoll/languages/go/detect.py +30 -0
  60. quoll/languages/go/docker.py +40 -0
  61. quoll/languages/java/Dockerfile.j2 +28 -0
  62. quoll/languages/java/__init__.py +20 -0
  63. quoll/languages/java/ci.py +163 -0
  64. quoll/languages/java/detect.py +77 -0
  65. quoll/languages/java/docker.py +45 -0
  66. quoll/languages/java/frameworks/__init__.py +1 -0
  67. quoll/languages/java/frameworks/spring_boot/Dockerfile.j2 +18 -0
  68. quoll/languages/java/frameworks/spring_boot/__init__.py +10 -0
  69. quoll/languages/java/frameworks/spring_boot/detect.py +9 -0
  70. quoll/languages/java/frameworks/spring_boot/docker.py +19 -0
  71. quoll/languages/java/frameworks/spring_boot/services.py +24 -0
  72. quoll/languages/node/Dockerfile.j2 +98 -0
  73. quoll/languages/node/__init__.py +29 -0
  74. quoll/languages/node/ci.py +200 -0
  75. quoll/languages/node/detect.py +164 -0
  76. quoll/languages/node/docker.py +141 -0
  77. quoll/languages/node/frameworks/__init__.py +1 -0
  78. quoll/languages/node/frameworks/angular/__init__.py +18 -0
  79. quoll/languages/node/frameworks/angular/ci.py +34 -0
  80. quoll/languages/node/frameworks/angular/detect.py +20 -0
  81. quoll/languages/node/frameworks/angular/docker.py +31 -0
  82. quoll/languages/node/frameworks/angular/starter.py +74 -0
  83. quoll/languages/node/frameworks/astro/__init__.py +10 -0
  84. quoll/languages/node/frameworks/astro/ci.py +17 -0
  85. quoll/languages/node/frameworks/astro/detect.py +14 -0
  86. quoll/languages/node/frameworks/astro/docker.py +17 -0
  87. quoll/languages/node/frameworks/nestjs/__init__.py +7 -0
  88. quoll/languages/node/frameworks/nestjs/detect.py +9 -0
  89. quoll/languages/node/frameworks/nestjs/docker.py +10 -0
  90. quoll/languages/node/frameworks/nextjs/Dockerfile.j2 +14 -0
  91. quoll/languages/node/frameworks/nextjs/__init__.py +8 -0
  92. quoll/languages/node/frameworks/nextjs/ci.py +29 -0
  93. quoll/languages/node/frameworks/nextjs/detect.py +13 -0
  94. quoll/languages/node/frameworks/nextjs/docker.py +41 -0
  95. quoll/languages/node/frameworks/nuxt/__init__.py +10 -0
  96. quoll/languages/node/frameworks/nuxt/ci.py +17 -0
  97. quoll/languages/node/frameworks/nuxt/detect.py +17 -0
  98. quoll/languages/node/frameworks/nuxt/docker.py +24 -0
  99. quoll/languages/node/frameworks/sveltekit/__init__.py +15 -0
  100. quoll/languages/node/frameworks/sveltekit/ci.py +15 -0
  101. quoll/languages/node/frameworks/sveltekit/detect.py +18 -0
  102. quoll/languages/node/frameworks/sveltekit/docker.py +18 -0
  103. quoll/languages/node/starter.py +42 -0
  104. quoll/languages/node/templates/express/README.md +11 -0
  105. quoll/languages/node/templates/express/env.example +2 -0
  106. quoll/languages/node/templates/express/eslint.config.js +15 -0
  107. quoll/languages/node/templates/express/gitignore +4 -0
  108. quoll/languages/node/templates/express/nvmrc +1 -0
  109. quoll/languages/node/templates/express/package.json +16 -0
  110. quoll/languages/node/templates/express/prettierignore +6 -0
  111. quoll/languages/node/templates/express/src/app.ts +27 -0
  112. quoll/languages/node/templates/express/src/server.ts +11 -0
  113. quoll/languages/node/templates/express/test/app.test.ts +29 -0
  114. quoll/languages/node/templates/express/tsconfig.build.json +8 -0
  115. quoll/languages/node/templates/express/tsconfig.json +14 -0
  116. quoll/languages/node/templates/express/vitest.config.ts +12 -0
  117. quoll/languages/php/Dockerfile.j2 +40 -0
  118. quoll/languages/php/__init__.py +19 -0
  119. quoll/languages/php/ci.py +147 -0
  120. quoll/languages/php/detect.py +75 -0
  121. quoll/languages/php/docker.py +59 -0
  122. quoll/languages/php/frameworks/__init__.py +1 -0
  123. quoll/languages/php/frameworks/laravel/__init__.py +17 -0
  124. quoll/languages/php/frameworks/laravel/ci.py +24 -0
  125. quoll/languages/php/frameworks/laravel/detect.py +13 -0
  126. quoll/languages/php/frameworks/laravel/docker.py +29 -0
  127. quoll/languages/php/frameworks/laravel/services.py +25 -0
  128. quoll/languages/php/frameworks/symfony/__init__.py +15 -0
  129. quoll/languages/php/frameworks/symfony/ci.py +22 -0
  130. quoll/languages/php/frameworks/symfony/detect.py +17 -0
  131. quoll/languages/php/frameworks/symfony/docker.py +25 -0
  132. quoll/languages/python/Dockerfile.j2 +61 -0
  133. quoll/languages/python/__init__.py +19 -0
  134. quoll/languages/python/ci.py +157 -0
  135. quoll/languages/python/detect.py +175 -0
  136. quoll/languages/python/docker.py +80 -0
  137. quoll/languages/python/frameworks/__init__.py +1 -0
  138. quoll/languages/python/frameworks/django/__init__.py +16 -0
  139. quoll/languages/python/frameworks/django/ci.py +43 -0
  140. quoll/languages/python/frameworks/django/detect.py +17 -0
  141. quoll/languages/python/frameworks/django/docker.py +20 -0
  142. quoll/languages/python/frameworks/fastapi/__init__.py +7 -0
  143. quoll/languages/python/frameworks/fastapi/detect.py +9 -0
  144. quoll/languages/python/frameworks/fastapi/docker.py +21 -0
  145. quoll/languages/python/frameworks/flask/__init__.py +7 -0
  146. quoll/languages/python/frameworks/flask/detect.py +9 -0
  147. quoll/languages/python/frameworks/flask/docker.py +22 -0
  148. quoll/languages/ruby/Dockerfile.j2 +45 -0
  149. quoll/languages/ruby/__init__.py +19 -0
  150. quoll/languages/ruby/ci.py +110 -0
  151. quoll/languages/ruby/detect.py +72 -0
  152. quoll/languages/ruby/docker.py +63 -0
  153. quoll/languages/ruby/frameworks/__init__.py +1 -0
  154. quoll/languages/ruby/frameworks/rails/Dockerfile.j2 +16 -0
  155. quoll/languages/ruby/frameworks/rails/__init__.py +18 -0
  156. quoll/languages/ruby/frameworks/rails/ci.py +26 -0
  157. quoll/languages/ruby/frameworks/rails/detect.py +15 -0
  158. quoll/languages/ruby/frameworks/rails/docker.py +62 -0
  159. quoll/languages/ruby/frameworks/rails/services.py +10 -0
  160. quoll/languages/rust/Dockerfile.j2 +23 -0
  161. quoll/languages/rust/__init__.py +17 -0
  162. quoll/languages/rust/ci.py +125 -0
  163. quoll/languages/rust/detect.py +46 -0
  164. quoll/languages/rust/docker.py +38 -0
  165. quoll/main.py +50 -0
  166. quoll/platforms/__init__.py +62 -0
  167. quoll/platforms/base.py +103 -0
  168. quoll/platforms/github/__init__.py +4 -0
  169. quoll/platforms/github/actions.py +78 -0
  170. quoll/platforms/github/api.py +124 -0
  171. quoll/platforms/github/cd.py +265 -0
  172. quoll/platforms/github/ci.py +497 -0
  173. quoll/platforms/github/dependabot.yml.j2 +49 -0
  174. quoll/platforms/github/platform.py +52 -0
  175. quoll/platforms/github/workflow.py +74 -0
  176. quoll/platforms/gitlab/__init__.py +4 -0
  177. quoll/platforms/gitlab/api.py +158 -0
  178. quoll/platforms/gitlab/cd.py +226 -0
  179. quoll/platforms/gitlab/ci.py +400 -0
  180. quoll/platforms/gitlab/pipeline.py +136 -0
  181. quoll/platforms/gitlab/platform.py +54 -0
  182. quoll/project.py +60 -0
  183. quoll/services.py +117 -0
  184. quoll/starters.py +117 -0
  185. quoll/workspace.py +232 -0
  186. quoll/yaml_dump.py +137 -0
  187. quoll_cli-0.2.0.dist-info/METADATA +641 -0
  188. quoll_cli-0.2.0.dist-info/RECORD +191 -0
  189. quoll_cli-0.2.0.dist-info/WHEEL +4 -0
  190. quoll_cli-0.2.0.dist-info/entry_points.txt +2 -0
  191. quoll_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,265 @@
1
+ """Turn a CDConfig into .github/workflows/cd.yml: build, scan with Trivy, then push.
2
+
3
+ In a workspace, every member with a [cd] gets its own workflow (cd-<member>.yml), run when its directory changed.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Optional, Union
9
+
10
+ from quoll.cd.config import CDConfig
11
+ from quoll.ci.steps import Step, step
12
+ from quoll.ci.tools import SBOM_FILE, image_sbom
13
+ from quoll.files import PlannedFile, header, planned
14
+ from quoll.k8s.ci import HELM_VERSION
15
+ from quoll.k8s.config import Environment, K8sConfig
16
+ from quoll.k8s.deploy import DIGEST, IMAGE, helm_upgrade, url
17
+ from quoll.platforms.github.actions import ACTIONS, gha_cache_scope
18
+ from quoll.platforms.github.workflow import checkout, dump, render_steps
19
+ from quoll.workspace import Delivery
20
+
21
+ WORKFLOW_PATH = Path(".github/workflows/cd.yml")
22
+
23
+ _TAGS = """type=ref,event=branch
24
+ type=sha
25
+ type=semver,pattern={{version}}
26
+ type=semver,pattern={{major}}.{{minor}}
27
+ type=raw,value=latest,enable={{is_default_branch}}"""
28
+
29
+ # On v* tags: the release (created if needed, with generated notes) gets the image's SBOM.
30
+ _RELEASE = """case "$GITHUB_REF" in
31
+ refs/tags/v*)
32
+ if ! gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
33
+ gh release create "$GITHUB_REF_NAME" --generate-notes --verify-tag
34
+ fi
35
+ gh release upload "$GITHUB_REF_NAME" sbom.cdx.json --clobber ;;
36
+ esac"""
37
+
38
+ # A workspace's members publish to the same release, possibly at the same time: each asset carries its name.
39
+ _MEMBER_RELEASE = """case "$GITHUB_REF" in
40
+ refs/tags/v*)
41
+ if ! gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
42
+ # Another member may create it meanwhile.
43
+ gh release create "$GITHUB_REF_NAME" --generate-notes --verify-tag \\
44
+ || gh release view "$GITHUB_REF_NAME" >/dev/null
45
+ fi
46
+ cp sbom.cdx.json "{name}-sbom.cdx.json"
47
+ gh release upload "$GITHUB_REF_NAME" "{name}-sbom.cdx.json" --clobber ;;
48
+ esac"""
49
+
50
+ _SUMMARY = """{
51
+ echo "### Published"
52
+ echo
53
+ # shellcheck disable=SC2016 # Markdown's backquotes
54
+ echo "$TAGS" | sed 's/^/- `/; s/$/`/'
55
+ } >> "$GITHUB_STEP_SUMMARY\""""
56
+
57
+
58
+ def render_workflow(cfg: CDConfig, name: str = "", directory: str = ".", k8s: Optional[K8sConfig] = None) -> str:
59
+ """The delivery of a project, or of the workspace member `name` living in `directory`; deployed to each of
60
+ `k8s`'s environments when it has a chart."""
61
+ gha_cache = gha_cache_scope(name)
62
+ push: Dict[str, Any] = {}
63
+ if cfg.branches:
64
+ push["branches"] = list(cfg.branches)
65
+ if cfg.release_tags:
66
+ push["tags"] = ["v*"]
67
+ if name:
68
+ # Branch pushes only publish what changed (tag pushes publish everything).
69
+ push["paths"] = [f"{directory}/**", "quoll.toml", workflow_path(name).as_posix()]
70
+ ghcr = cfg.registry == "ghcr"
71
+
72
+ steps: List[Step] = [checkout()]
73
+ if any(not p.endswith("amd64") for p in cfg.platforms):
74
+ steps.append(step("Set up QEMU", uses=ACTIONS["setup-qemu"]))
75
+ steps += [
76
+ step("Set up Docker Buildx", uses=ACTIONS["setup-buildx"]),
77
+ step(
78
+ "Image metadata",
79
+ id_="meta",
80
+ uses=ACTIONS["metadata"],
81
+ with_={"images": cfg.image, "tags": _TAGS},
82
+ ),
83
+ ]
84
+ if cfg.scan:
85
+ steps += [
86
+ # Built and loaded locally first: a vulnerable image never reaches the registry.
87
+ step(
88
+ "Build image to scan",
89
+ uses=ACTIONS["build-push"],
90
+ with_={
91
+ "context": directory,
92
+ "platforms": cfg.platforms[0],
93
+ "load": True,
94
+ "tags": "scan:${{ github.sha }}",
95
+ "cache-from": gha_cache,
96
+ "cache-to": f"{gha_cache},mode=max",
97
+ },
98
+ ),
99
+ step(
100
+ "Scan image",
101
+ uses=ACTIONS["trivy"],
102
+ with_={
103
+ "image-ref": "scan:${{ github.sha }}",
104
+ "severity": ",".join(cfg.severity),
105
+ "ignore-unfixed": cfg.ignore_unfixed,
106
+ "exit-code": "1",
107
+ },
108
+ ),
109
+ ]
110
+ steps += [
111
+ step(
112
+ "Log in to the registry",
113
+ uses=ACTIONS["login"],
114
+ with_={"registry": "ghcr.io", "username": "${{ github.actor }}", "password": "${{ secrets.GITHUB_TOKEN }}"}
115
+ if ghcr
116
+ else {"username": "${{ secrets.DOCKERHUB_USERNAME }}", "password": "${{ secrets.DOCKERHUB_TOKEN }}"},
117
+ ),
118
+ step(
119
+ "Push image",
120
+ id_="push",
121
+ uses=ACTIONS["build-push"],
122
+ with_={
123
+ "context": directory,
124
+ "platforms": ",".join(cfg.platforms),
125
+ "push": True,
126
+ "tags": "${{ steps.meta.outputs.tags }}",
127
+ "labels": "${{ steps.meta.outputs.labels }}",
128
+ "cache-from": gha_cache,
129
+ "cache-to": f"{gha_cache},mode=max",
130
+ # Supply-chain metadata stored next to the image.
131
+ "provenance": "mode=max",
132
+ "sbom": True,
133
+ },
134
+ ),
135
+ ]
136
+ steps += _supply_chain(cfg, ghcr, name, directory)
137
+ steps.append(step("Summary", run=_SUMMARY, env={"TAGS": "${{ steps.meta.outputs.tags }}"}))
138
+
139
+ permissions = {"contents": "read"}
140
+ if cfg.sbom and cfg.release_tags:
141
+ permissions["contents"] = "write" # attach the SBOM to the release
142
+ if ghcr:
143
+ permissions["packages"] = "write"
144
+ if cfg.sign:
145
+ permissions.update({"id-token": "write", "attestations": "write", "artifact-metadata": "write"})
146
+ job: Dict[str, Any] = {
147
+ "name": "Build, scan and push the image" if cfg.scan else "Build and push the image",
148
+ "runs-on": "ubuntu-latest",
149
+ "timeout-minutes": 30,
150
+ "permissions": permissions,
151
+ }
152
+ if directory != ".":
153
+ job["defaults"] = {"run": {"working-directory": directory}}
154
+ if k8s and k8s.environments:
155
+ job["outputs"] = {"digest": "${{ steps.push.outputs.digest }}"}
156
+ job["steps"] = render_steps(steps)
157
+ jobs = {"image": job}
158
+ for environment, settings in (k8s.environments.items() if k8s else ()):
159
+ jobs[f"deploy-{environment}"] = _deploy_job(cfg, k8s, environment, settings, name, directory)
160
+ doc = {
161
+ "name": f"CD ({name})" if name else "CD",
162
+ "on": {"push": push, "workflow_dispatch": None},
163
+ "permissions": {"contents": "read"},
164
+ # Never cancel a publication halfway; queue the next one instead.
165
+ "concurrency": {"group": "${{ github.workflow }}-${{ github.ref }}", "cancel-in-progress": False},
166
+ "jobs": jobs,
167
+ }
168
+ return header("cd") + dump(doc)
169
+
170
+
171
+ def _deploy_job(
172
+ cfg: CDConfig, k8s: K8sConfig, environment: str, settings: Environment, name: str, directory: str
173
+ ) -> Dict[str, Any]:
174
+ """Deploy the image just published, by digest, once its signature checks out. KUBECONFIG is a secret of the
175
+ GitHub environment: a cluster each; protect production's with required reviewers."""
176
+ on = "github.ref_type == 'branch'" if settings.deploy_on == "branch" else "startsWith(github.ref, 'refs/tags/v')"
177
+ env = {IMAGE: cfg.image, DIGEST: "${{ needs.image.outputs.digest }}"}
178
+ helm = step("Set up Helm", uses=ACTIONS["setup-helm"], with_={"version": f"v{HELM_VERSION}"})
179
+ steps: List[Step] = [checkout(), helm]
180
+ if cfg.sign:
181
+ verify = f'gh attestation verify "oci://{_registry_name(cfg.image)}@${DIGEST}" --repo "$GITHUB_REPOSITORY"'
182
+ steps.append(step("Verify the image's provenance", run=verify, env={**env, "GH_TOKEN": "${{ github.token }}"}))
183
+ deploy = f"{_KUBECONFIG}\n{helm_upgrade(k8s, environment, settings)}"
184
+ kubeconfig = {"KUBECONFIG_DATA": "${{ secrets.KUBECONFIG }}"}
185
+ steps.append(step(f"Deploy to {environment}", run=deploy, env={**env, **kubeconfig}))
186
+ target: Dict[str, Any] = {"name": environment}
187
+ address = url(settings, k8s.tls)
188
+ if address:
189
+ target["url"] = address
190
+ job: Dict[str, Any] = {
191
+ "name": f"Deploy to {environment}",
192
+ "needs": "image",
193
+ "if": "${{ " + on + " }}",
194
+ "runs-on": "ubuntu-latest",
195
+ "timeout-minutes": 15,
196
+ "environment": target,
197
+ # One deployment at a time per environment; the next one waits.
198
+ "concurrency": {"group": f"deploy-{environment}-{name or 'app'}", "cancel-in-progress": False},
199
+ "permissions": {"contents": "read", "attestations": "read"},
200
+ }
201
+ if directory != ".":
202
+ job["defaults"] = {"run": {"working-directory": directory}}
203
+ job["steps"] = render_steps(steps)
204
+ return job
205
+
206
+
207
+ def _supply_chain(cfg: CDConfig, ghcr: bool, name: str = "", directory: str = ".") -> List[Step]:
208
+ """SBOM of what was pushed (by digest), signed attestations, the SBOM attached to the release."""
209
+ # Commands run in the project's directory; actions' paths are given from the repository root.
210
+ sbom_path = SBOM_FILE if directory == "." else f"{directory}/{SBOM_FILE}"
211
+ digest = "${{ steps.push.outputs.digest }}"
212
+ steps: List[Step] = []
213
+ if cfg.sbom:
214
+ user, password = (
215
+ ("${{ github.actor }}", "${{ secrets.GITHUB_TOKEN }}")
216
+ if ghcr
217
+ else ("${{ secrets.DOCKERHUB_USERNAME }}", "${{ secrets.DOCKERHUB_TOKEN }}")
218
+ )
219
+ credentials = {"TRIVY_USERNAME": user, "TRIVY_PASSWORD": password}
220
+ steps += [
221
+ step("SBOM of the image", run=image_sbom(), env={"IMAGE": cfg.image, "DIGEST": digest, **credentials}),
222
+ step(
223
+ "Upload SBOM",
224
+ uses=ACTIONS["upload-artifact"],
225
+ with_={"name": f"{name}-image-sbom" if name else "image-sbom", "path": sbom_path},
226
+ ),
227
+ ]
228
+ if cfg.sign:
229
+ subject = {"subject-name": _registry_name(cfg.image), "subject-digest": digest, "push-to-registry": True}
230
+ steps.append(step("Attest build provenance", uses=ACTIONS["attest"], with_=subject))
231
+ if cfg.sbom:
232
+ steps.append(step("Attest SBOM", uses=ACTIONS["attest"], with_={**subject, "sbom-path": sbom_path}))
233
+ if cfg.sbom and cfg.release_tags:
234
+ release = _MEMBER_RELEASE.format(name=name) if name else _RELEASE
235
+ steps.append(step("Attach the SBOM to the release", run=release, env={"GH_TOKEN": "${{ github.token }}"}))
236
+ return steps
237
+
238
+
239
+ def _registry_name(image: str) -> str:
240
+ """Fully qualified name: Docker Hub images are attested as index.docker.io/<name>."""
241
+ first = image.split("/", 1)[0]
242
+ return image if "." in first or ":" in first else f"index.docker.io/{image}"
243
+
244
+
245
+ # The environment's kubeconfig, from its secret, for helm.
246
+ _KUBECONFIG = """if [ -z "$KUBECONFIG_DATA" ]; then
247
+ echo "::error::No KUBECONFIG secret in this GitHub environment: add the cluster's kubeconfig to it."
248
+ exit 1
249
+ fi
250
+ export KUBECONFIG="$RUNNER_TEMP/kubeconfig"
251
+ printf '%s' "$KUBECONFIG_DATA" > "$KUBECONFIG"
252
+ chmod 600 "$KUBECONFIG\""""
253
+
254
+
255
+ def workflow_path(name: str = "") -> Path:
256
+ return Path(f".github/workflows/cd-{name}.yml") if name else WORKFLOW_PATH
257
+
258
+
259
+ def plan(project_dir: Path, deliveries: Union[CDConfig, List[Delivery]]) -> List[PlannedFile]:
260
+ if isinstance(deliveries, CDConfig):
261
+ deliveries = [Delivery("", ".", deliveries)]
262
+ return [
263
+ planned(project_dir, workflow_path(d.name), render_workflow(d.cfg, d.name, d.directory, d.k8s))
264
+ for d in deliveries
265
+ ]