-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTaskfile.yml
More file actions
287 lines (263 loc) · 11.8 KB
/
Taskfile.yml
File metadata and controls
287 lines (263 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
---
# https://taskfile.dev
version: '3'
set:
- nounset
- errexit
- pipefail
vars:
IMAGE_NAME: '{{ cookiecutter.github_org }}/{{ cookiecutter.project_name | lower }}'
PROJECT_SLUG: {{ cookiecutter.project_slug }}
PYTHON_VERSION: {{ cookiecutter.python_version }}
SUPPORTED_PLATFORMS: 'linux/amd64,linux/arm64'
RUN_SCRIPT: 'uv run --frozen'
SCRIPTS_DIR: 'scripts'
VERSION:
sh: "{{ '{{.RUN_SCRIPT}}' }} python -c \"import sys; sys.path.insert(0, 'src'); from {{ '{{.PROJECT_SLUG}}' }} import __version__; print(__version__)\""
LOCAL_PLATFORM:
sh: "bash {{ '{{.SCRIPTS_DIR}}' }}/get_platform.sh"
# Use PLATFORM if specified, otherwise use LOCAL_PLATFORM
PLATFORM: '{{ '{{if .PLATFORM}}' }}{{ '{{.PLATFORM}}' }}{{ '{{else}}' }}{{ '{{.LOCAL_PLATFORM}}' }}{{ '{{end}}' }}'
# Output redirect based on CI environment
OUTPUT_REDIRECT: '{{ '{{if eq .GITHUB_ACTIONS "true"}}' }}| tee{{ '{{else}}' }}>{{ '{{end}}' }}'
silent: true
tasks:
init-uv:
desc: Initializes the uv virtual environment
internal: true
sources:
- pyproject.toml
- uv.lock
preconditions:
- which uv
cmds:
# Sync dependencies with uv
- uv sync --frozen --all-extras
init-pre-commit:
desc: Install the pre-commit hooks
internal: true
sources:
- .pre-commit-config.yaml
status:
# Don't do any of this if you aren't in a git repository; quote to avoid yaml intrepretering the ! as a node tag
# https://yaml.org/spec/1.2.2/#691-node-tags
- '! test -d .git'
cmds:
- uv tool install pre-commit
# Don't run this in pipelines
- '{{ '{{if ne .GITHUB_ACTIONS "true"}}{{.RUN_SCRIPT}} pre-commit install{{else}}echo "Detected a github actions pipeline; skipping the pre-commit install"{{end}}' }}'
init-docker-multiplatform:
desc: Setup docker for multiplatform builds
internal: true
cmds:
# This fixes an "ERROR: Multiple platforms feature is currently not supported for docker driver" pipeline error
# Only create our multiplatform builder if it doesn't already exist; otherwise list information about the one that exists
# It suppresses the inspect output when it's not running in a GitHub Action
- docker buildx inspect multiplatform {{ '{{if ne .GITHUB_ACTIONS "true"}}' }}>/dev/null{{ '{{end}}' }} 2>/dev/null || docker buildx create --name multiplatform --driver docker-container --use
init:
desc: Initialize the repo for local use; intended to be run after git clone
cmds:
- task: init-uv
- task: init-pre-commit
- task: init-docker-multiplatform
lint:
desc: Run the linter(s)
cmds:
- '{{ '{{.RUN_SCRIPT}}' }} pre-commit run --all-files'
validate:
desc: Validate the pre-commit config and hooks files
cmds:
- '{{ '{{.RUN_SCRIPT}}' }} pre-commit validate-config'
- '{{ '{{.RUN_SCRIPT}}' }} pre-commit validate-manifest'
- '{{ '{{.RUN_SCRIPT}}' }} python scripts/validate_service_definition.py'
build:
desc: Build the project; docker images, compiled binaries, etc.
vars:
PUBLISH: '{{ '{{.PUBLISH | default "false"}}' }}'
TIMESTAMP:
sh: '{{ '{{.RUN_SCRIPT}}' }} {{ '{{.SCRIPTS_DIR}}' }}/get_rfc3339_timestamp.py'
EPOCH:
sh: 'bash {{ '{{.SCRIPTS_DIR}}' }}/get_epoch.sh'
COMMIT_HASH:
sh: git rev-parse HEAD
BUILD_PLATFORM: '{{ '{{if eq .PLATFORM "all"}}' }}{{ '{{.SUPPORTED_PLATFORMS}}' }}{{ '{{else if .PLATFORM}}' }}{{ '{{.PLATFORM}}' }}{{ '{{else}}' }}{{ '{{.LOCAL_PLATFORM}}' }}{{ '{{end}}' }}'
PLATFORM_SUFFIX: '{{ '{{if eq .PLATFORM "all"}}' }}all{{ '{{else if .PLATFORM}}' }}{{ '{{.PLATFORM | replace "/" "_"}}' }}{{ '{{else}}' }}{{ '{{.LOCAL_PLATFORM | replace "/" "_"}}' }}{{ '{{end}}' }}'
# We always output to "latest", since we're also overwriting latest
OUTPUT_FILE: '{{ '{{.IMAGE_NAME | replace "/" "_"}}' }}_latest_{{ '{{.PLATFORM_SUFFIX}}' }}.tar'
DESCRIPTION: "{{ cookiecutter.project_short_description | replace('"', '\\"') | replace("'", "\\\\'") }}"
cmds:
# First build: load if same platform, output to file if cross-platform, or push if PUBLISH is true
- |
docker buildx build \
--platform {{ '{{.BUILD_PLATFORM}}' }} \
--pull \
{{ '{{if eq .PUBLISH "true"}}' }}--push{{ "{{else if eq .PLATFORM .LOCAL_PLATFORM}}" }}--load{{ "{{else}}" }}-o type=oci,dest="{{ '{{.OUTPUT_FILE}}' }}"{{ "{{end}}" }} \
{{ '{{if eq .GITHUB_ACTIONS "true"}}' }}--cache-from type=gha --cache-to type=gha,mode=max{{ '{{end}}' }} \
--build-arg NAME="{{ '{{.PROJECT_SLUG}}' }}" \
--build-arg DESCRIPTION="{{ '{{.DESCRIPTION}}' }}" \
--build-arg TIMESTAMP="{{ '{{.TIMESTAMP}}' }}" \
--build-arg COMMIT_HASH="{{ '{{.COMMIT_HASH}}' }}" \
-t {{ '{{.IMAGE_NAME}}:{{.VERSION}}' }} \
-t {{ '{{.IMAGE_NAME}}:latest' }} \
-t {{ '{{.IMAGE_NAME}}:{{.EPOCH}}' }} \
.
# Second build: only for same platform to also output to file
- |
{{ '{{if eq .PLATFORM .LOCAL_PLATFORM}}' }}
docker buildx build \
--platform {{ '{{.BUILD_PLATFORM}}' }} \
-o type=oci,dest="{{ '{{.OUTPUT_FILE}}' }}" \
{{ '{{if eq .GITHUB_ACTIONS "true"}}' }}--cache-from type=gha{{ '{{end}}' }} \
--build-arg NAME="{{ '{{.PROJECT_SLUG}}' }}" \
--build-arg DESCRIPTION="{{ '{{.DESCRIPTION}}' }}" \
--build-arg TIMESTAMP="{{ '{{.TIMESTAMP}}' }}" \
--build-arg COMMIT_HASH="{{ '{{.COMMIT_HASH}}' }}" \
-t {{ '{{.IMAGE_NAME}}:{{.VERSION}}' }} \
-t {{ '{{.IMAGE_NAME}}:latest' }} \
-t {{ '{{.IMAGE_NAME}}:{{.EPOCH}}' }} \
.
{{ '{{end}}' }}
- |
{{ '{{if and (ne .PLATFORM .LOCAL_PLATFORM) (ne .PLATFORM "all")}}' }}
echo >&2 "WARNING: Avoided loading {{ '{{.IMAGE_NAME}}' }}:latest and {{ '{{.IMAGE_NAME}}' }}:{{ '{{.EPOCH}}' }} into your Docker daemon because you built a cross-platform image of {{ '{{.PLATFORM}}' }}.
See {{ '{{.OUTPUT_FILE}}' }} for the OCI artifact."
{{ '{{end}}' }}
test:
desc: Run the project tests
deps: ["coverage-erase"]
cmds:
- task: unit-test
- task: integration-test
coverage-erase:
desc: Erase coverage
# This allows the task to be specified as as dep multiple times but only run once
run: once
cmds:
# Ensure we don't aggregate coverage from prior runs
- '{{ '{{.RUN_SCRIPT}}' }} coverage erase'
unit-test:
desc: Run the project unit tests
deps: ["coverage-erase"]
vars:
# If CLI_ARGS are set, append them as an "and" after the -m unit
MARK_EXPR: unit{{ '{{if .CLI_ARGS}}' }} and {{ '{{.CLI_ARGS}}{{end}}' }}
cmds:
- '{{ '{{.RUN_SCRIPT}}' }} pytest -m "{{ '{{.MARK_EXPR}}' }}" tests/'
integration-test:
desc: Run the project integration tests
deps: ["coverage-erase"]
vars:
# If CLI_ARGS are set, append them as an "and" after the -m integration
MARK_EXPR: integration{{ '{{if .CLI_ARGS}}' }} and {{ '{{.CLI_ARGS}}{{end}}' }}
status:
# Only run integration tests when the PLATFORM is set to all or the same platform as we're running on
- '{{ '{{if or (eq .PLATFORM "all") (eq .PLATFORM .LOCAL_PLATFORM) (not .PLATFORM)}}' }}exit 1{{ '{{else}}' }}exit 0{{ '{{end}}' }}'
cmds:
- '{{ '{{.RUN_SCRIPT}}' }} pytest -m "{{ '{{.MARK_EXPR}}' }}" tests/'
update:
desc: Update the project dev and runtime dependencies
cmds:
# Upgrade uv via brew on macOS/Linux when running locally (not CI)
- '{{ '{{if and (ne .GITHUB_ACTIONS "true") (ne OS "windows")}}' }}brew upgrade uv{{ '{{end}}' }}'
- uv tool upgrade --all
- pre-commit autoupdate --freeze --jobs 4
- uv lock --upgrade
# This can take a while but it's required for the following step to update BuildKit in the docker driver
- '{{ '{{if eq .CLI_ARGS "all"}}' }}docker buildx rm multiplatform || true{{ '{{end}}' }}'
# If we just destroyed the "multiplatform" builder instance, this will configure a new one. The next time the host runs a `docker buildx build` it will
# rebuild the builder instance, updating its BuildKit. There's no harm in running this even if we didn't do the `docker buildx rm` previously
- task: init-docker-multiplatform
clean:
desc: Clean up build artifacts, cache files/directories, temp files, etc.
cmds:
- rm -rf .pytest_cache
- rm -rf htmlcov
- rm -rf .coverage
- rm -rf dist
- rm -rf build
- rm -rf *.egg-info
- rm -f sbom.*.json
- rm -f vulns.*.json
- rm -f license-check.*.json
- rm -f {{ cookiecutter.github_org }}_{{ cookiecutter.project_slug }}_*_*.tar
- find . -type d -name __pycache__ -exec rm -rf {} + || true
- find . -type f -name '*.pyc' -delete || true
release:
desc: Cut a project release
env:
GH_TOKEN:
sh: |
if [[ -n "${GH_TOKEN:-}" ]]; then
echo "${GH_TOKEN}"
elif command -v gh &> /dev/null && gh auth token &> /dev/null; then
gh auth token
fi
cmds:
# Phase 1: Update version in files without any VCS operations
- '{{ '{{.RUN_SCRIPT}}' }} semantic-release version --no-changelog --skip-build --no-commit --no-tag --no-push --no-vcs-release'
# Re-lock dependencies to update version in uv.lock
- uv lock
# Stage the updated lock file
- git add uv.lock
# Phase 2: Run full release with commits, tags, and push; this will include the updated uv.lock
- '{{ '{{.RUN_SCRIPT}}' }} semantic-release version --no-changelog --skip-build {{ '{{.CLI_ARGS}}' }}'
sbom:
desc: Generate project SBOMs
cmds:
- |
{{ '{{.RUN_SCRIPT}}' }} {{ '{{.SCRIPTS_DIR}}' }}/scan_image.py sbom \
--platform "{{ '{{.PLATFORM}}' }}" \
--image-name "{{ '{{.IMAGE_NAME}}' }}"
vulnscan:
desc: Vuln scan the SBOM
cmds:
- |
{{ '{{.RUN_SCRIPT}}' }} {{ '{{.SCRIPTS_DIR}}' }}/scan_image.py vulnscan \
--platform "{{ '{{.PLATFORM}}' }}" \
--image-name "{{ '{{.IMAGE_NAME}}' }}"
license-check:
desc: Check license compliance using grant
cmds:
- |
{{ '{{.RUN_SCRIPT}}' }} {{ '{{.SCRIPTS_DIR}}' }}/scan_image.py license-check \
--platform "{{ '{{.PLATFORM}}' }}" \
--image-name "{{ '{{.IMAGE_NAME}}' }}"
brew-install:
desc: Install a tool via brew
internal: true
requires:
vars: [TOOLS]
vars:
FORCE_LINK: '{{ '{{.FORCE_LINK | default "false"}}' }}'
DEBUG: '{{ '{{if ne .ZENABLE_LOGLEVEL "DEBUG"}}' }}> /dev/null 2>&1{{ '{{end}}' }}'
env:
HOMEBREW_NO_INSTALL_UPGRADE: '{{ '{{if .GITHUB_ACTIONS}}' }}true{{ '{{end}}' }}'
cmds:
- 'echo "Installing tools with brew: {{ '{{.TOOLS}}' }}"'
- for:
var: TOOLS
split: ','
as: tool
# If the command fails, attempt to retry once before failing
cmd: brew install {{ '{{.tool}}' }} {{ '{{.DEBUG}}' }} || brew install {{ '{{.tool}}' }} {{ '{{.DEBUG}}' }}
- for:
var: TOOLS
split: ','
as: tool
cmd: '{{ '{{if eq .FORCE_LINK "true"}}' }}brew link --force {{ '{{.tool}}' }}{{ '{{else}}' }}true{{ '{{end}}' }}'
{%- if cookiecutter.dockerhub == 'yes' %}
publish:
desc: Publish the project artifacts; docker images, compiled binaries, etc.
requires:
vars:
- VERSION
cmds:
- task: build
vars:
PUBLISH: 'true'
VERSION: '{{ '{{.VERSION}}' }}'
PLATFORM: '{{ '{{.PLATFORM | default "all"}}' }}'
DOCKER_BUILDX_CUSTOM_ARGS: '{{ '{{.DOCKER_BUILDX_CUSTOM_ARGS | default ""}}' }}'
DOCKER_BUILDX_CUSTOM_TAGS: '{{ '{{.DOCKER_BUILDX_CUSTOM_TAGS | default ""}}' }}'
DOCKER_BUILDX_CUSTOM_CONTEXT: '{{ '{{.DOCKER_BUILDX_CUSTOM_CONTEXT}}' }}'
{%- endif %}