-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Expand file tree
/
Copy pathaction.yml
More file actions
181 lines (171 loc) · 9.1 KB
/
Copy pathaction.yml
File metadata and controls
181 lines (171 loc) · 9.1 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
name: Docker Build and Push
description: Set up a buildx builder and build/push an image, using Blacksmith's builder or the upstream Docker actions on GitHub-hosted runners.
inputs:
provider:
description: Empty or 'blacksmith' selects Blacksmith's builder; anything else selects the docker/* actions.
required: false
default: ''
context:
description: Build context.
required: false
default: .
file:
description: Path to the Dockerfile.
required: true
platforms:
description: Target platforms, e.g. linux/amd64.
required: true
tags:
description: Comma-separated list of tags to push.
required: true
max-cache-size-mb:
description: >-
Layer cache to retain after this action prunes, in MB. Must stay above one
build's working set (base + dependency layers + RUN --mount=type=cache
dirs) or every build evicts what the next one needs. Falls back to the
small-image default in the prune step when empty — the fallback lives there
rather than here because callers pass this from a matrix field, and an unset
matrix key arrives as the empty string, which counts as "provided" and would
bypass an input `default:` entirely.
required: false
# Registry logins must precede this action. provenance/sbom stay off: attestation
# manifests break `imagetools create` retagging in promote-images.
runs:
using: composite
steps:
# One sticky disk per Dockerfile per platform. v1 keyed it on the repo name
# alone, so every image shared one disk: matrix jobs all clone the same
# parent snapshot and only the first to finish becomes the next parent, so
# the app image's `deps` layer was written and discarded every run (~300-465s
# rebuilt each time). Platform is in the key because amd64 and arm64 build
# the same Dockerfiles concurrently on main and share no layers. Ref is
# deliberately not: cross-ref reuse is the point, and an occasional overlap
# costs one rebuild.
- name: Resolve Docker layer cache key
id: cache-key
if: inputs.provider == '' || inputs.provider == 'blacksmith'
shell: bash
env:
FILE: ${{ inputs.file }}
PLATFORMS: ${{ inputs.platforms }}
run: echo "value=${GITHUB_REPOSITORY##*/}/${FILE#./}/${PLATFORMS//\//-}" >> "$GITHUB_OUTPUT"
# This action does NOT bound the disk — see the prune step below. BuildKit's
# own GC is time-based only (layers unused for 8 days), and these disks are
# mounted many times a day, so nothing ever ages out: app.Dockerfile/linux-amd64
# reached 351 GB inside a day of being created, and realtime, whose image is
# under 300 MB, sat at 249 GB. Sticky disks bill at ~$0.51/GB-month.
- name: Set up Blacksmith builder
if: inputs.provider == '' || inputs.provider == 'blacksmith'
uses: useblacksmith/setup-docker-builder@a5256a73e30f09e37e3eceb8ca36043d17621d24 # v2
with:
cache-key: ${{ steps.cache-key.outputs.value }}
- name: Build and push (Blacksmith)
if: inputs.provider == '' || inputs.provider == 'blacksmith'
uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2
with:
context: ${{ inputs.context }}
file: ${{ inputs.file }}
platforms: ${{ inputs.platforms }}
push: true
tags: ${{ inputs.tags }}
provenance: false
sbom: false
# Bound the layer cache ourselves. setup-docker-builder v1 took a
# max-cache-size-mb input and pruned in its own post step, but the v2 rewrite
# dropped it — and GitHub only WARNS on an unknown composite input, so passing
# it to v2 silently did nothing for a day while the app disk sat at 200+ GB.
#
# This is v1's command verbatim (its dist/index.js pruneBuildkitCache), against
# the fixed address v2 itself uses for `buildctl du` and `debug workers`:
# sudo buildctl --addr tcp://127.0.0.1:1234 prune --all --keep-storage <MB>
#
# Note buildctl's --all is NOT `docker buildx prune --all`. Here it means
# "include internal/frontend references" (cache/manager.go: without it, records
# typed internal or frontend, and any ref shared with an external source, are
# skipped). It does not wipe the cache, and --keep-storage still caps what is
# retained -- it maps straight onto the modern MaxUsedSpace field, so it is the
# buildctl spelling of --max-used-space rather than a deprecated alias.
# `RUN --mount=type=cache` dirs are typed exec.cachemount and are reclaimed
# either way; --all is here because it is what v1 used and it prunes strictly
# more. Runs before the builder's post step, which is what commits the disk.
#
# Warn rather than fail: a cache that is too large is not worth failing a
# deploy over. The du either side is what makes a silent no-op visible — the
# failure mode that hid the v2 input regression in the first place.
- name: Prune the layer cache
if: (inputs.provider == '' || inputs.provider == 'blacksmith') && !cancelled()
shell: bash
env:
KEEP_MB: ${{ inputs.max-cache-size-mb || '25600' }}
run: |
addr='tcp://127.0.0.1:1234'
# A zero or non-numeric value is NOT a no-op. buildctl parses
# --keep-storage as a float, and BuildKit's cache manager treats
# keepBytes==0 as "no cap" (`gcMode := opt.keepBytes != 0`), pruning
# everything eligible rather than trimming to a limit. A typo such as
# '40GB' — valid in turbo.json, but this flag is a bare MB number — would
# silently empty the cache and make every later build cold, costing far
# more than the storage it saves. Refuse instead.
if ! [[ "$KEEP_MB" =~ ^[1-9][0-9]*$ ]]; then
echo "::warning::max-cache-size-mb must be a positive whole number of MB, got '${KEEP_MB}' — skipping prune rather than risk wiping the cache"
exit 0
fi
# Print the whole Total line rather than picking a column: buildctl's du
# table is whitespace-aligned and its layout is not a stable contract.
#
# The trailing `|| true` is load-bearing. Composite steps run under
# `bash -e -o pipefail`, where `cur="$(total)"` takes the substitution's
# exit status, so a failing du would abort the step and fail the build --
# `echo "$(total)"` survives but the assignment in the settle loop does
# not. buildctl exiting non-zero here is entirely plausible: deleting a
# sticky disk out from under a running job makes buildkitd panic inside
# DiskUsage, and grep also exits 1 whenever the table has no Total line.
# Cache hygiene must never be able to fail a deploy.
total() { sudo buildctl --addr "$addr" du 2>/dev/null | grep -iE '^total:' | tr -s ' \t' ' ' || true; }
echo "before prune -> $(total)"
if sudo buildctl --addr "$addr" prune --all --keep-storage "$KEEP_MB"; then
# buildctl prune returns BEFORE buildkitd has finished deleting
# (moby/buildkit#1198). The builder's post step then SIGTERMs buildkitd
# and SIGKILLs it after 30s (shutdownBuildkitd: `const a=3e4`); on
# SIGKILL it sets sigkillUsed and SKIPS the sticky disk commit, throwing
# away this run's cache and risking a corrupt bbolt metadata DB. So wait
# for du to stop moving before handing back. Bounded — this is hygiene,
# not correctness, and the steady-state trim settles almost at once.
prev=''; stable=0
for _ in $(seq 1 60); do
cur="$(total)"
# An empty reading means du FAILED, never that the cache is empty:
# buildctl prints its `Total:` line unconditionally (cmd/buildctl
# diskusage.go), so an empty cache still reports `Total: 0B`. Without
# the -n guard the initial prev='' matched two empty readings and the
# loop exited after ~2s -- precisely when du is failing and the prune
# is most likely still deleting. Treat it as unstable and wait out the
# bound instead.
if [ -n "$cur" ] && [ "$cur" = "$prev" ]; then
stable=$((stable + 1))
[ "$stable" -ge 2 ] && break
else
stable=0
fi
prev="$cur"
sleep 2
done
echo "after prune -> $(total) (keep-storage ${KEEP_MB} MB)"
else
echo "::warning::Layer cache prune failed; this sticky disk is unbounded for this run"
fi
- name: Set up Docker Buildx
if: inputs.provider != '' && inputs.provider != 'blacksmith'
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
# No cache-to: type=gha — it shares the 10 GB repo quota with the cache mounts.
- name: Build and push (GitHub)
if: inputs.provider != '' && inputs.provider != 'blacksmith'
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: ${{ inputs.context }}
file: ${{ inputs.file }}
platforms: ${{ inputs.platforms }}
push: true
tags: ${{ inputs.tags }}
provenance: false
sbom: false