-
Notifications
You must be signed in to change notification settings - Fork 1
477 lines (417 loc) · 18 KB
/
daemonless-build.yaml
File metadata and controls
477 lines (417 loc) · 18 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
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
name: Reusable App Build
on:
workflow_call:
inputs:
image_name:
description: 'Image name (e.g., radarr, sonarr)'
required: true
type: string
variant:
description: 'Variant to build (blank=all)'
required: false
type: string
default: ''
pre_artifact_name:
description: 'Artifact name to download before build (e.g. web-dist)'
required: false
type: string
default: ''
pre_artifact_path:
description: 'Path to download pre-build artifact into (e.g. web/dist/)'
required: false
type: string
default: ''
env:
REGISTRY: ghcr.io
DBUILD_REF: v1.8.3
COMPOSE_PACKAGES: py311-podman-compose
jobs:
# Detect which variants exist and build dynamic matrix
detect:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.detect.outputs.matrix }}
compose_only: ${{ steps.detect.outputs.compose_only }}
architectures: ${{ steps.detect.outputs.architectures }}
manifest_tags: ${{ steps.detect.outputs.manifest_tags }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install dependencies
run: pip install PyYAML
- name: Detect build matrix
id: detect
run: |
# Fetch dbuild
curl -sSL "https://github.com/daemonless/dbuild/archive/${{ env.DBUILD_REF }}.tar.gz" | tar xz -C /tmp
DBUILD_DIR=$(ls -d /tmp/dbuild-* | head -1)
export PYTHONPATH="$DBUILD_DIR"
python3 -m dbuild detect --format github ${{ inputs.variant && format('--variant {0}', inputs.variant) || '' }}
build:
needs: detect
if: ${{ needs.detect.outputs.matrix != '' && needs.detect.outputs.compose_only != 'true' }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.detect.outputs.matrix) }}
steps:
- name: Free disk space
uses: endersonmenezes/free-disk-space@v3
with:
remove_tool_cache: true
remove_android: true
remove_dotnet: true
remove_haskell: true
remove_swap: true
remove_packages: "azure-cli google-cloud-cli microsoft-edge-stable google-chrome-stable firefox mono-complete"
remove_packages_one_command: true
rm_cmd: rmz
- name: Checkout repository
uses: actions/checkout@v6
- name: Download pre-build artifact
if: inputs.pre_artifact_name != ''
uses: actions/download-artifact@v8
with:
name: ${{ inputs.pre_artifact_name }}
path: ${{ inputs.pre_artifact_path }}
- name: Prepare VM data directory
run: sudo mkdir -p /mnt/freebsd-vm && sudo chmod 777 /mnt/freebsd-vm
- name: Build in FreeBSD VM (${{ matrix.tag }}${{ matrix.arch_suffix }})
uses: vmactions/freebsd-vm@v1.4.4
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_ACTOR: ${{ github.actor }}
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
DBUILD_REGISTRY: ghcr.io/daemonless
with:
release: "15.0"
vnc-password: ${{ secrets.VNC_PASSWORD }}
arch: ${{ matrix.vm_arch }}
sync: ${{ matrix.vm_sync }}
usesh: true
copyback: true
data-dir: /mnt/freebsd-vm
debug-on-error: true
envs: "GITHUB_TOKEN GITHUB_ACTOR DOCKERHUB_USERNAME DOCKERHUB_TOKEN DBUILD_REGISTRY"
prepare: |
set -e
# Create 4GB swap for heavy builds
dd if=/dev/zero of=/swapfile bs=1m count=4096
chmod 0600 /swapfile
mdconfig -a -t vnode -f /swapfile -u 0
swapon /dev/md0
# Use latest repo for CI tools (quarterly often has sync issues)
mkdir -p /etc/pkg
echo 'FreeBSD: { url: "http://pkg.FreeBSD.org/${ABI}/latest" }' > /etc/pkg/FreeBSD.conf
pkg update -f
pkg install -y podman ${{ env.COMPOSE_PACKAGES }} jq skopeo buildah trivy python3 py311-pyyaml appjail
mkdir -p /var/log/appjail/jails /usr/local/appjail/jails
# Install patched ocijail for .NET apps (mlock support)
fetch -qo /tmp/ocijail.pkg "https://github.com/daemonless/freebsd-ports/releases/download/v0.4.0-patched/ocijail-0.4.0_3-${{ matrix.arch }}.pkg"
pkg install -fy /tmp/ocijail.pkg
rm -rf /var/db/containers /var/lib/containers 2>/dev/null || true
kldload pf
sysctl net.inet.ip.forwarding=1
sysctl kern.ipc.shm_allow_removed=1
run: |
set -e
# Fetch dbuild
fetch -qo /tmp/dbuild.tar.gz \
"https://github.com/daemonless/dbuild/archive/${{ env.DBUILD_REF }}.tar.gz"
mkdir -p /tmp/dbuild
tar -xzf /tmp/dbuild.tar.gz -C /tmp/dbuild --strip-components=1
export PYTHONPATH="/tmp/dbuild"
DBUILD="python3 -m dbuild"
echo "=== dbuild CI (GitHub Actions) ==="
$DBUILD --version
VARIANT="${{ matrix.tag }}"
ARCH="${{ matrix.arch }}"
IMAGE_NAME="${{ inputs.image_name }}"
# Read old version from sbom.json before build
OLD_VERSION=$(jq -r --arg tag "$VARIANT" '.tags[$tag].app_version // ""' sbom.json 2>/dev/null || echo "")
echo "$OLD_VERSION" > .version-old
# Build
$DBUILD -v build --variant "$VARIANT" --arch "$ARCH"
# Extract new version from built image
NEW_VERSION=$(podman run --rm --entrypoint "" "ghcr.io/daemonless/${IMAGE_NAME}:build-${VARIANT}" cat /app/version 2>/dev/null || echo "")
echo "$NEW_VERSION" > .version-new
# Install screenshot deps only if CIT mode requires it
if grep -q 'mode:.*screenshot' .daemonless/config.yaml 2>/dev/null || grep -q 'mode:.*screenshot' .dbuild.yaml 2>/dev/null; then
pkg clean -ay && rm -rf /var/cache/pkg/*
pkg install -y chromium py311-selenium py311-scikit-image
fi
# Test (capture exit code for copyback)
mkdir -p cit-results
cit_exit=0
$DBUILD -v test --variant "$VARIANT" \
--json cit-results/${{ inputs.image_name }}-${{ matrix.tag }}${{ matrix.arch_suffix }}.json \
|| cit_exit=$?
# Copy any temp screenshots to results
cp /tmp/cit-screenshot*.png cit-results/ 2>/dev/null || true
# Save exit code (don't exit here - let copyback happen first)
echo "$cit_exit" > cit-results/cit-exit-code
# Only push + sbom if tests passed and not a PR
if [ $cit_exit -ne 0 ]; then
echo "Tests failed with exit code $cit_exit - skipping push"
elif [ "${{ github.event_name }}" = "pull_request" ]; then
echo "PR build - skipping push"
else
# Free disk before push
swapoff /dev/md0 2>/dev/null; mdconfig -d -u 0 2>/dev/null; rm -f /swapfile
podman image prune -f
pkg clean -ay
rm -rf /tmp/cit-*
$DBUILD -v push --variant "$VARIANT" --arch "$ARCH"
$DBUILD -v sbom --variant "$VARIANT" --arch "$ARCH"
fi
- name: Upload test artifacts
uses: actions/upload-artifact@v7
if: always()
with:
name: cit-results-${{ matrix.tag }}${{ matrix.arch_suffix }}
path: cit-results/
retention-days: 30
- name: Upload SBOM
uses: actions/upload-artifact@v7
if: success() && github.event_name != 'pull_request'
with:
name: sbom-${{ matrix.tag }}${{ matrix.arch_suffix }}
path: sbom-results/
retention-days: 90
- name: Check test result
if: always()
run: |
if [ ! -f cit-results/cit-exit-code ]; then
echo "ERROR: cit-exit-code file not found - copyback may have failed"
exit 1
fi
exit_code=$(cat cit-results/cit-exit-code)
if [ "$exit_code" != "0" ]; then
echo "Tests failed with exit code $exit_code"
exit $exit_code
fi
- name: Update Docker Hub description
if: success() && matrix.tag == 'latest' && matrix.arch == 'amd64' && github.event_name != 'pull_request'
continue-on-error: true
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
uses: daemonless/dbuild/.github/actions/dockerhub-desc@main
with:
image_name: ${{ inputs.image_name }}
dockerhub_username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Determine version label
id: ver
if: always()
run: |
OLD=$(cat .version-old 2>/dev/null | tr -d '[:space:]' || echo "")
NEW=$(cat .version-new 2>/dev/null | tr -d '[:space:]' || echo "")
if [ -n "$NEW" ] && [ -n "$OLD" ] && [ "$OLD" != "$NEW" ]; then
echo "label=${OLD} → ${NEW}" >> $GITHUB_OUTPUT
elif [ -n "$NEW" ]; then
echo "label=${NEW}" >> $GITHUB_OUTPUT
else
echo "label=" >> $GITHUB_OUTPUT
fi
- name: Discord notification
if: always()
continue-on-error: true
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
uses: daemonless/dbuild/.github/actions/discord-notify@main
with:
webhook_url: ${{ secrets.DISCORD_WEBHOOK }}
image_name: ${{ inputs.image_name }}
tag: ${{ matrix.tag }}${{ matrix.arch_suffix }}
status: ${{ job.status }}
commit_sha: ${{ github.sha }}
commit_url: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}
run_url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
branch: ${{ github.ref_name }}
has_dockerhub: ${{ secrets.DOCKERHUB_USERNAME != '' }}
version: ${{ steps.ver.outputs.label }}
# Create multi-arch manifests
create-manifests:
needs: [detect, build]
if: success() && github.event_name != 'pull_request' && contains(needs.detect.outputs.architectures, 'aarch64')
runs-on: ubuntu-latest
steps:
- name: Login to GHCR
run: echo "${{ secrets.GITHUB_TOKEN }}" | podman login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Create and push manifests
env:
MANIFEST_TAGS: ${{ needs.detect.outputs.manifest_tags }}
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
run: |
IMAGE_NAME="${{ inputs.image_name }}"
# Login to Docker Hub if credentials available
if [ -n "$DOCKERHUB_USERNAME" ] && [ -n "$DOCKERHUB_TOKEN" ]; then
echo "$DOCKERHUB_TOKEN" | podman login docker.io -u $DOCKERHUB_USERNAME --password-stdin
fi
for tag in $MANIFEST_TAGS; do
echo "=== Creating manifest for $tag ==="
# GHCR manifest
podman manifest create ghcr.io/daemonless/${IMAGE_NAME}:${tag} \
ghcr.io/daemonless/${IMAGE_NAME}:${tag} \
ghcr.io/daemonless/${IMAGE_NAME}:${tag}-aarch64 || continue
podman manifest push ghcr.io/daemonless/${IMAGE_NAME}:${tag} \
docker://ghcr.io/daemonless/${IMAGE_NAME}:${tag}
# Docker Hub manifest
if [ -n "$DOCKERHUB_USERNAME" ]; then
podman manifest create docker.io/daemonless/${IMAGE_NAME}:${tag} \
docker.io/daemonless/${IMAGE_NAME}:${tag} \
docker.io/daemonless/${IMAGE_NAME}:${tag}-aarch64 || continue
podman manifest push docker.io/daemonless/${IMAGE_NAME}:${tag} \
docker://docker.io/daemonless/${IMAGE_NAME}:${tag}
fi
done
# Compose-only test (no build, just test pre-built images)
test-compose:
needs: detect
if: ${{ needs.detect.outputs.compose_only == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Free disk space
uses: endersonmenezes/free-disk-space@v3
with:
remove_tool_cache: true
remove_android: true
remove_dotnet: true
remove_haskell: true
remove_swap: true
remove_packages: "azure-cli google-cloud-cli microsoft-edge-stable google-chrome-stable firefox mono-complete"
remove_packages_one_command: true
rm_cmd: rmz
- name: Checkout repository
uses: actions/checkout@v6
- name: Prepare VM data directory
run: sudo mkdir -p /mnt/freebsd-vm && sudo chmod 777 /mnt/freebsd-vm
- name: Test in FreeBSD VM
uses: vmactions/freebsd-vm@v1.4.4
with:
release: "15.0"
vnc-password: ${{ secrets.VNC_PASSWORD }}
usesh: true
copyback: true
data-dir: /mnt/freebsd-vm
debug-on-error: true
prepare: |
set -e
mkdir -p /etc/pkg
echo 'FreeBSD: { url: "http://pkg.FreeBSD.org/${ABI}/latest" }' > /etc/pkg/FreeBSD.conf
pkg update -f
pkg install -y podman ${{ env.COMPOSE_PACKAGES }} jq python3 py311-pyyaml appjail
# Install patched ocijail for .NET apps (mlock support)
fetch -qo /tmp/ocijail.pkg "https://github.com/daemonless/freebsd-ports/releases/download/v0.4.0-patched/ocijail-0.4.0_3-amd64.pkg"
pkg install -fy /tmp/ocijail.pkg
rm -rf /var/db/containers /var/lib/containers 2>/dev/null || true
kldload pf
sysctl net.inet.ip.forwarding=1
sysctl kern.ipc.shm_allow_removed=1
run: |
set -e
# Fetch dbuild
fetch -qo /tmp/dbuild.tar.gz \
"https://github.com/daemonless/dbuild/archive/${{ env.DBUILD_REF }}.tar.gz"
mkdir -p /tmp/dbuild
tar -xzf /tmp/dbuild.tar.gz -C /tmp/dbuild --strip-components=1
export PYTHONPATH="/tmp/dbuild"
DBUILD="python3 -m dbuild"
mkdir -p cit-results
$DBUILD -v test \
--json cit-results/${{ inputs.image_name }}.json
- name: Upload test artifacts
uses: actions/upload-artifact@v7
if: always()
with:
name: cit-results
path: cit-results/
retention-days: 30
- name: Discord notification
if: always()
continue-on-error: true
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
run: |
[ -z "$DISCORD_WEBHOOK" ] && exit 0
if [ "${{ job.status }}" = "success" ]; then
COLOR=3066993
STATUS="Success"
else
COLOR=15158332
STATUS="Failed"
fi
SHORT_SHA="${{ github.sha }}"
SHORT_SHA="${SHORT_SHA:0:7}"
curl -sS -H "Content-Type: application/json" \
-d "{\"embeds\":[{\"title\":\"${{ inputs.image_name }} compose test $STATUS\",\"url\":\"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\",\"color\":$COLOR,\"fields\":[{\"name\":\"Commit\",\"value\":\"[$SHORT_SHA](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }})\",\"inline\":true},{\"name\":\"Branch\",\"value\":\"${{ github.ref_name }}\",\"inline\":true}]}]}" \
"$DISCORD_WEBHOOK"
# Merge SBOMs and commit to repo
commit-sbom:
needs: build
if: success() && github.event_name != 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Download all SBOM artifacts
uses: actions/download-artifact@v8
with:
pattern: sbom-*
path: sbom-artifacts/
- name: Merge SBOMs
run: |
jq -n \
--arg image "${{ inputs.image_name }}" \
--arg generated "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{image: $image, generated: $generated, tags: {}}' > sbom.json
for f in $(find sbom-artifacts/ -name '*-sbom.json'); do
[ -f "$f" ] || continue
tag=$(jq -r '.tag' "$f")
[ -z "$tag" ] && continue
echo "Processing: $tag"
jq --arg tag "$tag" \
--slurpfile data "$f" \
'.tags[$tag] = ($data[0] | del(.image, .tag))' \
sbom.json > tmp.json && mv tmp.json sbom.json
done
# Sort all keys and package lists alphabetically for stable ordering
jq -S '.tags[].packages[] |= sort_by(.name)' sbom.json > tmp.json && mv tmp.json sbom.json
echo "Tags: $(jq -r '.tags | keys | join(", ")' sbom.json)"
jq -r '.tags | to_entries[] | "\(.key): \(.value.summary.total) packages"' sbom.json
- name: Commit SBOM
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add sbom.json
if git diff --staged --quiet; then
echo "No changes to sbom.json"
else
git commit -m "Update sbom.json [skip ci]"
git push
fi
# Trigger status page update after successful builds
notify-status:
needs: [build, test-compose, commit-sbom, create-manifests]
if: always() && !failure() && !cancelled() && github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Trigger status update
env:
DISPATCH_TOKEN: ${{ secrets.DISPATCH_TOKEN }}
run: |
curl -X POST \
-H "Authorization: token $DISPATCH_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/daemonless/daemonless-io/dispatches \
-d '{"event_type": "status-update", "client_payload": {"image": "${{ inputs.image_name }}"}}'