Skip to content

Commit 982db07

Browse files
committed
fix(ci): publish multi-arch panel and bot images for linux/amd64 and linux/arm64
- Split container builds into separate jobs per platform (amd64 on ubuntu-22.04, arm64 on ubuntu-24.04-arm) with arch-specific caching scopes - Add merge job that assembles digests into multi-arch manifest lists, requiring both architectures before publishing :latest to avoid stranding Apple Silicon or Linux hosts - Verify merged manifests include both linux/amd64 and linux/arm64 before tagging - Update install-panel.sh to provide better error messaging for arm64 hosts when pulling single-arch images, with guidance on multi-arch requirements - Document multi-arch support in install-panel.md and AI_INSTALL_PROMPT.md - Add platform detection and warnings for macOS hosts attempting server mode (which requires /dev/net/tun on Linux) - Include shasum instructions for macOS checksum verification alongside sha256sum for Linux - Bump versions to 0.1.118
1 parent 0655a57 commit 982db07

9 files changed

Lines changed: 268 additions & 33 deletions

.github/workflows/container-panel.yml

Lines changed: 113 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ name: Container (panel)
33
# The Stitch (web UI) image. Separate from the bot image so a frontend change doesn't
44
# rebuild and republish the bot, and so a broken panel build can never hold up a
55
# bot release.
6+
#
7+
# Multi-arch: linux/amd64 and linux/arm64 on native runners, then a merge job
8+
# stitches the digests into one manifest list. Apple Silicon hosts need the
9+
# arm64 variant — a single-arch amd64 :latest fails with
10+
# "no matching manifest for linux/arm64/v8".
611

712
on:
813
pull_request:
@@ -26,6 +31,9 @@ on:
2631
- 'web/**'
2732
workflow_dispatch:
2833

34+
env:
35+
REGISTRY_IMAGE: ghcr.io/textile-protocol/textile-stitch-panel
36+
2937
permissions:
3038
contents: read
3139
packages: write
@@ -36,8 +44,21 @@ concurrency:
3644

3745
jobs:
3846
build:
39-
runs-on: ubuntu-22.04
47+
runs-on: ${{ matrix.runner }}
48+
strategy:
49+
fail-fast: false
50+
matrix:
51+
include:
52+
- platform: linux/amd64
53+
runner: ubuntu-22.04
54+
- platform: linux/arm64
55+
runner: ubuntu-24.04-arm
4056
steps:
57+
- name: Prepare
58+
run: |
59+
platform='${{ matrix.platform }}'
60+
echo "PLATFORM_PAIR=${platform//\//-}" >> "$GITHUB_ENV"
61+
4162
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
4263
with:
4364
persist-credentials: false
@@ -56,23 +77,104 @@ jobs:
5677
id: meta
5778
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
5879
with:
59-
images: ghcr.io/textile-protocol/textile-stitch-panel
60-
tags: |
61-
type=sha,prefix=sha-
62-
type=raw,value=latest,enable={{is_default_branch}}
63-
type=ref,event=branch
64-
type=ref,event=pr
80+
images: ${{ env.REGISTRY_IMAGE }}
6581

6682
- name: Build container image
83+
id: build
6784
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
6885
with:
6986
context: .
7087
file: Dockerfile.panel
71-
push: ${{ github.event_name != 'pull_request' }}
72-
tags: ${{ steps.meta.outputs.tags }}
88+
platforms: ${{ matrix.platform }}
7389
labels: ${{ steps.meta.outputs.labels }}
74-
cache-from: type=gha,scope=panel
90+
# PRs only compile-check each arch. Digests are pushed (untagged) on
91+
# main so the merge job can assemble the multi-arch manifest list.
92+
outputs: ${{ github.event_name == 'pull_request' && 'type=cacheonly' || format('type=image,name={0},push-by-digest=true,name-canonical=true,push=true', env.REGISTRY_IMAGE) }}
93+
cache-from: type=gha,scope=panel-${{ env.PLATFORM_PAIR }}
7594
# Only write the shared build cache from trusted (non-PR) runs. A PR can
7695
# run arbitrary code during the build; letting it populate the gha cache
7796
# would let it poison a layer later consumed by the main publish build.
78-
cache-to: ${{ github.event_name != 'pull_request' && 'type=gha,mode=max,scope=panel' || '' }}
97+
cache-to: ${{ github.event_name != 'pull_request' && format('type=gha,mode=max,scope=panel-{0}', env.PLATFORM_PAIR) || '' }}
98+
99+
- name: Export digest
100+
if: github.event_name != 'pull_request'
101+
run: |
102+
mkdir -p "${{ runner.temp }}/digests"
103+
digest='${{ steps.build.outputs.digest }}'
104+
touch "${{ runner.temp }}/digests/${digest#sha256:}"
105+
106+
- name: Upload digest
107+
if: github.event_name != 'pull_request'
108+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
109+
with:
110+
name: digests-panel-${{ env.PLATFORM_PAIR }}
111+
path: ${{ runner.temp }}/digests/*
112+
if-no-files-found: error
113+
retention-days: 1
114+
115+
merge:
116+
needs: build
117+
if: github.event_name != 'pull_request'
118+
runs-on: ubuntu-22.04
119+
steps:
120+
- name: Download digests
121+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
122+
with:
123+
path: ${{ runner.temp }}/digests
124+
pattern: digests-panel-*
125+
merge-multiple: true
126+
127+
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
128+
129+
- name: Log in to GitHub Container Registry
130+
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
131+
with:
132+
registry: ghcr.io
133+
username: ${{ github.actor }}
134+
password: ${{ secrets.GITHUB_TOKEN }}
135+
136+
- name: Extract Docker metadata
137+
id: meta
138+
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
139+
with:
140+
images: ${{ env.REGISTRY_IMAGE }}
141+
tags: |
142+
type=sha,prefix=sha-
143+
type=raw,value=latest,enable={{is_default_branch}}
144+
type=ref,event=branch
145+
146+
- name: Create manifest list and push
147+
working-directory: ${{ runner.temp }}/digests
148+
run: |
149+
# Require both digests before tagging :latest — a partial publish would
150+
# strand Apple Silicon (arm64) or Linux servers (amd64).
151+
set -euo pipefail
152+
count="$(find . -type f | wc -l | tr -d ' ')"
153+
if [ "$count" -lt 2 ]; then
154+
echo "error: expected digests for linux/amd64 and linux/arm64, found $count" >&2
155+
find . -type f -print >&2 || true
156+
exit 1
157+
fi
158+
# shellcheck disable=SC2046
159+
docker buildx imagetools create \
160+
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
161+
$(printf '%s@sha256:%s ' "$REGISTRY_IMAGE" *)
162+
163+
- name: Verify multi-arch manifest
164+
run: |
165+
set -euo pipefail
166+
tag='${{ steps.meta.outputs.version }}'
167+
docker buildx imagetools inspect "${REGISTRY_IMAGE}:${tag}"
168+
raw="$(docker buildx imagetools inspect --raw "${REGISTRY_IMAGE}:${tag}")"
169+
arches="$(printf '%s' "$raw" | jq -r '
170+
[.manifests[]?
171+
| select(.platform.os == "linux")
172+
| select(.platform.architecture == "amd64" or .platform.architecture == "arm64")
173+
| .platform.architecture]
174+
| unique | sort | join(",")
175+
')"
176+
if [ "$arches" != "amd64,arm64" ]; then
177+
echo "error: ${REGISTRY_IMAGE}:${tag} must publish linux/amd64 and linux/arm64 (got: ${arches:-none})" >&2
178+
exit 1
179+
fi
180+
echo "ok: ${REGISTRY_IMAGE}:${tag} includes linux/amd64 and linux/arm64"

.github/workflows/container.yml

Lines changed: 112 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
name: Container
22

3+
# Multi-arch: linux/amd64 and linux/arm64 on native runners, then a merge job
4+
# stitches the digests into one manifest list. Apple Silicon hosts need the
5+
# arm64 variant when the panel starts a bot from the published image.
6+
37
on:
48
pull_request:
59
paths:
@@ -22,6 +26,9 @@ on:
2226
- 'src/**'
2327
workflow_dispatch:
2428

29+
env:
30+
REGISTRY_IMAGE: ghcr.io/textile-protocol/textile-stitch
31+
2532
permissions:
2633
contents: read
2734
packages: write
@@ -32,8 +39,21 @@ concurrency:
3239

3340
jobs:
3441
build:
35-
runs-on: ubuntu-22.04
42+
runs-on: ${{ matrix.runner }}
43+
strategy:
44+
fail-fast: false
45+
matrix:
46+
include:
47+
- platform: linux/amd64
48+
runner: ubuntu-22.04
49+
- platform: linux/arm64
50+
runner: ubuntu-24.04-arm
3651
steps:
52+
- name: Prepare
53+
run: |
54+
platform='${{ matrix.platform }}'
55+
echo "PLATFORM_PAIR=${platform//\//-}" >> "$GITHUB_ENV"
56+
3757
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
3858
with:
3959
persist-credentials: false
@@ -52,22 +72,103 @@ jobs:
5272
id: meta
5373
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
5474
with:
55-
images: ghcr.io/textile-protocol/textile-stitch
56-
tags: |
57-
type=sha,prefix=sha-
58-
type=raw,value=latest,enable={{is_default_branch}}
59-
type=ref,event=branch
60-
type=ref,event=pr
75+
images: ${{ env.REGISTRY_IMAGE }}
6176

6277
- name: Build container image
78+
id: build
6379
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
6480
with:
6581
context: .
66-
push: ${{ github.event_name != 'pull_request' }}
67-
tags: ${{ steps.meta.outputs.tags }}
82+
platforms: ${{ matrix.platform }}
6883
labels: ${{ steps.meta.outputs.labels }}
69-
cache-from: type=gha
84+
# PRs only compile-check each arch. Digests are pushed (untagged) on
85+
# main so the merge job can assemble the multi-arch manifest list.
86+
outputs: ${{ github.event_name == 'pull_request' && 'type=cacheonly' || format('type=image,name={0},push-by-digest=true,name-canonical=true,push=true', env.REGISTRY_IMAGE) }}
87+
cache-from: type=gha,scope=bot-${{ env.PLATFORM_PAIR }}
7088
# Only write the shared build cache from trusted (non-PR) runs. A PR can
7189
# run arbitrary code during the build; letting it populate the gha cache
7290
# would let it poison a layer later consumed by the main publish build.
73-
cache-to: ${{ github.event_name != 'pull_request' && 'type=gha,mode=max' || '' }}
91+
cache-to: ${{ github.event_name != 'pull_request' && format('type=gha,mode=max,scope=bot-{0}', env.PLATFORM_PAIR) || '' }}
92+
93+
- name: Export digest
94+
if: github.event_name != 'pull_request'
95+
run: |
96+
mkdir -p "${{ runner.temp }}/digests"
97+
digest='${{ steps.build.outputs.digest }}'
98+
touch "${{ runner.temp }}/digests/${digest#sha256:}"
99+
100+
- name: Upload digest
101+
if: github.event_name != 'pull_request'
102+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
103+
with:
104+
name: digests-bot-${{ env.PLATFORM_PAIR }}
105+
path: ${{ runner.temp }}/digests/*
106+
if-no-files-found: error
107+
retention-days: 1
108+
109+
merge:
110+
needs: build
111+
if: github.event_name != 'pull_request'
112+
runs-on: ubuntu-22.04
113+
steps:
114+
- name: Download digests
115+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
116+
with:
117+
path: ${{ runner.temp }}/digests
118+
pattern: digests-bot-*
119+
merge-multiple: true
120+
121+
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
122+
123+
- name: Log in to GitHub Container Registry
124+
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
125+
with:
126+
registry: ghcr.io
127+
username: ${{ github.actor }}
128+
password: ${{ secrets.GITHUB_TOKEN }}
129+
130+
- name: Extract Docker metadata
131+
id: meta
132+
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
133+
with:
134+
images: ${{ env.REGISTRY_IMAGE }}
135+
tags: |
136+
type=sha,prefix=sha-
137+
type=raw,value=latest,enable={{is_default_branch}}
138+
type=ref,event=branch
139+
140+
- name: Create manifest list and push
141+
working-directory: ${{ runner.temp }}/digests
142+
run: |
143+
# Require both digests before tagging :latest — a partial publish would
144+
# strand Apple Silicon (arm64) or Linux servers (amd64).
145+
set -euo pipefail
146+
count="$(find . -type f | wc -l | tr -d ' ')"
147+
if [ "$count" -lt 2 ]; then
148+
echo "error: expected digests for linux/amd64 and linux/arm64, found $count" >&2
149+
find . -type f -print >&2 || true
150+
exit 1
151+
fi
152+
# shellcheck disable=SC2046
153+
docker buildx imagetools create \
154+
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
155+
$(printf '%s@sha256:%s ' "$REGISTRY_IMAGE" *)
156+
157+
- name: Verify multi-arch manifest
158+
run: |
159+
set -euo pipefail
160+
tag='${{ steps.meta.outputs.version }}'
161+
docker buildx imagetools inspect "${REGISTRY_IMAGE}:${tag}"
162+
raw="$(docker buildx imagetools inspect --raw "${REGISTRY_IMAGE}:${tag}")"
163+
arches="$(printf '%s' "$raw" | jq -r '
164+
[.manifests[]?
165+
| select(.platform.os == "linux")
166+
| select(.platform.architecture == "amd64" or .platform.architecture == "arm64")
167+
| .platform.architecture]
168+
| unique | sort | join(",")
169+
')"
170+
if [ "$arches" != "amd64,arm64" ]; then
171+
echo "error: ${REGISTRY_IMAGE}:${tag} must publish linux/amd64 and linux/arm64 (got: ${arches:-none})" >&2
172+
exit 1
173+
fi
174+
echo "ok: ${REGISTRY_IMAGE}:${tag} includes linux/amd64 and linux/arm64"

.textile-monorepo-source

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
f049ea0ae3129a5318775155ec3fe64364280c0b
1+
ba5e3708f100cbb430de9d92664a2930761fd2c2

.textile-stitch-release-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.117
1+
0.1.118

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "stitch-bot"
3-
version = "0.1.117"
3+
version = "0.1.118"
44
edition = "2021"
55
description = "Stitch — Textile filler-network operator bot; market-makes the filler order book with signed UniswapX limit orders."
66
license = "AGPL-3.0-or-later"

docs/AI_INSTALL_PROMPT.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,10 @@ Ask ONLY this before installing:
5555
- Options: Local computer (recommended) — password login at
5656
http://127.0.0.1:8420 on this machine only; Server — Tailscale, so you can
5757
open Stitch from your other devices on the tailnet.
58-
- Local computer → password auth, loopback bind. No Tailscale.
59-
- Server → Tailscale sidecar, no host port published.
58+
- Local computer → password auth, loopback bind. No Tailscale. Use this on
59+
macOS (Apple Silicon or Intel) and on Linux laptops.
60+
- Server → Tailscale sidecar on a Linux Docker host, no host port published.
61+
Prefer local on a Mac; server mode expects Linux (/dev/net/tun).
6062
- Remember the answer. It selects PANEL_MODE=local or PANEL_MODE=server for
6163
the installer.
6264

0 commit comments

Comments
 (0)