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.
- quoll/__init__.py +0 -0
- quoll/cd/__init__.py +1 -0
- quoll/cd/config.py +75 -0
- quoll/ci/__init__.py +0 -0
- quoll/ci/config.py +213 -0
- quoll/ci/custom.py +32 -0
- quoll/ci/detect.py +139 -0
- quoll/ci/steps.py +85 -0
- quoll/ci/tools.py +104 -0
- quoll/commands/__init__.py +0 -0
- quoll/commands/_common.py +145 -0
- quoll/commands/_workspace.py +83 -0
- quoll/commands/add.py +104 -0
- quoll/commands/add_secret.py +75 -0
- quoll/commands/cd.py +214 -0
- quoll/commands/ci.py +521 -0
- quoll/commands/docker.py +244 -0
- quoll/commands/k8s.py +211 -0
- quoll/commands/new.py +337 -0
- quoll/commands/remove.py +79 -0
- quoll/config/__init__.py +0 -0
- quoll/config/quoll_toml.py +196 -0
- quoll/config/settings.py +22 -0
- quoll/config/store.py +39 -0
- quoll/console.py +3 -0
- quoll/docker/__init__.py +1 -0
- quoll/docker/config.py +64 -0
- quoll/docker/detect.py +41 -0
- quoll/docker/render.py +162 -0
- quoll/files.py +64 -0
- quoll/k8s/__init__.py +0 -0
- quoll/k8s/chart/templates/_helpers.tpl +38 -0
- quoll/k8s/chart/templates/deployment.yaml +93 -0
- quoll/k8s/chart/templates/hpa.yaml +24 -0
- quoll/k8s/chart/templates/ingress.yaml +35 -0
- quoll/k8s/chart/templates/pdb.yaml +16 -0
- quoll/k8s/chart/templates/service.yaml +18 -0
- quoll/k8s/chart/templates/serviceaccount.yaml +10 -0
- quoll/k8s/ci.py +39 -0
- quoll/k8s/config.py +97 -0
- quoll/k8s/deploy.py +22 -0
- quoll/k8s/detect.py +32 -0
- quoll/k8s/render.py +94 -0
- quoll/languages/__init__.py +121 -0
- quoll/languages/base.py +350 -0
- quoll/languages/cpp/Dockerfile.j2 +37 -0
- quoll/languages/cpp/__init__.py +17 -0
- quoll/languages/cpp/ci.py +192 -0
- quoll/languages/cpp/detect.py +84 -0
- quoll/languages/cpp/docker.py +47 -0
- quoll/languages/dotnet/Dockerfile.j2 +21 -0
- quoll/languages/dotnet/__init__.py +17 -0
- quoll/languages/dotnet/ci.py +88 -0
- quoll/languages/dotnet/detect.py +53 -0
- quoll/languages/dotnet/docker.py +47 -0
- quoll/languages/go/Dockerfile.j2 +25 -0
- quoll/languages/go/__init__.py +17 -0
- quoll/languages/go/ci.py +126 -0
- quoll/languages/go/detect.py +30 -0
- quoll/languages/go/docker.py +40 -0
- quoll/languages/java/Dockerfile.j2 +28 -0
- quoll/languages/java/__init__.py +20 -0
- quoll/languages/java/ci.py +163 -0
- quoll/languages/java/detect.py +77 -0
- quoll/languages/java/docker.py +45 -0
- quoll/languages/java/frameworks/__init__.py +1 -0
- quoll/languages/java/frameworks/spring_boot/Dockerfile.j2 +18 -0
- quoll/languages/java/frameworks/spring_boot/__init__.py +10 -0
- quoll/languages/java/frameworks/spring_boot/detect.py +9 -0
- quoll/languages/java/frameworks/spring_boot/docker.py +19 -0
- quoll/languages/java/frameworks/spring_boot/services.py +24 -0
- quoll/languages/node/Dockerfile.j2 +98 -0
- quoll/languages/node/__init__.py +29 -0
- quoll/languages/node/ci.py +200 -0
- quoll/languages/node/detect.py +164 -0
- quoll/languages/node/docker.py +141 -0
- quoll/languages/node/frameworks/__init__.py +1 -0
- quoll/languages/node/frameworks/angular/__init__.py +18 -0
- quoll/languages/node/frameworks/angular/ci.py +34 -0
- quoll/languages/node/frameworks/angular/detect.py +20 -0
- quoll/languages/node/frameworks/angular/docker.py +31 -0
- quoll/languages/node/frameworks/angular/starter.py +74 -0
- quoll/languages/node/frameworks/astro/__init__.py +10 -0
- quoll/languages/node/frameworks/astro/ci.py +17 -0
- quoll/languages/node/frameworks/astro/detect.py +14 -0
- quoll/languages/node/frameworks/astro/docker.py +17 -0
- quoll/languages/node/frameworks/nestjs/__init__.py +7 -0
- quoll/languages/node/frameworks/nestjs/detect.py +9 -0
- quoll/languages/node/frameworks/nestjs/docker.py +10 -0
- quoll/languages/node/frameworks/nextjs/Dockerfile.j2 +14 -0
- quoll/languages/node/frameworks/nextjs/__init__.py +8 -0
- quoll/languages/node/frameworks/nextjs/ci.py +29 -0
- quoll/languages/node/frameworks/nextjs/detect.py +13 -0
- quoll/languages/node/frameworks/nextjs/docker.py +41 -0
- quoll/languages/node/frameworks/nuxt/__init__.py +10 -0
- quoll/languages/node/frameworks/nuxt/ci.py +17 -0
- quoll/languages/node/frameworks/nuxt/detect.py +17 -0
- quoll/languages/node/frameworks/nuxt/docker.py +24 -0
- quoll/languages/node/frameworks/sveltekit/__init__.py +15 -0
- quoll/languages/node/frameworks/sveltekit/ci.py +15 -0
- quoll/languages/node/frameworks/sveltekit/detect.py +18 -0
- quoll/languages/node/frameworks/sveltekit/docker.py +18 -0
- quoll/languages/node/starter.py +42 -0
- quoll/languages/node/templates/express/README.md +11 -0
- quoll/languages/node/templates/express/env.example +2 -0
- quoll/languages/node/templates/express/eslint.config.js +15 -0
- quoll/languages/node/templates/express/gitignore +4 -0
- quoll/languages/node/templates/express/nvmrc +1 -0
- quoll/languages/node/templates/express/package.json +16 -0
- quoll/languages/node/templates/express/prettierignore +6 -0
- quoll/languages/node/templates/express/src/app.ts +27 -0
- quoll/languages/node/templates/express/src/server.ts +11 -0
- quoll/languages/node/templates/express/test/app.test.ts +29 -0
- quoll/languages/node/templates/express/tsconfig.build.json +8 -0
- quoll/languages/node/templates/express/tsconfig.json +14 -0
- quoll/languages/node/templates/express/vitest.config.ts +12 -0
- quoll/languages/php/Dockerfile.j2 +40 -0
- quoll/languages/php/__init__.py +19 -0
- quoll/languages/php/ci.py +147 -0
- quoll/languages/php/detect.py +75 -0
- quoll/languages/php/docker.py +59 -0
- quoll/languages/php/frameworks/__init__.py +1 -0
- quoll/languages/php/frameworks/laravel/__init__.py +17 -0
- quoll/languages/php/frameworks/laravel/ci.py +24 -0
- quoll/languages/php/frameworks/laravel/detect.py +13 -0
- quoll/languages/php/frameworks/laravel/docker.py +29 -0
- quoll/languages/php/frameworks/laravel/services.py +25 -0
- quoll/languages/php/frameworks/symfony/__init__.py +15 -0
- quoll/languages/php/frameworks/symfony/ci.py +22 -0
- quoll/languages/php/frameworks/symfony/detect.py +17 -0
- quoll/languages/php/frameworks/symfony/docker.py +25 -0
- quoll/languages/python/Dockerfile.j2 +61 -0
- quoll/languages/python/__init__.py +19 -0
- quoll/languages/python/ci.py +157 -0
- quoll/languages/python/detect.py +175 -0
- quoll/languages/python/docker.py +80 -0
- quoll/languages/python/frameworks/__init__.py +1 -0
- quoll/languages/python/frameworks/django/__init__.py +16 -0
- quoll/languages/python/frameworks/django/ci.py +43 -0
- quoll/languages/python/frameworks/django/detect.py +17 -0
- quoll/languages/python/frameworks/django/docker.py +20 -0
- quoll/languages/python/frameworks/fastapi/__init__.py +7 -0
- quoll/languages/python/frameworks/fastapi/detect.py +9 -0
- quoll/languages/python/frameworks/fastapi/docker.py +21 -0
- quoll/languages/python/frameworks/flask/__init__.py +7 -0
- quoll/languages/python/frameworks/flask/detect.py +9 -0
- quoll/languages/python/frameworks/flask/docker.py +22 -0
- quoll/languages/ruby/Dockerfile.j2 +45 -0
- quoll/languages/ruby/__init__.py +19 -0
- quoll/languages/ruby/ci.py +110 -0
- quoll/languages/ruby/detect.py +72 -0
- quoll/languages/ruby/docker.py +63 -0
- quoll/languages/ruby/frameworks/__init__.py +1 -0
- quoll/languages/ruby/frameworks/rails/Dockerfile.j2 +16 -0
- quoll/languages/ruby/frameworks/rails/__init__.py +18 -0
- quoll/languages/ruby/frameworks/rails/ci.py +26 -0
- quoll/languages/ruby/frameworks/rails/detect.py +15 -0
- quoll/languages/ruby/frameworks/rails/docker.py +62 -0
- quoll/languages/ruby/frameworks/rails/services.py +10 -0
- quoll/languages/rust/Dockerfile.j2 +23 -0
- quoll/languages/rust/__init__.py +17 -0
- quoll/languages/rust/ci.py +125 -0
- quoll/languages/rust/detect.py +46 -0
- quoll/languages/rust/docker.py +38 -0
- quoll/main.py +50 -0
- quoll/platforms/__init__.py +62 -0
- quoll/platforms/base.py +103 -0
- quoll/platforms/github/__init__.py +4 -0
- quoll/platforms/github/actions.py +78 -0
- quoll/platforms/github/api.py +124 -0
- quoll/platforms/github/cd.py +265 -0
- quoll/platforms/github/ci.py +497 -0
- quoll/platforms/github/dependabot.yml.j2 +49 -0
- quoll/platforms/github/platform.py +52 -0
- quoll/platforms/github/workflow.py +74 -0
- quoll/platforms/gitlab/__init__.py +4 -0
- quoll/platforms/gitlab/api.py +158 -0
- quoll/platforms/gitlab/cd.py +226 -0
- quoll/platforms/gitlab/ci.py +400 -0
- quoll/platforms/gitlab/pipeline.py +136 -0
- quoll/platforms/gitlab/platform.py +54 -0
- quoll/project.py +60 -0
- quoll/services.py +117 -0
- quoll/starters.py +117 -0
- quoll/workspace.py +232 -0
- quoll/yaml_dump.py +137 -0
- quoll_cli-0.2.0.dist-info/METADATA +641 -0
- quoll_cli-0.2.0.dist-info/RECORD +191 -0
- quoll_cli-0.2.0.dist-info/WHEEL +4 -0
- quoll_cli-0.2.0.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|