-
Notifications
You must be signed in to change notification settings - Fork 0
491 lines (461 loc) · 26.4 KB
/
Copy pathdocker-bundle.yml
File metadata and controls
491 lines (461 loc) · 26.4 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
478
479
480
481
482
483
484
485
486
487
488
489
490
491
name: Docker (bundle)
# Build and publish getbusbar/busbar-headroom: a single FROM-scratch image bundling the busbar
# binary + THIS repo's headroom-hook cdylib, pre-installed and pre-wired as a global
# prompt-compression rewrite gate (see docker/bundle/Dockerfile + docker/bundle/config.yaml).
# This is the "one container, zero config" convenience path — see README.md.
#
# Distinct from:
# - busbarAI's own getbusbar/busbar image (plugin-free — every plugin gets the same treatment).
# - this repo's OWN getbusbar/headroom-hook image (root Dockerfile/docker-compose.yml/docker.yml)
# — DEPRECATED, see those files' headers: they describe a retired Unix-socket transport this
# repo no longer builds a binary for (headroom-hook is a dlopen cdylib only, no `fn main`).
#
# busbar itself is built FROM SOURCE from a sibling ../busbarAI checkout in this same CI run —
# this repo already declares busbarAI as a path dependency for busbar-plugin-sdk (see Cargo.toml),
# so the sibling-checkout layout is already the convention. The build mirrors EXACTLY what
# busbarAI's own docker.yml used to do when it bundled headroom into its own image, before that
# responsibility moved to this repo (busbarAI stopped bundling headroom into busbar's own release
# and image at that point): same PGO-mandatory fail-closed release build, same
# musl-native `rust:alpine` container trick, exact-ABI-matched runtime libs extracted from the
# same container so dlopen() succeeds in a FROM-scratch image with no libc at all.
#
# ONE DELIBERATE DIVERGENCE from busbarAI's own image: busbar is built here with crt-static
# DISABLED, so the bundled binary is dynamically linked against musl rather than static-pie. A
# static musl binary cannot dlopen() anything, which meant every bundle published before this
# change exited 1 at startup with "Dynamic loading not supported" and never loaded the plugin it
# exists to preload. busbarAI's image can stay static because it ships plugin-free; this one
# cannot. See the container build step and the boot gate in the publish job.
#
# Triggers:
# - v* tags: publish with X.Y.Z + latest.
# - workflow_dispatch: publish a `test` tag only (or a specific version via the input) —
# end-to-end pipeline check without cutting a release.
#
# Needs real `DOCKERHUB_USERNAME`/`DOCKERHUB_TOKEN` secrets on THIS repo to push. As of this
# workflow's authoring both are provisioned (`gh secret list --repo GetBusbar/headroom-hook`
# shows DOCKERHUB_TOKEN + DOCKERHUB_USERNAME). The push step is only exercised by a real tagged
# run, so confirm it once against this repo's credentials before relying on it.
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
version:
description: >-
Publish as this version (e.g. 1.1.0) with the full semver tag cascade + latest. Leave
empty to publish only a `test` tag.
required: false
default: ""
env:
DOCKERHUB_IMAGE: getbusbar/busbar-headroom
# Ref for the sibling busbarAI checkout (source of the busbar binary this image bundles).
# `main` is busbarAI's release branch (matches the convention: main = release-only).
BUSBARAI_REF: main
permissions:
contents: read
id-token: write # OIDC identity for keyless build-provenance attestation
attestations: write
jobs:
# Dynamic musl busbar binary (PGO) + this repo's headroom-hook cdylib, built together per-arch
# on native runners (no QEMU) in ONE musl-native container, so the binary, the cdylib and the
# extracted runtime libs all come from the same musl userland (exact ABI match).
build-binaries:
name: musl ${{ matrix.arch }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
include:
- arch: amd64
os: ubuntu-latest
target: x86_64-unknown-linux-musl
- arch: arm64
os: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
steps:
- name: Checkout headroom-hook (this repo)
uses: actions/checkout@v4
with:
path: headroom-hook
- name: Checkout busbarAI (sibling — source of the busbar binary)
uses: actions/checkout@v4
with:
# NOT "GetBusbar/busbarAI" — that repo does not exist (confirmed via `gh repo list
# GetBusbar`; every other workflow in this repo, e.g. release.yml and
# headroom-release-watch.yml, already correctly uses GetBusbar/busbar). As authored this
# checkout 404'd, so this job could never get past its second step.
repository: GetBusbar/busbar
ref: ${{ env.BUSBARAI_REF }}
path: busbarAI
# headroom-hook's Cargo.toml declares busbar-plugin-sdk (and its dev-dependency) as
# SIBLING-RELATIVE path dependencies (`../busbarAI/crates/...`), which is exactly the layout
# the two checkout steps above produce. An absolute or otherwise non-sibling path here would
# not resolve against this job's tree, and cargo's failure for it is a confusing one, so
# assert the shape up front rather than letting the build step report it.
- name: Assert headroom-hook's path dependencies are sibling-relative
run: |
set -euo pipefail
grep 'path = "\.\./busbarAI' headroom-hook/Cargo.toml
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: busbarAI
key: ${{ matrix.target }}
# Built on the HOST (a plain build tool that only has to run on the runner, never ship), and
# built BEFORE the musl-native container step below: that container runs as root and writes
# root-owned directories into busbarAI/target, which a later host `cargo build` in the same
# workspace cannot always write through. Ordering it first sidesteps that entirely.
- name: Build busbar-plugin-pack (host target, build tool only)
working-directory: busbarAI
run: cargo build --release -p busbar-plugin-pack
# scripts/pgo-build.sh sets RUSTFLAGS itself for both of its cargo phases, and a RUSTFLAGS
# set in the environment would simply be overridden by it (cargo gives the env var
# precedence over config-file rustflags, and the script's own assignment wins over ours).
# busbar has to be built with crt-static DISABLED here (see the next step for why), so the
# script is patched to append an opt-in extra-flags variable to both phases. The patch is
# asserted to have applied, so an upstream rewording of those lines fails the build loudly
# instead of silently producing a static binary that cannot dlopen anything.
- name: Let scripts/pgo-build.sh accept extra RUSTFLAGS
working-directory: busbarAI
run: |
set -euo pipefail
# shellcheck disable=SC2016 # the $-expressions are what we are WRITING into the script
sed -i \
-e 's|RUSTFLAGS="-Cprofile-generate=$PROF_DIR"|RUSTFLAGS="-Cprofile-generate=$PROF_DIR ${PGO_EXTRA_RUSTFLAGS:-}"|' \
-e 's|RUSTFLAGS="-Cprofile-use=$MERGED"|RUSTFLAGS="-Cprofile-use=$MERGED ${PGO_EXTRA_RUSTFLAGS:-}"|' \
scripts/pgo-build.sh
n="$(grep -c 'PGO_EXTRA_RUSTFLAGS' scripts/pgo-build.sh)"
if [ "$n" -ne 2 ]; then
echo "::error::expected to patch both pgo-build.sh RUSTFLAGS phases, patched $n" >&2
exit 1
fi
grep -n 'RUSTFLAGS=' scripts/pgo-build.sh
# ONE musl-native `rust:alpine` container builds BOTH halves of this image: busbar itself
# (PGO, from the sibling busbarAI checkout) and this repo's headroom cdylib. They used to be
# built separately, busbar on the Ubuntu host against `musl-tools` and the cdylib in Alpine,
# and that split was the reason this image never worked:
#
# * The host build produced a FULLY STATIC musl busbar. A static musl binary contains no
# dynamic loader, so its dlopen() is a stub that always fails. Every published bundle
# died at boot with "failed to load plugin 'busbar-headroom': Dynamic loading not
# supported" - i.e. the one thing this image exists to do could never happen. Verified
# by running the published getbusbar/busbar-headroom:2.0.3 and :2.0.4 images directly.
# * Even built dynamically, a busbar linked against Ubuntu's musl-tools musl and a cdylib
# linked against Alpine's musl would be carrying two different libcs into one
# from-scratch image. Building both in the SAME container makes the ABI match by
# construction, which is the identical argument the cdylib-only step already made for
# its extracted runtime libs.
#
# So: crt-static disabled for both, one apk userland, one set of extracted runtime libs.
#
# The two reasons the cdylib half already needed Alpine, ported verbatim from busbarAI's own
# docker.yml, still stand and now apply to busbar too:
# 1. headroom-core pulls in `tokenizers` -> `esaxx-rs`, which needs a C++ compiler. The
# Ubuntu host's `musl-tools` package provides `musl-gcc` (C only), no musl-targeted
# C++ toolchain. Alpine ships a real musl-native g++.
# 2. aarch64-unknown-linux-musl does not support `crate-type = ["cdylib"]` under Rust's
# DEFAULT (crt-static-on) linking for this target: the build silently drops the cdylib
# output. `-C target-feature=-crt-static` fixes it but switches to DYNAMIC linking
# against musl's libc/libgcc_s/libstdc++, which a FROM-scratch image does not otherwise
# contain, hence the extracted runtime libs. Applied uniformly to both arches (same
# judgment call busbarAI made): x86_64-musl's cdylib support was never cleanly
# confirmed either way, so the proven mechanism is used for both rather than assuming.
#
# PGO stays MANDATORY and fail-closed (see the marker gate below), matching busbarAI's own
# docker.yml/release.yml - this image's busbar is equivalently optimized, not a naive
# release build. bash/python3/curl/procps are what pgo-build.sh's training phase needs on
# top of the cdylib build's own apk set. No --target is passed: inside this container the
# HOST triple already IS the musl triple, so cargo's plain host build is the musl build and
# the deterministic output path is target/pgo/release/busbar.
- name: Build busbar (PGO, dynamic musl) + headroom cdylib + runtime libs (one container)
run: |
set -euo pipefail
mkdir -p headroom-libs busbar-out
docker run --rm \
-e BUSBAR_RELEASE_PUBKEY \
-v "$PWD/busbarAI":/src/busbarAI \
-v "$PWD/headroom-hook":/src/headroom-hook \
-v "$PWD/headroom-libs":/libs \
-v "$PWD/busbar-out":/bb \
rust:alpine sh -exc '
apk add --no-cache musl-dev sqlite-dev pkgconfig gcc g++ git perl make bash python3 curl procps
export CARGO_HOME=/tmp/cargo-home
cd /src/busbarAI
PGO_EXTRA_RUSTFLAGS="-C target-feature=-crt-static" bash scripts/pgo-build.sh
cp target/pgo/release/busbar /bb/busbar
cp target/pgo/release/busbar.pgo-verified /bb/busbar.pgo-verified
cd /src/headroom-hook
RUSTFLAGS="-C target-feature=-crt-static" cargo build --release
cp target/release/libheadroom_hook.so /libs/libheadroom_hook.so
cp /usr/lib/libstdc++.so.6 /usr/lib/libgcc_s.so.1 /libs/
# BOTH musl names as REAL files: ld-musl-<arch>.so.1 is busbar PT_INTERP (without it
# the kernel cannot exec the binary at all and reports a misleading "permission
# denied"), libc.musl-<arch>.so.1 is the DT_NEEDED soname. In Alpine the second is a
# symlink to the first, and a symlink does not survive a CI artifact round trip.
cp /lib/ld-musl-*.so.1 /libs/
cp /lib/libc.musl-*.so.1 /libs/
chmod -R a+rwX /libs /bb
'
ls -l headroom-libs busbar-out
env:
# The busbar release ed25519 PUBLIC key, embedded at build time (plugin-sign's
# option_env!) so the bundled busbar accepts its own signed headroom plugin as
# first-party rather than refusing it with "embeds no busbar release key".
BUSBAR_RELEASE_PUBKEY: ${{ vars.BUSBAR_RELEASE_PUBKEY }}
shell: bash
# POSITIVE LINKAGE GATE. This inverts the assertion this workflow used to make. It used to
# refuse a dynamically linked binary; it now refuses a STATIC one, because a static musl
# busbar cannot dlopen the plugin this image exists to preload. Asserting the PT_INTERP
# explicitly (rather than just "not static") also catches the case where the loader name
# changes and the Dockerfile stops carrying the right file.
- name: Verify busbar is dynamically linked and names the musl loader
run: |
set -euo pipefail
file busbar-out/busbar
if ! file busbar-out/busbar | grep -q 'dynamically linked'; then
echo "::error::busbar is not dynamically linked - it cannot dlopen the headroom plugin, which is the entire point of this image" >&2
exit 1
fi
interp="$(readelf -l busbar-out/busbar | sed -n 's/.*interpreter: \(.*\)\]/\1/p')"
echo "PT_INTERP=$interp"
case "$interp" in
/lib/ld-musl-*.so.1) : ;;
*) echo "::error::unexpected PT_INTERP '$interp'; the Dockerfile carries /lib/ld-musl-<arch>.so.1 and nothing else" >&2; exit 1 ;;
esac
test -f "headroom-libs/$(basename "$interp")" \
|| { echo "::error::the loader '$interp' this binary needs was not extracted into headroom-libs" >&2; exit 1; }
# POSITIVE PGO GATE — same marker assertion busbarAI's own workflows use, so a non-PGO
# binary can never ship in this image either.
- name: Verify PGO was applied (marker gate)
run: |
set -euo pipefail
marker="busbar-out/busbar.pgo-verified"
if [ ! -s "$marker" ]; then
echo "::error::PGO proof marker missing or empty at $marker - refusing to ship a non-PGO binary" >&2
exit 1
fi
cat "$marker"
grep -q '^pgo-verified=1$' "$marker" || { echo "::error::marker not marked verified" >&2; exit 1; }
bytes="$(grep '^profile_bytes=' "$marker" | cut -d= -f2)"
raw="$(grep '^profraw_count=' "$marker" | cut -d= -f2)"
if [ -z "$bytes" ] || [ "$bytes" -le 0 ] 2>/dev/null; then
echo "::error::merged profile was empty (profile_bytes=$bytes)" >&2; exit 1
fi
if [ -z "$raw" ] || [ "$raw" -le 0 ] 2>/dev/null; then
echo "::error::no .profraw files fed the profile (profraw_count=$raw)" >&2; exit 1
fi
- uses: actions/upload-artifact@v4
with:
name: busbar-${{ matrix.arch }}
path: busbar-out/busbar
if-no-files-found: error
- uses: actions/upload-artifact@v4
# One leg only: v4 artifacts are per-name; both matrix legs uploading the same name
# produces duplicate artifacts (the file is identical — same checkout — so pick amd64).
if: matrix.arch == 'amd64'
with:
name: providers-yaml
path: busbarAI/providers.yaml
if-no-files-found: error
# Same PUBLIC/PRIVATE first-party signing scheme as busbarAI's own release/docker
# pipelines: BUSBAR_SIGN_KEY (this repo's secret, if provisioned) signs the manifest; the
# matching BUSBAR_RELEASE_PUBKEY baked into the busbar binary above verifies it as
# first-party. Falls back to an UNSIGNED tarball (loadable only under
# plugins.trust.allow_unsigned) if the secret is not provisioned, so a bundle build is
# never blocked on key setup — same non-blocking posture busbarAI used.
- name: Package + sign the headroom-hook plugin tarball
env:
BUSBAR_SIGN_KEY: ${{ secrets.BUSBAR_SIGN_KEY }}
run: |
set -euo pipefail
ver="${{ inputs.version }}"
if [ -z "$ver" ] && [ "$GITHUB_REF_TYPE" = "tag" ]; then ver="${GITHUB_REF_NAME#v}"; fi
[ -n "$ver" ] || ver="0.0.0-test"
outdir="plugin-dist"; mkdir -p "$outdir"
unsigned_flag=""
if [ -z "${BUSBAR_SIGN_KEY:-}" ]; then
echo "::warning::BUSBAR_SIGN_KEY secret is not provisioned on this repo — packaging an UNSIGNED headroom plugin tarball (loadable only under plugins.trust.allow_unsigned)."
unsigned_flag="--allow-unsigned"
fi
busbarAI/target/release/busbar-plugin-pack pack \
--lib "headroom-libs/libheadroom_hook.so" \
--name "busbar-headroom" --alias "headroom" --kind hook \
--version "$ver" --publisher busbar \
--needs-prompt rw \
--description "busbar headroom hook plugin: deterministic rule-based prompt-compression rewrite gate" \
--license Apache-2.0 \
--out "${outdir}/busbar-headroom-${{ matrix.arch }}.tar.gz" \
$unsigned_flag
ls -l "$outdir"
shell: bash
# Runtime libs the cdylib needs at dlopen time — bundled alongside the plugin tarball so
# the Dockerfile can COPY them into /lib. Named per-arch so the publish job (single image
# build spanning both arches) can stage each into its own arch-specific build context dir.
- uses: actions/upload-artifact@v4
with:
name: busbar-headroom-libs-${{ matrix.arch }}
path: |
headroom-libs/libstdc++.so.6
headroom-libs/libgcc_s.so.1
headroom-libs/libc.musl-*.so.1
headroom-libs/ld-musl-*.so.1
if-no-files-found: error
- uses: actions/upload-artifact@v4
with:
name: busbar-headroom-${{ matrix.arch }}
path: plugin-dist/busbar-headroom-${{ matrix.arch }}.tar.gz
if-no-files-found: error
publish:
name: build & push image
needs: build-binaries
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Stage binaries + plugins into build context
uses: actions/download-artifact@v4
with:
pattern: busbar-*
path: staged
# `pattern: busbar-*` can never match the `providers-yaml` artifact — the "Arrange" step's
# `cp staged/providers-yaml/providers.yaml` failed on every run until this explicit download.
- name: Stage providers.yaml into build context
uses: actions/download-artifact@v4
with:
name: providers-yaml
path: staged/providers-yaml
- name: Arrange per-arch layout
run: |
mkdir -p binaries/amd64 binaries/arm64 plugins/amd64/lib plugins/arm64/lib
mv staged/busbar-amd64/busbar binaries/amd64/busbar
mv staged/busbar-arm64/busbar binaries/arm64/busbar
chmod +x binaries/amd64/busbar binaries/arm64/busbar
cp staged/providers-yaml/providers.yaml providers.yaml
mv staged/busbar-headroom-amd64/busbar-headroom-amd64.tar.gz plugins/amd64/busbar-headroom.tar.gz
mv staged/busbar-headroom-arm64/busbar-headroom-arm64.tar.gz plugins/arm64/busbar-headroom.tar.gz
mv staged/busbar-headroom-libs-amd64/* plugins/amd64/lib/
mv staged/busbar-headroom-libs-arm64/* plugins/arm64/lib/
# actions/upload-artifact does not preserve the executable bit (which is exactly why the
# busbar binary above already needs an explicit chmod). ld-musl-<arch>.so.1 is the
# program INTERPRETER, and the kernel refuses to exec a binary whose interpreter is not
# executable - reporting a bare "exec /busbar: permission denied" that points at the
# wrong file entirely. Observed while reproducing this locally; restore the bit for all
# of them rather than only the loader.
chmod +x plugins/amd64/lib/* plugins/arm64/lib/*
ls -l plugins/amd64/lib plugins/arm64/lib
- uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# The version being published: the pushed v* tag, or the workflow_dispatch `version`
# input. Empty (bare dispatch) -> publish only a `test` tag.
- name: Resolve version
id: ver
run: |
VER="${{ inputs.version }}"
if [ -z "$VER" ] && [ "$GITHUB_REF_TYPE" = "tag" ]; then VER="$GITHUB_REF_NAME"; fi
echo "ver=${VER#v}" >> "$GITHUB_OUTPUT"
- name: Compute tags and labels
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.DOCKERHUB_IMAGE }}
tags: |
type=semver,pattern={{version}},value=v${{ steps.ver.outputs.ver }},enable=${{ steps.ver.outputs.ver != '' }}
type=raw,value=test,enable=${{ steps.ver.outputs.ver == '' }}
labels: |
org.opencontainers.image.title=busbar-headroom
org.opencontainers.image.description=busbar + the headroom-hook prompt-compression plugin, pre-installed and pre-wired — one image, zero plugin setup.
org.opencontainers.image.url=https://getbusbar.com
org.opencontainers.image.documentation=https://github.com/GetBusbar/headroom-hook
org.opencontainers.image.licenses=Apache-2.0
# BOOT GATE, and the reason this workflow exists in its current shape. Every previously
# published bundle built green and was completely non-functional: the busbar inside it could
# not dlopen the plugin the image is built to preload, so the container exited 1 on startup.
# A green build proves nothing about that; only starting the container does. So: build the
# runner-native (amd64) image, RUN it, and require that busbar actually comes up and serves
# /healthz before anything is pushed to a registry.
#
# Only amd64 is booted, because that is the runner's architecture and QEMU-emulating arm64
# here would test the emulator as much as the image. The arm64 leg is covered by the
# linkage/PGO gates in the build job, which run natively on ubuntu-24.04-arm.
#
# BUSBAR_ADMIN_TOKEN and ANTHROPIC_KEY are dummies: this asserts the gateway BOOTS with its
# baked config and the plugin loads, not that it can reach a provider.
- name: Boot the image and require busbar to serve (amd64)
run: |
set -euo pipefail
docker buildx build --load --platform linux/amd64 \
-f docker/bundle/Dockerfile -t busbar-headroom:smoke .
docker run -d --name smoke -p 18080:8080 \
-e ANTHROPIC_KEY=smoke-not-a-real-key \
-e BUSBAR_ADMIN_TOKEN=smoke-not-a-real-token \
busbar-headroom:smoke
ok=""
for _ in $(seq 1 30); do
if [ "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:18080/healthz || true)" = "200" ]; then
ok=1; break
fi
sleep 1
done
echo "===== container log ====="
docker logs smoke 2>&1 || true
echo "========================="
if [ -z "$ok" ]; then
echo "::error::busbar did not serve /healthz - refusing to publish an image that does not boot" >&2
docker rm -f smoke >/dev/null 2>&1 || true
exit 1
fi
# Loading the plugin is the WHOLE POINT of this image, so a 200 from /healthz is not
# sufficient proof on its own. Two POSITIVE assertions, in order of strength:
#
# "plugin validated" - the manifest parsed and passed trust checks.
# Manifest-only; proves nothing about dlopen.
# "hook plugin declared content intent" - busbar called INTO the loaded cdylib and got
# its declaration back. Only reachable if the
# dlopen actually succeeded, so this is THE line
# that distinguishes a working bundle from the
# one that shipped for two releases.
#
# Deliberately NOT grepping for the string "failed to load plugin": busbar emits
# [warn] memfd load unavailable for plugin '...' (failed to load plugin '...' from
# memfd: ...); falling back to private temp staging
# on the perfectly healthy path where memfd_create is unavailable and the loader uses its
# temp-staging fallback instead. Matching that substring would fail a GOOD build. The
# real failure signature is busbar's fail-closed refusal, which is matched instead.
log="$(docker logs smoke 2>&1)"
fail() {
echo "::error::$1" >&2
docker rm -f smoke >/dev/null 2>&1 || true
exit 1
}
grep -q "plugin validated.*busbar-headroom" <<<"$log" \
|| fail "the headroom plugin manifest did not validate inside the image"
grep -q "hook plugin declared content intent.*busbar-headroom" <<<"$log" \
|| fail "the headroom cdylib was never successfully dlopened - busbar never got a content-intent declaration back from it. This is the exact defect this gate exists to catch."
# `if`, not `cmd && fail`: under `set -e` a non-matching grep in an `&&` list makes the
# whole list return 1 and kills the step, i.e. the gate would fail precisely when the
# image is healthy.
if grep -q "refusing to boot/reload with a silently-absent admission gate" <<<"$log"; then
fail "busbar refused to boot with the headroom gate absent"
fi
curl -s -i http://127.0.0.1:18080/healthz
docker rm -f smoke >/dev/null
- name: Build and push
id: push
uses: docker/build-push-action@v6
with:
context: .
file: docker/bundle/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Attest build provenance
uses: actions/attest-build-provenance@v4
with:
subject-name: index.docker.io/${{ env.DOCKERHUB_IMAGE }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: false