-
Notifications
You must be signed in to change notification settings - Fork 81
350 lines (327 loc) · 14.9 KB
/
Copy pathimages.yml
File metadata and controls
350 lines (327 loc) · 14.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
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
# Builds the Code API container images and publishes them to GHCR, so a host
# can pull a tested image instead of compiling one in place. Like ci.yml this
# file is inert inside the monorepo — GitHub only runs workflows from the repo
# root — and becomes a root workflow in the published repo.
#
# Entry points:
#
# * push to main — build every image, push `sha-<commit>` tags, and refresh
# each image's registry build cache. The promote job then points the
# `main` tags at the newest main commit that has all images.
# * pull_request — build every image without pushing, reading the cache that
# main wrote, so Dockerfile breakage surfaces before merge.
# * workflow_dispatch — on main, the same as a push; on any other branch, a
# build-only run.
#
# The image list lives in the plan job and feeds both the build matrix and the
# promotion, so the two cannot drift apart. README.md ("Prebuilt images")
# documents the tags and how a Compose host switches to them.
name: Images
on:
push:
branches: [main]
# Everything the Dockerfiles below COPY from the build context, plus this
# workflow. Every run builds every image, so a commit that has tags has
# them for all images; unchanged images are cache hits.
paths: &image-inputs
- .dockerignore
- .github/scripts/image-revision.sh
- .github/workflows/images.yml
- api/**
- docker/**
- javascript-packages.txt
- launcher/**
- packages/code/src/**
- service/**
- shared/**
pull_request:
paths: *image-inputs
workflow_dispatch:
permissions:
contents: read
concurrency:
# Each main commit gets its own group and is never cancelled, so no commit's
# build can be dropped by a later queued run (GitHub keeps only one pending
# run per group). Other refs share one group per ref and cancel superseded
# runs.
group: images-${{ github.ref == 'refs/heads/main' && github.sha || github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
NAMESPACE: ghcr.io/librechat-ai
jobs:
plan:
name: Plan
runs-on: ubuntu-24.04
outputs:
images: ${{ steps.images.outputs.images }}
publish: ${{ github.repository == 'LibreChat-AI/code-interpreter' && github.ref == 'refs/heads/main' }}
steps:
- name: List images
id: images
# One image per distinct Compose build in docker-compose.yaml. The
# sandbox runner's two Compose targets (KVM_ENABLED=true/false) are
# published as two images. The Lambda MicroVM targets are not built
# here; they are not used by Compose hosts.
run: |
images=$(jq -c . <<'JSON'
[
{"image": "code-interpreter-api", "dockerfile": "service/Dockerfile", "target": "api",
"description": "Code API HTTP server (Compose service api)", "timeout": 30},
{"image": "code-interpreter-worker", "dockerfile": "service/Dockerfile", "target": "worker",
"description": "Code API job worker (Compose service service-worker)", "timeout": 30},
{"image": "code-interpreter-file-server", "dockerfile": "service/Dockerfile", "target": "production",
"description": "Code API file server (Compose service file_server)", "timeout": 30},
{"image": "code-interpreter-egress-gateway", "dockerfile": "service/Dockerfile.egress-gateway", "target": "production",
"description": "Sandbox egress gateway (Compose service egress_gateway)", "timeout": 30},
{"image": "code-interpreter-tool-call-server", "dockerfile": "service/Dockerfile.tool-call-server", "target": "production",
"description": "Tool call server (Compose service tool_call_server)", "timeout": 30},
{"image": "code-interpreter-sandbox-runner", "dockerfile": "api/Dockerfile", "target": "sandbox-runner-true",
"description": "libkrun microVM sandbox runner with the guest rootfs and runtime packages baked in (Compose service sandbox-runner, KVM_ENABLED=true)",
"timeout": 60, "free_disk": true},
{"image": "code-interpreter-sandbox-runner-direct", "dockerfile": "api/Dockerfile", "target": "sandbox-runner-false",
"description": "Directory-root sandbox runner; runtime packages are mounted from the host (Compose service sandbox-runner, KVM_ENABLED=false)",
"timeout": 30}
]
JSON
)
echo "images=$images" >> "$GITHUB_OUTPUT"
build:
name: ${{ matrix.image }}
needs: plan
runs-on: ubuntu-24.04
timeout-minutes: ${{ matrix.timeout }}
permissions:
contents: read
# Pushes happen only when needs.plan.outputs.publish is true (main).
# Pull request runs read the build cache and never push; GitHub caps a
# fork's token at read access regardless.
packages: write
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(needs.plan.outputs.images) }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
- name: Free runner disk
# A cold KVM runner build peaks at roughly 17 GB of builder state. The
# hosted runner had about 87 GB free when measured, but GitHub only
# guarantees 14 GB, so this leg removes unused preinstalled SDKs
# (about 28 GB, 1.5 minutes) rather than depend on the surplus.
if: matrix.free_disk
run: |
sudo rm -rf \
/opt/ghc \
/opt/hostedtoolcache/CodeQL \
/usr/local/.ghcup \
/usr/local/lib/android \
/usr/local/share/powershell \
/usr/share/dotnet \
/usr/share/swift
docker image prune --all --force >/dev/null
df -h /
- name: Sample free disk
run: |
nohup bash -c 'while :; do df -BM --output=avail / | tail -n1 | tr -dc 0-9; echo; sleep 10; done' \
>"$RUNNER_TEMP/disk-avail-mib.log" 2>/dev/null &
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Log in to GHCR
# Pull request runs log in only to read the cache, and only from this
# repository; fork runs read public packages anonymously.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Check for an existing tag
# `sha-` tags are written once. A re-run (or a dispatch on the same
# commit) builds only the images whose tag is missing, so a failed
# re-run cannot leave the commit's set mixed across two builds.
id: existing
if: needs.plan.outputs.publish == 'true'
env:
REF: ${{ env.NAMESPACE }}/${{ matrix.image }}:sha-${{ github.sha }}
# A registry that cannot answer fails the job rather than risk
# rebuilding over a tag that may exist.
run: |
status=0
revision=$(.github/scripts/image-revision.sh "$REF") || status=$?
case $status in
0)
if [ "$revision" != "$GITHUB_SHA" ]; then
echo "::error::$REF records revision $revision"
exit 1
fi
echo "$REF already exists; not rebuilding it."
echo "exists=true" >> "$GITHUB_OUTPUT"
;;
3)
echo "exists=false" >> "$GITHUB_OUTPUT"
;;
*)
echo "::error::Could not read $REF from the registry."
exit 1
;;
esac
- name: Image metadata
id: meta
if: steps.existing.outputs.exists != 'true'
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
with:
images: ${{ env.NAMESPACE }}/${{ matrix.image }}
tags: type=sha,format=long
labels: |
org.opencontainers.image.title=${{ matrix.image }}
org.opencontainers.image.description=${{ matrix.description }}
annotations: |
org.opencontainers.image.title=${{ matrix.image }}
org.opencontainers.image.description=${{ matrix.description }}
- name: Build${{ needs.plan.outputs.publish == 'true' && ' and push' || '' }}
id: build
if: steps.existing.outputs.exists != 'true'
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
file: ${{ matrix.dockerfile }}
target: ${{ matrix.target }}
platforms: linux/amd64
push: ${{ needs.plan.outputs.publish == 'true' }}
# Without a push, keep the result in the builder only: nothing is
# loaded into the runner's Docker engine, which would need a second
# copy of the multi-gigabyte sandbox image on disk.
outputs: ${{ needs.plan.outputs.publish != 'true' && 'type=cacheonly' || '' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
annotations: ${{ steps.meta.outputs.annotations }}
# A registry cache next to each image, written only from main.
# mode=max keeps the intermediate stages (the KVM runner's package
# stage above all), which the GitHub Actions cache could not hold:
# its 10 GB repository limit would evict them between pushes.
cache-from: type=registry,ref=${{ env.NAMESPACE }}/${{ matrix.image }}:buildcache
cache-to: ${{ needs.plan.outputs.publish == 'true' && format('type=registry,ref={0}/{1}:buildcache,mode=max,ignore-error=true', env.NAMESPACE, matrix.image) || '' }}
- name: Summarize
if: always()
env:
IMAGE_NAME: ${{ matrix.image }}
PUSHED: ${{ needs.plan.outputs.publish == 'true' && steps.build.outcome == 'success' }}
EXISTED: ${{ steps.existing.outputs.exists == 'true' }}
DIGEST: ${{ steps.build.outputs.digest }}
run: |
lowest=$(sort -n "$RUNNER_TEMP/disk-avail-mib.log" 2>/dev/null | head -n1)
{
echo "### $IMAGE_NAME"
if [ "$EXISTED" = true ]; then
echo "\`$NAMESPACE/$IMAGE_NAME:sha-$GITHUB_SHA\` was already published; left unchanged."
echo
fi
if [ "$PUSHED" = true ]; then
echo "Pushed \`$NAMESPACE/$IMAGE_NAME:sha-$GITHUB_SHA\`, pinnable as \`$NAMESPACE/$IMAGE_NAME@$DIGEST\`."
echo
fi
echo "Lowest free space on / while building: ${lowest:-unknown} MiB"
echo '```'
df -h /
docker buildx du 2>/dev/null | tail -n1 || true
echo '```'
} | tee -a "$GITHUB_STEP_SUMMARY"
promote:
name: Move main tags
needs: [plan, build]
if: needs.plan.outputs.publish == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 15
# Promotions run one at a time. Each resolves its target when it runs
# instead of trusting its own commit, so a stale re-run, or a queued
# promotion that GitHub replaces, cannot leave `main` behind or move it
# backwards.
concurrency:
group: images-promote
cancel-in-progress: false
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
sparse-checkout: .github/scripts
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Point main at the newest complete commit
# Target: the newest of the last 100 commits on main for which every
# image has a `sha-` tag. No image's `main` moves to an older commit
# than the one it names, and a registry that cannot answer fails the
# promotion instead of being read as a missing tag, so an error can
# delay a promotion but never reverse one. A registry cannot move
# several repositories' tags in one transaction, so a failure part-way
# leaves some `main` tags behind; the next promotion, or a re-run of
# this job, finishes the move. Deployments should pin `sha-` tags
# rather than `main`.
env:
GH_TOKEN: ${{ github.token }}
IMAGES: ${{ needs.plan.outputs.images }}
run: |
set -euo pipefail
# Prints the revision a tag records, or nothing if the tag does not
# exist; exits the step if the registry cannot answer.
revision_of() {
local revision status=0
revision=$(.github/scripts/image-revision.sh "$1") || status=$?
case $status in
0) echo "$revision" ;;
3) ;;
*) echo "::error::Could not read $1 from the registry." >&2; exit 1 ;;
esac
}
complete() {
local image revision
for image in "${images[@]}"; do
revision=$(revision_of "$NAMESPACE/$image:sha-$1") || exit 1
[ "$revision" = "$1" ] || return 1
done
}
retag() {
local attempt
for attempt in 1 2 3; do
docker buildx imagetools create --tag "$1:main" "$1:sha-$2" && return 0
sleep $(( attempt * 10 ))
done
return 1
}
mapfile -t images < <(jq -r '.[].image' <<<"$IMAGES")
mapfile -t commits < <(gh api "repos/$GITHUB_REPOSITORY/commits?sha=main&per_page=100" --jq '.[].sha')
declare -A position
for i in "${!commits[@]}"; do position[${commits[$i]}]=$i; done
target=''
for sha in "${commits[@]}"; do
if complete "$sha"; then
target=$sha
break
fi
done
if [ -z "$target" ]; then
echo "::error::None of the last ${#commits[@]} commits on main has all images."
exit 1
fi
for image in "${images[@]}"; do
ref="$NAMESPACE/$image"
current=$(revision_of "$ref:main") || exit 1
if [ "$current" = "$target" ]; then
echo "$ref:main already at sha-$target" | tee -a "$GITHUB_STEP_SUMMARY"
continue
fi
if [ -n "$current" ] && [ -n "${position[$current]:-}" ] &&
[ "${position[$current]}" -lt "${position[$target]}" ]; then
echo "::warning::$ref:main names $current, newer than $target; leaving it."
continue
fi
retag "$ref" "$target"
echo "$ref:main -> sha-$target" | tee -a "$GITHUB_STEP_SUMMARY"
done