Skip to content

Commit 1fa63ba

Browse files
tlongwell-blocknpub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc
andauthored
ci(docker): publish public ghcr.io/block/buzz image (native multi-arch) (#986)
Signed-off-by: Tyler Longwell <[email protected]> Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co>
1 parent 84f499c commit 1fa63ba

3 files changed

Lines changed: 383 additions & 33 deletions

File tree

‎.dockerignore‎

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,54 @@
1+
# Build outputs
12
target/
3+
**/target/
4+
desktop/src-tauri/target/
5+
6+
# Native non-relay UI builds (not part of the public image)
27
desktop/
8+
mobile/
9+
10+
# Node artifacts (built inside the image; host outputs would invalidate cache)
11+
node_modules/
12+
**/node_modules/
13+
web/dist/
14+
15+
# VCS, IDE, scratch
316
.git/
17+
.github/
18+
.gitignore
19+
.gitattributes
20+
.vscode/
21+
.idea/
422
.scratch/
5-
*.md
6-
!README.md
7-
.env*
23+
24+
# Local environment / secrets — never want these in the build context
25+
.env
26+
.env.*
27+
!.env.example
28+
*.pem
29+
*.key
30+
secrets/
31+
32+
# OS junk
33+
.DS_Store
34+
Thumbs.db
35+
36+
# CI artifacts and tooling that the image doesn't need
37+
.cache/
38+
coverage/
39+
dist/
40+
playwright-report/
41+
test-results/
42+
bin/
43+
sidecars/
44+
45+
# Docs the image doesn't ship. examples/ stays — examples/countdown-bot is
46+
# a Cargo workspace member, so cargo-chef needs its manifest. Markdown files
47+
# inside crates (Cargo.toml `readme = ...`) likewise need to be present to
48+
# avoid manifest warnings; the savings from excluding them are negligible.
49+
docs/
50+
51+
# Old Dockerfile noise (this Dockerfile is THE source of truth now)
52+
docker-compose.yml
53+
docker-compose.*.yml
54+
prometheus.yml

‎.github/workflows/docker.yml‎

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
name: Docker image
2+
3+
# Builds and publishes the public Buzz relay image as ghcr.io/block/buzz.
4+
#
5+
# Strategy: each architecture builds on its native runner (ubuntu-24.04 for
6+
# amd64, ubuntu-24.04-arm for arm64), pushes to GHCR by digest, then a final
7+
# job stitches the per-arch digests into a single multi-arch manifest.
8+
# This avoids QEMU emulation (~10× slower for Rust) at zero cost on free
9+
# GitHub-hosted runners.
10+
#
11+
# Triggers:
12+
# - push to main → :main + :sha-<7>
13+
# - push tags v*.*.* → :latest + :{version} + :{major}.{minor} + :{major}
14+
# - pull_request → build only (no push), cache stays warm
15+
# - workflow_dispatch → manual canary
16+
17+
on:
18+
push:
19+
branches: [main]
20+
tags: ["v[0-9]*"]
21+
pull_request:
22+
paths:
23+
- "Dockerfile"
24+
- ".dockerignore"
25+
- ".github/workflows/docker.yml"
26+
- "Cargo.toml"
27+
- "Cargo.lock"
28+
- "rust-toolchain.toml"
29+
- "crates/**"
30+
- "web/**"
31+
- "package.json"
32+
- "pnpm-lock.yaml"
33+
- "pnpm-workspace.yaml"
34+
- "patches/**"
35+
workflow_dispatch:
36+
37+
# One image build per ref; cancel superseded PR builds, but never cancel
38+
# tag/main builds (publishing must not be aborted mid-flight).
39+
concurrency:
40+
group: docker-${{ github.workflow }}-${{ github.ref }}
41+
cancel-in-progress: ${{ github.ref_type == 'branch' && github.event_name == 'pull_request' }}
42+
43+
permissions: {}
44+
45+
env:
46+
# Single source of truth for the image name. Set GHCR_IMAGE as a repo
47+
# variable to override (e.g., for forks that want to push to their own
48+
# namespace without forking this file).
49+
IMAGE_NAME: ${{ vars.GHCR_IMAGE != '' && vars.GHCR_IMAGE || 'ghcr.io/block/buzz' }}
50+
51+
jobs:
52+
build:
53+
name: Build (${{ matrix.platform }})
54+
runs-on: ${{ matrix.runner }}
55+
timeout-minutes: 60
56+
permissions:
57+
contents: read
58+
packages: write # push to GHCR
59+
id-token: write # OIDC for build provenance attestation
60+
attestations: write
61+
strategy:
62+
fail-fast: false
63+
matrix:
64+
include:
65+
- platform: linux/amd64
66+
runner: ubuntu-24.04
67+
arch: amd64
68+
- platform: linux/arm64
69+
runner: ubuntu-24.04-arm
70+
arch: arm64
71+
72+
outputs:
73+
# Used downstream by `merge` to stitch the manifest.
74+
version: ${{ steps.meta.outputs.version }}
75+
76+
steps:
77+
- name: Checkout
78+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
79+
with:
80+
persist-credentials: false
81+
82+
- name: Set up Docker Buildx
83+
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
84+
with:
85+
# Default parallelism of 4 OOMs the 7GB GitHub runner during Rust
86+
# compiles (see moby/buildkit#3969). Vaultwarden hit this; we will
87+
# too without the cap.
88+
buildkitd-config-inline: |
89+
[worker.oci]
90+
max-parallelism = 2
91+
92+
- name: Log in to GHCR
93+
# Skip on pull_request from forks — no GHCR creds, build-only.
94+
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
95+
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
96+
with:
97+
registry: ghcr.io
98+
username: ${{ github.repository_owner }}
99+
password: ${{ secrets.GITHUB_TOKEN }}
100+
101+
- name: Extract metadata
102+
id: meta
103+
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
104+
with:
105+
images: ${{ env.IMAGE_NAME }}
106+
# Tag matrix — every main commit gets sha-<7>, releases get full
107+
# semver family. Pull requests get nothing (push: false below).
108+
tags: |
109+
type=ref,event=branch
110+
type=sha,prefix=sha-,format=short
111+
type=semver,pattern={{version}}
112+
type=semver,pattern={{major}}.{{minor}}
113+
type=semver,pattern={{major}}
114+
type=raw,value=latest,enable={{is_default_branch}}
115+
labels: |
116+
org.opencontainers.image.title=Buzz
117+
org.opencontainers.image.description=WebSocket relay server for the Buzz communications platform
118+
org.opencontainers.image.licenses=Apache-2.0
119+
120+
- name: Build and push by digest
121+
id: build
122+
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
123+
with:
124+
context: .
125+
file: ./Dockerfile
126+
platforms: ${{ matrix.platform }}
127+
labels: ${{ steps.meta.outputs.labels }}
128+
# Push by digest, not by tag — the merge job assembles the tags
129+
# into one multi-arch manifest. This is what makes the native-arm
130+
# matrix possible.
131+
outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
132+
cache-from: |
133+
type=registry,ref=${{ env.IMAGE_NAME }}-buildcache:${{ matrix.arch }}
134+
cache-to: |
135+
type=registry,ref=${{ env.IMAGE_NAME }}-buildcache:${{ matrix.arch }},mode=max,compression=zstd
136+
137+
- name: Export digest
138+
if: github.event_name != 'pull_request'
139+
env:
140+
DIGEST: ${{ steps.build.outputs.digest }}
141+
run: |
142+
mkdir -p /tmp/digests
143+
touch "/tmp/digests/${DIGEST#sha256:}"
144+
145+
- name: Upload digest
146+
if: github.event_name != 'pull_request'
147+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
148+
with:
149+
name: digests-${{ matrix.arch }}
150+
path: /tmp/digests/*
151+
if-no-files-found: error
152+
retention-days: 1
153+
154+
merge:
155+
name: Merge multi-arch manifest
156+
if: github.event_name != 'pull_request'
157+
runs-on: ubuntu-24.04
158+
needs: build
159+
timeout-minutes: 15
160+
permissions:
161+
contents: read
162+
packages: write # push the merged manifest
163+
id-token: write # OIDC for provenance attestation on the manifest
164+
attestations: write
165+
166+
steps:
167+
- name: Download all per-arch digests
168+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
169+
with:
170+
path: /tmp/digests
171+
pattern: digests-*
172+
merge-multiple: true
173+
174+
- name: Set up Docker Buildx
175+
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
176+
177+
- name: Log in to GHCR
178+
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
179+
with:
180+
registry: ghcr.io
181+
username: ${{ github.repository_owner }}
182+
password: ${{ secrets.GITHUB_TOKEN }}
183+
184+
- name: Extract metadata
185+
id: meta
186+
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
187+
with:
188+
images: ${{ env.IMAGE_NAME }}
189+
tags: |
190+
type=ref,event=branch
191+
type=sha,prefix=sha-,format=short
192+
type=semver,pattern={{version}}
193+
type=semver,pattern={{major}}.{{minor}}
194+
type=semver,pattern={{major}}
195+
type=raw,value=latest,enable={{is_default_branch}}
196+
197+
- name: Create and push manifest list
198+
id: manifest
199+
working-directory: /tmp/digests
200+
env:
201+
IMAGE_NAME: ${{ env.IMAGE_NAME }}
202+
META_TAGS: ${{ steps.meta.outputs.tags }}
203+
run: |
204+
set -euo pipefail
205+
# Build -t flags from the metadata-action output.
206+
tags=()
207+
while IFS= read -r tag; do
208+
[ -n "$tag" ] && tags+=("-t" "$tag")
209+
done <<< "$META_TAGS"
210+
211+
# Build the digest refs from the per-arch artifacts.
212+
digests=()
213+
for digest in *; do
214+
digests+=("${IMAGE_NAME}@sha256:${digest}")
215+
done
216+
217+
docker buildx imagetools create "${tags[@]}" "${digests[@]}"
218+
219+
# Capture the merged manifest digest for the attestation step.
220+
first_tag=$(echo "$META_TAGS" | head -n1)
221+
merged_digest=$(docker buildx imagetools inspect "$first_tag" \
222+
--format '{{json .Manifest}}' | jq -r '.digest')
223+
echo "digest=${merged_digest}" >> "$GITHUB_OUTPUT"
224+
225+
- name: Attest provenance for the merged image
226+
# Sigstore-signed in-toto attestation, verifiable with:
227+
# gh attestation verify oci://ghcr.io/block/buzz:<tag> --owner block
228+
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
229+
with:
230+
subject-name: ${{ env.IMAGE_NAME }}
231+
subject-digest: ${{ steps.manifest.outputs.digest }}
232+
push-to-registry: true
233+
234+
- name: Summary
235+
env:
236+
IMAGE_NAME: ${{ env.IMAGE_NAME }}
237+
MERGED_DIGEST: ${{ steps.manifest.outputs.digest }}
238+
META_TAGS: ${{ steps.meta.outputs.tags }}
239+
run: |
240+
{
241+
echo "### Published \`${IMAGE_NAME}\`"
242+
echo
243+
echo "**Digest:** \`${MERGED_DIGEST}\`"
244+
echo
245+
echo "**Tags:**"
246+
echo '```'
247+
echo "${META_TAGS}"
248+
echo '```'
249+
echo
250+
echo "Verify provenance:"
251+
echo '```'
252+
echo "gh attestation verify oci://${IMAGE_NAME}@${MERGED_DIGEST} --owner block"
253+
echo '```'
254+
} >> "$GITHUB_STEP_SUMMARY"

0 commit comments

Comments
 (0)