Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 91 additions & 91 deletions .github/workflows/shared-tauri-ci-images.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Shared Tauri CI Images
name: Shared CI and Dev Images

on:
push:
Expand All @@ -16,10 +16,6 @@ permissions:

env:
IMAGE_NAMESPACE: ghcr.io/liminal-hq
CI_DESKTOP_IMAGE: tauri-ci-desktop
CI_MOBILE_IMAGE: tauri-ci-mobile
DEV_DESKTOP_IMAGE: tauri-dev-desktop
DEV_MOBILE_IMAGE: tauri-dev-mobile

jobs:
cadence:
Expand Down Expand Up @@ -54,6 +50,7 @@ jobs:
echo "" >> "${GITHUB_STEP_SUMMARY}"
echo "Skipping this scheduled run to keep a two-week publish cadence." >> "${GITHUB_STEP_SUMMARY}"

# Single-platform images build, smoke-check, and push in one matrixed job.
publish-images:
name: Publish ${{ matrix.label }}
runs-on: ubuntu-latest
Expand All @@ -72,6 +69,18 @@ jobs:
platforms: linux/amd64
smoke_platform: linux/amd64
smoke_profile: ci-mobile
- label: Dev Rust image
image: dev-rust
target: dev-rust
platforms: linux/amd64
smoke_platform: linux/amd64
smoke_profile: dev-rust
- label: Dev web image
image: dev-web
target: dev-web
platforms: linux/amd64
smoke_platform: linux/amd64
smoke_profile: dev-web
- label: Dev desktop image
image: tauri-dev-desktop
target: dev-desktop
Expand Down Expand Up @@ -124,57 +133,7 @@ jobs:
cache-to: type=registry,ref=${{ env.IMAGE_NAMESPACE }}/${{ matrix.image }}:buildcache,mode=max

- name: Run smoke checks
shell: bash
run: |
case "${{ matrix.smoke_profile }}" in
ci-mobile)
docker run --rm "local-smoke:${{ matrix.target }}" bash -lc '
command -v cargo rustup node pnpm cargo-tauri gh sdkmanager java
cargo --version
node --version
pnpm --version
gh --version
sdkmanager --version
'
;;
dev-desktop)
docker run --rm "local-smoke:${{ matrix.target }}" bash -lc '
[[ "$(id -u)" != "0" ]]
command -v cargo rustup node pnpm cargo-tauri gh
cargo --version
node --version
pnpm --version
gh --version
[[ "$CARGO_HOME" == "$HOME/.cargo" ]]
[[ "$RUSTUP_HOME" == "$HOME/.rustup" ]]
[[ "$PNPM_HOME" == "$HOME/.local/share/pnpm" ]]
touch "$CARGO_HOME/.smoke-write"
rm "$CARGO_HOME/.smoke-write"
touch "$PNPM_HOME/.smoke-write"
rm "$PNPM_HOME/.smoke-write"
'
;;
dev-mobile)
docker run --rm "local-smoke:${{ matrix.target }}" bash -lc '
[[ "$(id -u)" != "0" ]]
command -v cargo rustup node pnpm cargo-tauri gh sdkmanager java
cargo --version
node --version
pnpm --version
gh --version
sdkmanager --version
[[ "$ANDROID_HOME" == "$HOME/Android/Sdk" ]]
touch "$CARGO_HOME/.smoke-write"
rm "$CARGO_HOME/.smoke-write"
mkdir -p "$ANDROID_HOME/.smoke"
rmdir "$ANDROID_HOME/.smoke"
'
;;
*)
echo "Unknown smoke profile: ${{ matrix.smoke_profile }}" >&2
exit 1
;;
esac
run: bash docker/ci/smoke-check.sh "${{ matrix.smoke_profile }}" "local-smoke:${{ matrix.target }}"

- name: Build and push image
id: build
Expand Down Expand Up @@ -205,13 +164,13 @@ jobs:
echo "### Tags" >> "${GITHUB_STEP_SUMMARY}"
echo "${TAGS}" | sed 's/^/- `/; s/$/`/' >> "${GITHUB_STEP_SUMMARY}"

# CI desktop is the only multi-platform image (linux/amd64,linux/arm64). Building both
# platforms in one buildx invocation on an amd64 runner emulates arm64 via QEMU, which
# takes 2+ hours for this Dockerfile's from-source cargo installs. Building each platform
# natively (arm64 on GitHub's native arm64 runner, public-repo GA) and merging the
# resulting digests into one manifest avoids emulation entirely.
build-ci-desktop:
name: Build CI desktop image (${{ matrix.platform }})
# Multi-platform images (linux/amd64,linux/arm64): building both platforms in one
# buildx invocation on an amd64 runner emulates arm64 via QEMU, which takes 2+ hours
# for this Dockerfile's from-source cargo installs. Building each platform natively
# (arm64 on GitHub's native arm64 runner, public-repo GA) and merging the resulting
# digests into one manifest avoids emulation entirely.
build-multiarch:
name: Build ${{ matrix.label }} (${{ matrix.platform }})
runs-on: ${{ matrix.runner }}
needs: cadence
if: needs.cadence.outputs.run_publish == 'true'
Expand All @@ -222,9 +181,41 @@ jobs:
fail-fast: false
matrix:
include:
- platform: linux/amd64
- label: CI Rust image
image: ci-rust
target: ci-rust
smoke_profile: ci-rust
platform: linux/amd64
runner: ubuntu-24.04
- platform: linux/arm64
- label: CI Rust image
image: ci-rust
target: ci-rust
smoke_profile: ci-rust
platform: linux/arm64
runner: ubuntu-24.04-arm
- label: CI web image
image: ci-web
target: ci-web
smoke_profile: ci-web
platform: linux/amd64
runner: ubuntu-24.04
- label: CI web image
image: ci-web
target: ci-web
smoke_profile: ci-web
platform: linux/arm64
runner: ubuntu-24.04-arm
- label: CI desktop image
image: tauri-ci-desktop
target: ci-desktop
smoke_profile: ci-desktop
platform: linux/amd64
runner: ubuntu-24.04
- label: CI desktop image
image: tauri-ci-desktop
target: ci-desktop
smoke_profile: ci-desktop
platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
- name: Checkout
Expand All @@ -251,36 +242,29 @@ jobs:
with:
context: .
file: docker/ci/Dockerfile
target: ci-desktop
target: ${{ matrix.target }}
platforms: ${{ matrix.platform }}
load: true
tags: local-smoke:ci-desktop-${{ steps.platform_pair.outputs.pair }}
cache-from: type=registry,ref=${{ env.IMAGE_NAMESPACE }}/${{ env.CI_DESKTOP_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ env.IMAGE_NAMESPACE }}/${{ env.CI_DESKTOP_IMAGE }}:buildcache,mode=max
tags: local-smoke:${{ matrix.target }}-${{ steps.platform_pair.outputs.pair }}
cache-from: type=registry,ref=${{ env.IMAGE_NAMESPACE }}/${{ matrix.image }}:buildcache
cache-to: type=registry,ref=${{ env.IMAGE_NAMESPACE }}/${{ matrix.image }}:buildcache,mode=max

- name: Run smoke checks
env:
PLATFORM_PAIR: ${{ steps.platform_pair.outputs.pair }}
run: |
docker run --rm "local-smoke:ci-desktop-${PLATFORM_PAIR}" bash -lc '
command -v cargo rustup node pnpm cargo-tauri gh
cargo --version
node --version
pnpm --version
gh --version
'
run: bash docker/ci/smoke-check.sh "${{ matrix.smoke_profile }}" "local-smoke:${{ matrix.target }}-${PLATFORM_PAIR}"

- name: Build and push by digest
id: build
uses: docker/build-push-action@v6.19.2
with:
context: .
file: docker/ci/Dockerfile
target: ci-desktop
target: ${{ matrix.target }}
platforms: ${{ matrix.platform }}
outputs: type=image,name=${{ env.IMAGE_NAMESPACE }}/${{ env.CI_DESKTOP_IMAGE }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=registry,ref=${{ env.IMAGE_NAMESPACE }}/${{ env.CI_DESKTOP_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ env.IMAGE_NAMESPACE }}/${{ env.CI_DESKTOP_IMAGE }}:buildcache,mode=max
outputs: type=image,name=${{ env.IMAGE_NAMESPACE }}/${{ matrix.image }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=registry,ref=${{ env.IMAGE_NAMESPACE }}/${{ matrix.image }}:buildcache
cache-to: type=registry,ref=${{ env.IMAGE_NAMESPACE }}/${{ matrix.image }}:buildcache,mode=max

- name: Export digest
env:
Expand All @@ -292,27 +276,40 @@ jobs:
- name: Upload digest
uses: actions/upload-artifact@v4.6.2
with:
name: digests-ci-desktop-${{ steps.platform_pair.outputs.pair }}
name: digests-${{ matrix.target }}-${{ steps.platform_pair.outputs.pair }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1

merge-ci-desktop:
name: Publish CI desktop image (merged manifest)
merge-multiarch:
name: Publish ${{ matrix.label }} (merged manifest)
runs-on: ubuntu-24.04
needs: build-ci-desktop
needs: build-multiarch
# No checkout in this job (only artifact download + buildx/GHCR), so
# `contents: read` is deliberately omitted here. If a future step needs
# repo content, add it back explicitly.
permissions:
actions: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- label: CI Rust image
image: ci-rust
target: ci-rust
- label: CI web image
image: ci-web
target: ci-web
- label: CI desktop image
image: tauri-ci-desktop
target: ci-desktop
steps:
- name: Download digests
uses: actions/download-artifact@v4.3.0
with:
path: /tmp/digests
pattern: digests-ci-desktop-*
pattern: digests-${{ matrix.target }}-*
merge-multiple: true

- name: Set up Docker Buildx
Expand All @@ -329,7 +326,7 @@ jobs:
id: meta
uses: docker/metadata-action@v5.10.0
with:
images: ${{ env.IMAGE_NAMESPACE }}/${{ env.CI_DESKTOP_IMAGE }}
images: ${{ env.IMAGE_NAMESPACE }}/${{ matrix.image }}
tags: |
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=staging,enable=${{ github.ref != 'refs/heads/main' }}
Expand All @@ -338,23 +335,26 @@ jobs:

- name: Create manifest list and push
working-directory: /tmp/digests
env:
IMAGE_REF: ${{ env.IMAGE_NAMESPACE }}/${{ matrix.image }}
run: |
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.IMAGE_NAMESPACE }}/${{ env.CI_DESKTOP_IMAGE }}@sha256:%s ' *)
$(printf "${IMAGE_REF}@sha256:%s " *)

- name: Inspect image
env:
VERSION: ${{ steps.meta.outputs.version }}
run: docker buildx imagetools inspect "${{ env.IMAGE_NAMESPACE }}/${{ env.CI_DESKTOP_IMAGE }}:${VERSION}"
IMAGE_REF: ${{ env.IMAGE_NAMESPACE }}/${{ matrix.image }}
run: docker buildx imagetools inspect "${IMAGE_REF}:${VERSION}"

- name: Write publish summary
env:
TAGS: ${{ steps.meta.outputs.tags }}
run: |
echo "## CI desktop image published" >> "${GITHUB_STEP_SUMMARY}"
echo "## ${{ matrix.label }} published" >> "${GITHUB_STEP_SUMMARY}"
echo "" >> "${GITHUB_STEP_SUMMARY}"
echo "- Image: \`${{ env.IMAGE_NAMESPACE }}/${{ env.CI_DESKTOP_IMAGE }}\`" >> "${GITHUB_STEP_SUMMARY}"
echo "- Image: \`${{ env.IMAGE_NAMESPACE }}/${{ matrix.image }}\`" >> "${GITHUB_STEP_SUMMARY}"
echo "- Platforms: \`linux/amd64,linux/arm64\` (built natively per-arch, merged)" >> "${GITHUB_STEP_SUMMARY}"
echo "" >> "${GITHUB_STEP_SUMMARY}"
echo "### Tags" >> "${GITHUB_STEP_SUMMARY}"
Expand Down
14 changes: 8 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,22 @@ There is no app build/lint/test suite (no package.json, no source code to compil

### Shared container images (`docker/ci/Dockerfile`)

A single multi-stage Dockerfile produces four published image targets, built from two parallel stage chains:
A single multi-stage Dockerfile produces eight published image targets, built from two parallel tier chains where each tier adds exactly one concern:

- `ci-base` `ci-toolchain` → **`ci-desktop`** / **`ci-mobile`**: root-friendly images for GitHub Actions and other automated pipelines. Tool paths live under `/usr/local` (`CARGO_HOME`, `RUSTUP_HOME`); mobile adds Android SDK/NDK under `/opt/android-sdk`.
- `dev-base` `dev-toolchain` → **`dev-desktop`** / **`dev-mobile`**: non-root devcontainer images (runs as `vscode` user) for interactive local development. Tool paths live under `/home/vscode` instead; mobile adds Android SDK/NDK under `$HOME/Android/Sdk`.
- `ci-base` (unpublished, universal CLI only) → **`ci-rust`** (pinned Rust + clippy/rustfmt/cargo-nextest) → **`ci-desktop`** (+ Node/pnpm/Bun + GTK/webkit GUI stack + GStreamer/xvfb/emoji fonts + tauri-cli) → **`ci-mobile`** (+ Java + Android SDK/NDK); plus **`ci-web`** (Node/pnpm/Bun, no Rust) as a sibling off `ci-base`. Root-friendly images for automated pipelines; tool paths under `/usr/local`, Android under `/opt/android-sdk`, Bun under `/usr/local/bun`.
- `dev-base` (unpublished, universal CLI + vim/ripgrep/fd/jq) → **`dev-rust`** → **`dev-desktop`** (adds X11 inspection tools too) → **`dev-mobile`**; plus **`dev-web`** off `dev-base`. Non-root devcontainer images (run as `vscode`); tool paths under `/home/vscode`, Android under `$HOME/Android/Sdk`, Bun under `$HOME/.bun`.

Both families install the GitHub CLI (`gh`) from GitHub's own apt repo (not the distro package) and pin Node/pnpm/Rust versions via build args at the top of the file. `cargo-tauri` is installed from a Tauri fork branch (`feat/truly-portable-appimage`) to get portable AppImage behaviour, paired with `TAURI_BUNDLER_NEW_APPIMAGE_FORMAT=true`. Images publish to `ghcr.io/liminal-hq/tauri-{ci,dev}-{desktop,mobile}`.
The GUI/Tauri system libraries live only in the desktop tiers — never in a base or lean tier. The Node+pnpm+Bun install block deliberately appears in both the web and Tauri-desktop tiers of each family (single inheritance can't give the desktop tier two parents); all sites consume the same `ARG` pins so versions can't drift.

Both families install the GitHub CLI (`gh`) from GitHub's own apt repo (not the distro package) and pin Node/pnpm/Bun/Rust versions via build args at the top of the file. `cargo-tauri` is installed from a Tauri fork branch (`feat/truly-portable-appimage`) to get portable AppImage behaviour, paired with `TAURI_BUNDLER_NEW_APPIMAGE_FORMAT=true`. Images publish to `ghcr.io/liminal-hq/{ci,dev}-{rust,web}` and `ghcr.io/liminal-hq/tauri-{ci,dev}-{desktop,mobile}`. Smoke checks live in `docker/ci/smoke-check.sh` (per-tier profiles, including leanness assertions on `ci-rust`/`ci-web`).

Keep version pins aligned across the CI and dev chains, and keep target names using the `ci-`/`dev-` prefixes — see `docs/reference/shared-image-layout.md` for the full layout contract and `docs/reference/shared-image-implementation-spec.md` for the implementation spec.

### Image publish workflow (`.github/workflows/shared-tauri-ci-images.yml`)

Builds and pushes the four images above on push to `main` (when `docker/ci/**` or the workflow itself changes), on `workflow_dispatch`, and on a weekly `schedule` gated to a two-week publish cadence (an ISO week-number parity check in the `cadence` job). Each image build does a local single-platform "smoke" build/run first, then the real (possibly multi-platform) build/push.
Builds and pushes the eight images above on push to `main` (when `docker/ci/**` or the workflow itself changes), on `workflow_dispatch`, and on a weekly `schedule` gated to a two-week publish cadence (an ISO week-number parity check in the `cadence` job). Each image build does a local single-platform "smoke" build/run first (via `docker/ci/smoke-check.sh`), then the real build/push.

`ci-desktop` is the only multi-platform image (`linux/amd64` + `linux/arm64`) and is handled specially to avoid QEMU emulation: `build-ci-desktop` builds each platform natively on its own runner (including GitHub's native `ubuntu-24.04-arm` runner) and pushes by digest, then `merge-ci-desktop` combines the digests into one multi-arch manifest via `docker buildx imagetools create`. The other three images build directly in a single matrixed job (`publish-images`).
`ci-rust`, `ci-web`, and `ci-desktop` are multi-platform (`linux/amd64` + `linux/arm64`) and are handled specially to avoid QEMU emulation: `build-multiarch` builds each image×platform combination natively on its own runner (including GitHub's native `ubuntu-24.04-arm` runner) and pushes by digest, then `merge-multiarch` combines each image's digests into one multi-arch manifest via `docker buildx imagetools create`. The five single-platform images build directly in one matrixed job (`publish-images`).

### Reusable AppImage packaging workflow (`.github/workflows/package-arch-appimage.yml`)

Expand Down
Loading