-
Notifications
You must be signed in to change notification settings - Fork 0
306 lines (290 loc) · 10.9 KB
/
ci.yml
File metadata and controls
306 lines (290 loc) · 10.9 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
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
---
name: "CI"
permissions:
contents: read
on:
push:
branches:
- main
pull_request:
branches:
- main
schedule:
- cron: '47 5 * * 0'
env:
python_version: "3.13"
defaults:
run:
shell: 'bash --noprofile --norc -Eeuo pipefail {0}'
jobs:
lint:
name: Lint
runs-on: ubuntu-24.04
steps:
- name: Checkout the repository
uses: actions/checkout@v6
with:
persist-credentials: 'false'
- name: Bootstrap repository
uses: ./.github/actions/bootstrap
with:
token: ${{ secrets.GITHUB_TOKEN }}
python-version: ${{ env.python_version }}
- name: Lint
run: task -v lint
test:
name: Test
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v6
# Necessary for hooks to succeed during tests for commits/schedule
if: github.event_name != 'pull_request'
with:
fetch-depth: 0
persist-credentials: 'false'
- name: Checkout the repository
uses: actions/checkout@v6
# Necessary for hooks to succeed during tests for PRs
if: github.event_name == 'pull_request'
with:
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0
persist-credentials: 'false'
- name: Bootstrap repository
uses: ./.github/actions/bootstrap
with:
token: ${{ secrets.GITHUB_TOKEN }}
python-version: ${{ env.python_version }}
- name: Validate the repo
run: task -v validate
- name: Install license compliance tool
run: |
mkdir "${RUNNER_TEMP}/bin"
# Install grant via curl until official Docker image is available
# See: https://github.com/anchore/grant/issues/222
curl -sSfL https://raw.githubusercontent.com/anchore/grant/main/install.sh | sh -s -- -b "${RUNNER_TEMP}/bin"
chmod +x "${RUNNER_TEMP}/bin/grant"
echo "${RUNNER_TEMP}/bin" | tee -a "${GITHUB_PATH}"
- name: Run the tests
run: task -v test
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Run SBOM generation
run: task -v sbom
- name: Upload SBOM artifacts
uses: actions/upload-artifact@v7
with:
name: sbom-files
path: |
sbom.*.json
if-no-files-found: error
- name: Check license compliance
run: task -v license-check
- name: Upload license check results
uses: actions/upload-artifact@v7
with:
name: license-check-results
path: license-check.json
if-no-files-found: error
- name: Run vulnerability scan
run: task -v vulnscan
- name: Upload vulnerability scan results
uses: actions/upload-artifact@v7
with:
name: vuln-scan-results
path: vulns.json
if-no-files-found: error
windows-smoke-test:
name: Windows Smoke Test
runs-on: windows-latest
steps:
- name: Setup uv
uses: astral-sh/setup-uv@v7
with:
python-version: ${{ env.python_version }}
enable-cache: false
ignore-empty-workdir: true
- name: Install Task
uses: go-task/setup-task@v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Generate project from template
shell: bash
env:
RUN_POST_HOOK: 'true'
SKIP_GIT_PUSH: 'true'
TEMPLATE_REPO: ${{ github.repository }}
TEMPLATE_REF: ${{ github.head_ref || github.ref_name }}
run: |
git config --global user.name "CI Automation"
git config --global user.email "ci@zenable.io"
# Use the same command as README.md (gh: syntax). On Windows, if the
# default branch still has NTFS-illegal paths (pre-merge), the gh: clone
# fails because cookiecutter clones main first. Fall back to a local
# clone in that case. After merge, the gh: command succeeds directly.
uvx --with gitpython cookiecutter \
"gh:${TEMPLATE_REPO}" \
--checkout "${TEMPLATE_REF}" \
--no-input project_name="ci-test-project" \
--output-dir "$RUNNER_TEMP" \
|| {
echo "::warning::gh: clone failed (expected pre-merge on Windows). Using local clone."
template_dir="$RUNNER_TEMP/template-repo"
git clone --no-checkout "https://github.com/${TEMPLATE_REPO}.git" "$template_dir"
git -C "$template_dir" checkout "${TEMPLATE_REF}"
uvx --with gitpython cookiecutter "$template_dir" \
--no-input project_name="ci-test-project" \
--output-dir "$RUNNER_TEMP"
}
- name: Verify generated project
shell: pwsh
run: |
$project = Join-Path $env:RUNNER_TEMP "ci-test-project"
# Verify the project directory was created
if (-not (Test-Path $project)) {
Write-Error "Project directory not found at $project"
exit 1
}
# Verify key files exist
$requiredFiles = @(
"pyproject.toml",
"Taskfile.yml",
"Dockerfile",
"CLAUDE.md",
".github/project.yml",
".github/workflows/ci.yml"
)
foreach ($file in $requiredFiles) {
$filePath = Join-Path $project $file
if (-not (Test-Path $filePath)) {
Write-Error "Required file missing: $file"
exit 1
}
}
# Verify no unrendered cookiecutter variables remain
$pattern = '\{\{\s*cookiecutter\.'
$matches = Get-ChildItem -Path $project -Recurse -File -Exclude '.git' |
Where-Object { $_.FullName -notmatch '[\\/]\.git[\\/]' } |
Select-String -Pattern $pattern
if ($matches) {
Write-Error "Unrendered cookiecutter variables found:"
$matches | ForEach-Object { Write-Error $_.ToString() }
exit 1
}
# Verify git repo was initialized and has a commit
$gitDir = Join-Path $project ".git"
if (-not (Test-Path $gitDir)) {
Write-Error "Git repository not initialized"
exit 1
}
Push-Location $project
$commitCount = git rev-list --count HEAD 2>$null
Pop-Location
if ($commitCount -lt 1) {
Write-Error "No commits found in generated project"
exit 1
}
Write-Host "Windows smoke test passed: project generated and verified successfully"
- name: Verify shell scripts have LF line endings
shell: pwsh
run: |
$project = Join-Path $env:RUNNER_TEMP "ci-test-project"
$failed = $false
# Check all .sh files for CRLF
Get-ChildItem -Path $project -Recurse -Filter "*.sh" | ForEach-Object {
$bytes = [System.IO.File]::ReadAllBytes($_.FullName)
$content = [System.Text.Encoding]::UTF8.GetString($bytes)
if ($content -match "`r`n") {
Write-Error "$($_.Name) has CRLF line endings - this breaks bash on Windows"
$failed = $true
}
}
# Check Dockerfile
$dockerfile = Join-Path $project "Dockerfile"
if (Test-Path $dockerfile) {
$bytes = [System.IO.File]::ReadAllBytes($dockerfile)
$content = [System.Text.Encoding]::UTF8.GetString($bytes)
if ($content -match "`r`n") {
Write-Error "Dockerfile has CRLF line endings - this breaks Docker builds"
$failed = $true
}
}
if ($failed) { exit 1 }
Write-Host "All shell scripts and Dockerfile have correct LF line endings"
- name: Setup WSL with Docker
shell: bash
run: |
wsl --install -d Ubuntu-24.04 --no-launch
wsl -d Ubuntu-24.04 -u root -- bash -ec "
apt-get update -qq
apt-get install -y -qq curl ca-certificates >/dev/null
curl -fsSL https://get.docker.com | sh -s -- --quiet
service docker start
"
# Create docker wrappers that route to WSL's Docker.
# - .bat for Task's mvdan/sh (Go's exec.LookPath needs a Windows extension)
# - bash script for Git Bash steps
mkdir -p "$HOME/bin"
printf '@wsl -d Ubuntu-24.04 -u root -- docker %%*\r\n' > "$HOME/bin/docker.bat"
cat > "$HOME/bin/docker" << 'WRAPPER'
#!/bin/bash
exec wsl -d Ubuntu-24.04 -u root -- docker "$@"
WRAPPER
chmod +x "$HOME/bin/docker"
echo "$HOME/bin" >> "$GITHUB_PATH"
- name: Initialize generated project
shell: bash
run: |
cd "$RUNNER_TEMP/ci-test-project"
task -v init
- name: Run unit tests
shell: bash
# Integration tests require Docker (Linux images) which is not
# available on Windows runners; those are covered by the Linux CI job.
run: |
cd "$RUNNER_TEMP/ci-test-project"
task -v unit-test
- name: Build Docker image
shell: bash
run: |
cd "$RUNNER_TEMP/ci-test-project"
task -v build
- name: Verify Docker image
shell: bash
run: |
docker run --rm zenable-io/ci-test-project:latest --version
docker run --rm zenable-io/ci-test-project:latest --help
- name: Verify zenable CLI
shell: bash
run: |
export PATH="$HOME/.zenable/bin:$PATH"
zenable version
finalizer:
# This gives us something to set as required in the repo settings. Some projects use dynamic fan-outs using matrix strategies and the fromJSON function, so
# you can't hard-code what _should_ run vs not. Having a finalizer simplifies that so you can just check that the finalizer succeeded, and if so, your
# requirements have been met
# Example: https://x.com/JonZeolla/status/1877344137713766516
name: Finalize the pipeline
runs-on: ubuntu-24.04
# Keep this aligned with the above jobs
needs: [lint, test, windows-smoke-test]
if: always() # Ensure it runs even if "needs" fails or is cancelled
steps:
- name: Check for failed or cancelled jobs
run: |
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ||
"${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One or more required jobs failed or was cancelled. Marking finalizer as failed."
exit 1
fi
- name: Checkout the repository
uses: actions/checkout@v6
- name: Scan workflow logs for warnings and errors
run: scripts/scan_workflow_logs.sh ${{ github.run_id }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Finalize
run: echo "Pipeline complete!"