Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Add local release pipeline and migrate CI to GitHub Actions
Rūdolfs Ošiņš committed 1 month ago
commit 3ef1eb612ed574156779c10bfab41889d9e111f5
parent e48cc10
18 files changed +550 -391
added .github/workflows/check-arch-package.yaml
@@ -0,0 +1,57 @@
+
on:
+
  pull_request:
+
    paths:
+
      - 'arch/**'
+
  push:
+
    paths:
+
      - 'arch/**'
+
  workflow_dispatch:
+

+
jobs:
+
  build-arch:
+
    runs-on: ubuntu-latest
+
    container:
+
      image: archlinux:base-devel
+
      options: --user root
+

+
    steps:
+
      - name: Install git
+
        run: pacman -Sy --noconfirm git
+

+
      - uses: actions/checkout@v4
+

+
      - name: Set up builder user
+
        run: |
+
          useradd -m builder
+
          echo 'builder ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/builder
+
          chown -R builder:builder "$GITHUB_WORKSPACE"
+

+
      - name: Cache cargo registry
+
        uses: actions/cache@v4
+
        with:
+
          path: /home/builder/.cargo/registry
+
          key: arch-cargo-${{ hashFiles('arch/radicle-desktop/PKGBUILD') }}
+
          restore-keys: arch-cargo-
+

+
      - name: Install radicle-bin AUR dependency
+
        run: |
+
          cd /home/builder
+
          git clone https://aur.archlinux.org/radicle-bin.git --depth=1
+
          chown -R builder:builder radicle-bin
+
          sudo -u builder makepkg --dir radicle-bin --install --syncdeps --noconfirm
+

+
      - name: Check .SRCINFO is up-to-date
+
        run: |
+
          cd arch/radicle-desktop
+
          sudo -u builder makepkg --printsrcinfo > .SRCINFO
+
          sudo -u builder git -C "$GITHUB_WORKSPACE" diff --exit-code arch/radicle-desktop/.SRCINFO
+

+
      - name: Build package
+
        run: |
+
          sudo -u builder env \
+
            CI=true \
+
            BUILDDIR=/home/builder/build \
+
            PKGDEST=/home/builder \
+
            SRCDEST=/home/builder \
+
            makepkg --dir "$GITHUB_WORKSPACE/arch/radicle-desktop" \
+
              --syncdeps --noconfirm --force
added .github/workflows/e2e.yaml
@@ -0,0 +1,45 @@
+
on:
+
  pull_request:
+
  push:
+
  workflow_dispatch:
+

+
jobs:
+
  e2e:
+
    runs-on: ubuntu-latest
+
    container:
+
      image: quay.io/radicle_garden/radicle-desktop-e2e:latest
+

+
    steps:
+
      - uses: actions/checkout@v4
+

+
      - name: Cache cargo registry
+
        uses: actions/cache@v4
+
        with:
+
          path: |
+
            ~/.cargo/registry
+
            ~/.cargo/git
+
          key: rust-e2e-${{ hashFiles('**/Cargo.lock') }}
+
          restore-keys: rust-e2e-
+

+
      - name: Cache node_modules
+
        id: cache-npm
+
        uses: actions/cache@v4
+
        with:
+
          path: node_modules
+
          key: node-modules-${{ hashFiles('package-lock.json') }}
+

+
      - run: npm ci
+
        if: steps.cache-npm.outputs.cache-hit != 'true'
+

+
      - name: Cache heartwood binaries
+
        uses: actions/cache@v4
+
        with:
+
          path: tests/tmp/bin
+
          key: heartwood-${{ hashFiles('tests/support/heartwood-release') }}
+

+
      - run: scripts/install-binaries
+
      - run: npm run build:http
+
      - run: mkdir -p tests/artifacts
+

+
      - name: Run e2e tests
+
        run: npm run test:e2e -- --project webkit
added .github/workflows/lint.yaml
@@ -0,0 +1,44 @@
+
on:
+
  pull_request:
+
  push:
+
  workflow_dispatch:
+

+
jobs:
+
  lint-typescript:
+
    runs-on: ubuntu-latest
+
    container:
+
      image: node:22.11.0
+

+
    steps:
+
      - uses: actions/checkout@v4
+

+
      - name: Cache node_modules
+
        id: cache-npm
+
        uses: actions/cache@v4
+
        with:
+
          path: node_modules
+
          key: node-modules-${{ hashFiles('package-lock.json') }}
+

+
      - run: npm ci
+
        if: steps.cache-npm.outputs.cache-hit != 'true'
+

+
      - run: npm run check-js
+

+
  lint-rust:
+
    runs-on: ubuntu-latest
+
    container:
+
      image: quay.io/radicle_garden/radicle-desktop-base:latest
+

+
    steps:
+
      - uses: actions/checkout@v4
+

+
      - name: Cache cargo registry
+
        uses: actions/cache@v4
+
        with:
+
          path: |
+
            ~/.cargo/registry
+
            ~/.cargo/git
+
          key: rust-lint-${{ hashFiles('**/Cargo.lock') }}
+
          restore-keys: rust-lint-
+

+
      - run: scripts/check-rs
added .github/workflows/unit-test.yaml
@@ -0,0 +1,25 @@
+
on:
+
  pull_request:
+
  push:
+
  workflow_dispatch:
+

+
jobs:
+
  unit-tests:
+
    runs-on: ubuntu-latest
+
    container:
+
      image: node:22.11.0
+

+
    steps:
+
      - uses: actions/checkout@v4
+

+
      - name: Cache node_modules
+
        id: cache-npm
+
        uses: actions/cache@v4
+
        with:
+
          path: node_modules
+
          key: node-modules-${{ hashFiles('package-lock.json') }}
+

+
      - run: npm ci
+
        if: steps.cache-npm.outputs.cache-hit != 'true'
+

+
      - run: npm run test:unit
modified .gitignore
@@ -1,6 +1,7 @@
/build/
/target/
-
/release-artifacts
+
/target-release/
+
/releases/
node_modules/

# Tauri
deleted .woodpecker/build-arch.yaml
@@ -1,58 +0,0 @@
-
when:
-
  - event: pull_request
-
  - event: push
-
    branch: main
-
  - event: manual
-

-
variables:
-
  - &plugin-sccache-read-only
-
    s3-bucket: "build-caches"
-
    s3-endpoint: "https://minio-api.radworks.garden"
-
    s3-key-prefix: "radicle-desktop"
-
    save-if: "false"
-
    s3-access-key:
-
      from_secret: minio_access_key
-
    s3-secret-access-key:
-
      from_secret: minio_secret_key
-

-
steps:
-
  read_cache:
-
    image: quay.io/radicle_garden/plugin-sccache:latest
-
    volumes:
-
      - sccache:/sccache_data
-
    settings: *plugin-sccache-read-only
-

-
  update_cache:
-
    image: quay.io/radicle_garden/plugin-sccache:latest
-
    when:
-
      evaluate: 'CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == CI_REPO_DEFAULT_BRANCH && CI_COMMIT_MESSAGE startsWith "Release"'
-
    volumes:
-
      - sccache:/sccache_data
-
    settings:
-
      <<: *plugin-sccache-read-only
-
      save-if: true
-

-
  build-arch:
-
    image: docker.io/library/archlinux:base-devel
-
    when:
-
      - path: "arch/*/*"
-
      - event: manual
-
    commands:
-
      - pacman -Sy --noconfirm git
-
      - useradd -m builder
-
      - 'echo "builder ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/builder'
-
      - chown -R builder:builder /home/builder
-
      - su builder
-
      - set -e
-
      - cd /home/builder
-
      - rm -rf radicle-bin && git clone https://aur.archlinux.org/radicle-bin.git --depth=1
-
      - makepkg --dir radicle-bin --install --syncdeps --noconfirm
-
      - cd $CI_WORKSPACE/arch/radicle-desktop
-
      # Ensure .SCRINFO is up-to-date
-
      - makepkg --printsrcinfo > .SRCINFO && git diff --exit-code .SRCINFO
-
      - |
-
        CI=true \
-
        BUILDDIR=/home/builder/build \
-
        PKGDEST=/home/builder/ \
-
        SRCDEST=/home/builder/ \
-
        makepkg --syncdeps --noconfirm --force
deleted .woodpecker/build.yaml
@@ -1,84 +0,0 @@
-
when:
-
  - event: pull_request
-
  - event: push
-
    branch: main
-
  - event: manual
-

-
variables:
-
  - &plugin-sccache-read-only
-
    s3-bucket: "build-caches"
-
    s3-endpoint: "https://minio-api.radworks.garden"
-
    s3-key-prefix: "radicle-desktop"
-
    save-if: "false"
-
    s3-access-key:
-
      from_secret: minio_access_key
-
    s3-secret-access-key:
-
      from_secret: minio_secret_key
-

-
steps:
-
  read_cache:
-
    image: quay.io/radicle_garden/plugin-sccache:latest
-
    volumes:
-
      - sccache:/sccache_data
-
    settings: *plugin-sccache-read-only
-

-
  update_cache:
-
    image: quay.io/radicle_garden/plugin-sccache:latest
-
    when:
-
      evaluate: 'CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == CI_REPO_DEFAULT_BRANCH && CI_COMMIT_MESSAGE startsWith "Release"'
-
    volumes:
-
      - sccache:/sccache_data
-
    settings:
-
      <<: *plugin-sccache-read-only
-
      save-if: true
-

-
  build:
-
    image: quay.io/radicle_garden/radicle-desktop-base:latest
-
    volumes:
-
      - sccache:/sccache_data
-
    environment:
-
      CI: true
-
    entrypoint:
-
      - "/bin/bash"
-
      - "-c"
-
      - |
-
        set -euo pipefail
-

-
        export PATH="/sccache_data:$PATH"
-
        source .sccache
-

-
        export VERSION=$(jq -r '.version' crates/radicle-tauri/tauri.conf.json)
-
        export ARTIFACT_DIR="radicle-desktop/pre-release/""$VERSION""_""${CI_COMMIT_SHA:0:8}"
-
        export LATEST_DIR="radicle-desktop/latest"
-
        mkdir -p "$ARTIFACT_DIR"
-
        mkdir -p "$LATEST_DIR"
-
        mkdir -p "$LATEST_DIR""/deb"
-
        mkdir -p "$LATEST_DIR""/appimage"
-
        echo $ARTIFACT_DIR
-
        echo $LATEST_DIR
-

-
        npm ci
-
        npm run tauri build
-

-
        sccache --show-stats
-

-
        # Organize build artifacts for upload
-
        cp target/release/bundle/deb/*.deb "$ARTIFACT_DIR"
-
        cp target/release/bundle/appimage/*.AppImage "$ARTIFACT_DIR"
-

-
        # Update latest build artifacts for upload
-
        cp target/release/bundle/deb/*.deb "$LATEST_DIR/deb"
-
        cp target/release/bundle/appimage/*.AppImage "$LATEST_DIR/appimage"
-

-
  upload-artifacts:
-
    image: woodpeckerci/plugin-s3
-
    settings:
-
      endpoint: https://minio-api.radworks.garden
-
      bucket: radworks-releases
-
      source: radicle-desktop/pre-release/*/*.{deb,AppImage}
-
      target: ""
-
      path_style: true
-
      access_key:
-
        from_secret: minio_access_key
-
      secret_key:
-
        from_secret: minio_secret_key
deleted .woodpecker/docker/Dockerfile
@@ -1,25 +0,0 @@
-
FROM devraymondsh/ubuntu-rust:24.04-1.84
-

-
RUN apt-get update && apt-get install -y \
-
    build-essential \
-
    jq \
-
    curl \
-
    git \
-
    wget \
-
    file \
-
    zstd \
-
    libxdo-dev \
-
    libssl-dev \
-
    libayatana-appindicator3-dev \
-
    librsvg2-dev \
-
    libwebkit2gtk-4.1-0=2.44.0-2 \
-
    libwebkit2gtk-4.1-dev=2.44.0-2 \
-
    libjavascriptcoregtk-4.1-0=2.44.0-2 \
-
    libjavascriptcoregtk-4.1-dev=2.44.0-2 \
-
    gir1.2-javascriptcoregtk-4.1=2.44.0-2 \
-
    gir1.2-webkit2-4.1=2.44.0-2
-

-
RUN rustup component add rustfmt clippy
-
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && apt-get install -y nodejs
-

-
SHELL ["/bin/bash", "-c"]
deleted .woodpecker/docker/playwright.Dockerfile
@@ -1,26 +0,0 @@
-
FROM mcr.microsoft.com/playwright:v1.52.0-noble
-

-
# Install Rust
-
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain 1.84-x86_64-unknown-linux-gnu
-
ENV PATH="/root/.cargo/bin:${PATH}"
-

-
RUN apt-get update && apt-get install -y \
-
    build-essential \
-
    jq \
-
    curl \
-
    git \
-
    wget \
-
    file \
-
    zstd \
-
    libxdo-dev \
-
    libssl-dev \
-
    libayatana-appindicator3-dev \
-
    librsvg2-dev \
-
    libwebkit2gtk-4.1-0=2.44.0-2 \
-
    libwebkit2gtk-4.1-dev=2.44.0-2 \
-
    libjavascriptcoregtk-4.1-0=2.44.0-2 \
-
    libjavascriptcoregtk-4.1-dev=2.44.0-2 \
-
    gir1.2-javascriptcoregtk-4.1=2.44.0-2 \
-
    gir1.2-webkit2-4.1=2.44.0-2
-

-
SHELL ["/bin/bash", "-c"]
deleted .woodpecker/e2e.yaml
@@ -1,58 +0,0 @@
-
when:
-
  - event: pull_request
-
  - event: push
-
    branch: main
-
  - event: manual
-

-
variables:
-
  - &plugin-sccache-read-only
-
    s3-bucket: "build-caches"
-
    s3-endpoint: "https://minio-api.radworks.garden"
-
    s3-key-prefix: "radicle-desktop"
-
    save-if: "false"
-
    s3-access-key:
-
      from_secret: minio_access_key
-
    s3-secret-access-key:
-
      from_secret: minio_secret_key
-

-
# test
-

-
steps:
-
  read_cache:
-
    image: quay.io/radicle_garden/plugin-sccache:latest
-
    volumes:
-
      - sccache:/sccache_data
-
    settings: *plugin-sccache-read-only
-

-
  update_cache:
-
    image: quay.io/radicle_garden/plugin-sccache:latest
-
    when:
-
      evaluate: 'CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == CI_REPO_DEFAULT_BRANCH && CI_COMMIT_MESSAGE startsWith "Release"'
-
    volumes:
-
      - sccache:/sccache_data
-
    settings:
-
      <<: *plugin-sccache-read-only
-
      save-if: true
-

-
  end-to-end:
-
    image: quay.io/radicle_garden/radicle-desktop-e2e:latest
-
    volumes:
-
      - sccache:/sccache_data
-
    entrypoint:
-
      - "/bin/bash"
-
      - "-c"
-
      - |
-
        set -euo pipefail
-

-
        export PATH="/sccache_data:$PATH"
-
        source .sccache
-

-
        ./scripts/install-binaries;
-
        npm ci
-
        npm run build:http
-
        mkdir -p tests/artifacts;
-

-
        # Install and run playwright
-
        npm run test:e2e -- --project webkit
-

-
        sccache --show-stats
deleted .woodpecker/lint.yaml
@@ -1,61 +0,0 @@
-
when:
-
  - event: pull_request
-
  - event: push
-
    branch: main
-
  - event: manual
-

-
variables:
-
  - &plugin-sccache-read-only
-
    s3-bucket: "build-caches"
-
    s3-endpoint: "https://minio-api.radworks.garden"
-
    s3-key-prefix: "radicle-desktop"
-
    save-if: "false"
-
    s3-access-key:
-
      from_secret: minio_access_key
-
    s3-secret-access-key:
-
      from_secret: minio_secret_key
-

-
steps:
-
  read_cache:
-
    image: quay.io/radicle_garden/plugin-sccache:latest
-
    volumes:
-
      - sccache:/sccache_data
-
    settings: *plugin-sccache-read-only
-

-
  update_cache:
-
    image: quay.io/radicle_garden/plugin-sccache:latest
-
    when:
-
      evaluate: 'CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == CI_REPO_DEFAULT_BRANCH && CI_COMMIT_MESSAGE startsWith "Release"'
-
    volumes:
-
      - sccache:/sccache_data
-
    settings:
-
      <<: *plugin-sccache-read-only
-
      save-if: true
-

-
  lint_typescript:
-
    image: docker.io/library/node:22.11.0
-
    entrypoint:
-
      - "/bin/bash"
-
      - "-c"
-
      - |
-
        set -euo pipefail
-

-
        npm ci
-
        npm run check-js
-

-
  lint_rust:
-
    image: quay.io/radicle_garden/radicle-desktop-base:latest
-
    volumes:
-
      - sccache:/sccache_data
-
    entrypoint:
-
      - "/bin/bash"
-
      - "-c"
-
      - |
-
        set -euo pipefail
-

-
        export PATH="/sccache_data:$PATH"
-
        source .sccache
-

-
        scripts/check-rs
-

-
        sccache --show-stats
deleted .woodpecker/unit-test.yaml
@@ -1,15 +0,0 @@
-
when:
-
  - event: pull_request
-
  - event: push
-
    branch: main
-
  - event: manual
-

-
steps:
-
  unit-tests:
-
    image: docker.io/library/node:22.11.0
-
    entrypoint:
-
      - "/bin/bash"
-
      - "-c"
-
      - |
-
        npm ci
-
        npm run test:unit
added Dockerfile.release
@@ -0,0 +1,87 @@
+
FROM --platform=linux/arm64 ubuntu:24.04
+

+
ENV DEBIAN_FRONTEND=noninteractive
+

+
# Add amd64 multiarch so we can install x86_64 sysroot libraries alongside
+
# the native ARM64 toolchain. This lets the Tauri CLI run natively (fast, no
+
# QEMU) while cross-compiling the Rust binary to x86_64.
+
# Configure apt: restrict existing ports.ubuntu.com sources to arm64, then add
+
# archive.ubuntu.com as the amd64 source (ports.ubuntu.com has no amd64 packages).
+
RUN dpkg --add-architecture amd64 && \
+
    sed -i '/^Types: deb/a Architectures: arm64' /etc/apt/sources.list.d/ubuntu.sources && \
+
    printf 'Types: deb\nURIs: http://archive.ubuntu.com/ubuntu\nSuites: noble noble-updates noble-backports noble-security\nComponents: main restricted universe multiverse\nArchitectures: amd64\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\n' \
+
        > /etc/apt/sources.list.d/archive-amd64.sources && \
+
    apt-get update
+

+
# Native ARM64 build essentials + cross-compilation toolchain
+
RUN apt-get install -y --no-install-recommends \
+
    build-essential \
+
    ca-certificates \
+
    crossbuild-essential-amd64 \
+
    curl \
+
    git \
+
    qemu-user-static \
+
    wget \
+
    file \
+
    jq \
+
    zstd
+

+
# Tauri system library headers for the amd64 target.
+
# The :amd64 suffix installs the x86_64 variant via multiarch.
+
#
+
# gobject-introspection-bin-linux:amd64 was split out as an arch-specific
+
# package in Ubuntu 24.04 but is not available for amd64 in the multiarch
+
# context on an ARM64 host. It is not needed for cross-compilation (it
+
# contains g-ir-scanner which is a build tool, not a library). We create
+
# empty dummy packages to satisfy apt's dependency resolver so the webkit
+
# and related libraries can be installed normally.
+
RUN for pkg in gobject-introspection-bin gobject-introspection-bin-linux; do \
+
      mkdir -p /tmp/fake/DEBIAN && \
+
      printf "Package: %s\nVersion: 1.80.1-1\nArchitecture: amd64\nMaintainer: fake\nDescription: fake\n" \
+
        "$pkg" > /tmp/fake/DEBIAN/control && \
+
      dpkg-deb --build /tmp/fake /tmp/${pkg}_1.80.1-1_amd64.deb && \
+
      rm -rf /tmp/fake; \
+
    done && \
+
    dpkg -i /tmp/gobject-introspection-bin_1.80.1-1_amd64.deb \
+
             /tmp/gobject-introspection-bin-linux_1.80.1-1_amd64.deb && \
+
    rm -f /tmp/gobject-introspection-bin*.deb
+

+
RUN apt-get install -y --no-install-recommends \
+
    libxdo-dev:amd64 \
+
    libssl-dev:amd64 \
+
    libayatana-appindicator3-dev:amd64 \
+
    librsvg2-dev:amd64 \
+
    libwebkit2gtk-4.1-0:amd64 \
+
    libjavascriptcoregtk-4.1-0:amd64 \
+
    libwebkit2gtk-4.1-dev:amd64 \
+
    libjavascriptcoregtk-4.1-dev:amd64
+

+
# Rust: native ARM64 toolchain so the Tauri CLI runs without emulation,
+
# plus the x86_64 target for the actual compiled output.
+
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
+
    sh -s -- -y --default-toolchain 1.84.1
+
ENV PATH="/root/.cargo/bin:${PATH}"
+
RUN rustup target add x86_64-unknown-linux-gnu
+

+
# Node.js 22 (ARM64, runs natively)
+
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
+
    apt-get install -y nodejs
+

+
# Cross-compilation environment for Cargo and cc-rs.
+
# crossbuild-essential-amd64 provides x86_64-linux-gnu-{gcc,g++,pkg-config}.
+
ENV CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc
+
ENV CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc
+
ENV CXX_x86_64_unknown_linux_gnu=x86_64-linux-gnu-g++
+
ENV AR_x86_64_unknown_linux_gnu=x86_64-linux-gnu-ar
+
# pkg-config must resolve against the amd64 sysroot, not the ARM64 one.
+
ENV PKG_CONFIG=x86_64-linux-gnu-pkg-config
+

+
# Default pkg-config search paths for tools that call `pkg-config` directly
+
# (e.g. linuxdeploy-plugin-gtk.sh). Already in x86_64-linux-gnu-pkg-config but
+
# not in the plain `pkg-config` binary that runs on the ARM64 host.
+
ENV PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig
+
# Ubuntu 24.04 installs gtk-query-immodules-3.0 under libgtk-3-0t64.
+
# binfmt_misc routes it through QEMU when invoked by the GTK linuxdeploy plugin.
+
ENV PATH="/usr/lib/x86_64-linux-gnu/libgtk-3-0t64:${PATH}"
+

+
SHELL ["/bin/bash", "-c"]
modified RELEASE.md
@@ -1,11 +1,11 @@
# Release process

-
**Note:** We release every second Thursday, before the end of the cycle.
-

-
- In your working shell set the following variable
+
- In your working shell set the following variables

  ```bash
  VERSION="X.Y.Z"
+
  RADICLE_DESKTOP_DIR="$(pwd)"                   # absolute path to this repo
+
  SIGNING_KEY="$HOME/work/apt-signing/rudolfs"   # adjust to your signing key path
  ```

- Create a new release branch
@@ -23,44 +23,6 @@

  ```bash
  RELEASE_SHA="$(git rev-parse HEAD)"
-
  SHORT_RELEASE_SHA="$(git rev-parse --short=8 HEAD)"
-
  ```
-

-
- Create a _Release Patch_ with `git push rad HEAD:refs/patches`
-
- Build the macOS app locally
-

-
  ```bash
-
  cargo clean
-
  rm -rf node_modules
-
  npm install
-
  npm exec -- tauri build --bundles dmg
-
  ```
-

-
  This creates a file `target/release/bundle/dmg/Radicle_X.Y.Z_aarch64.dmg`.
-

-
- Install the build macOS DMG and start the app to verify that it works.
-
- Wait for CI of the Release Commit in the Release Patch to pass
-

-
- Collect release artifacts
-

-
  ```bash
-
  rm -rf release-artifacts && mkdir release-artifacts
-
  curl -fL \
-
    "https://minio-api.radworks.garden/radworks-releases/radicle-desktop/pre-release/${VERSION}_${SHORT_RELEASE_SHA}/radicle-desktop_${VERSION}_amd64.AppImage" \
-
    --output release-artifacts/radicle-desktop-amd64.AppImage
-
  cp -a "target/release/bundle/dmg/Radicle_${VERSION}_aarch64.dmg" \
-
    release-artifacts/radicle-desktop-aarch64.dmg
-
  echo -n "{\"sha\": \"${RELEASE_SHA}\", \"version\": \"${VERSION}\"}" \
-
    > release-artifacts/latest.json
-
  ```
-

-
  The content of `release-artifacts` should look like this:
-

-
  ```plain
-
  release-artifacts
-
    latest.json
-
    radicle-desktop-aarch64.dmg
-
    radicle-desktop-amd64.AppImage
  ```

- Update Arch Linux package info
@@ -73,32 +35,71 @@
    cd arch && ./generate-srcinfo.sh
    ```

-
  - Commit and push the changes to `arch/radicle-desktop/PKGBUILD` and
-
    `arch/radicle-desktop/.SRCINFO`:
+
  - Commit the changes (from the repo root):

    ```bash
-
    git add radicle-desktop/.SRCINFO
-
    git add radicle-desktop/PKGBUILD
+
    git add arch/radicle-desktop/.SRCINFO
+
    git add arch/radicle-desktop/PKGBUILD
    git commit -m "Update arch package to v${VERSION}"
-
    git push rad
    ```

+
- Create a _Release Patch_ with `git push rad HEAD:refs/patches`
- Wait for CI of the Release Patch to pass
- Wait for approval of the Release Patch and merge it into `main`

+
- Build all release artifacts
+

+
  ```bash
+
  scripts/release
+
  ```
+

+
  This builds the macOS DMG, Linux amd64 deb and AppImage.
+
  Artifacts are written to `releases/v${VERSION}/`.
+

+
  To build a single artifact, pass the corresponding flag:
+

+
  ```bash
+
  scripts/release --only-dmg        # macOS aarch64 DMG
+
  scripts/release --only-deb        # Linux amd64 deb
+
  scripts/release --only-appimage   # Linux amd64 AppImage
+
  ```
+

+
  The macOS DMG is built natively. The Linux builds run in an ARM64
+
  Podman container (`Dockerfile.release`) that cross-compiles Rust to
+
  x86_64. The AppImage additionally requires x86_64 emulation via
+
  binfmt_misc + QEMU to run linuxdeploy inside the ARM64 container — see
+
  `scripts/appimage-build` for details.
+

+
  The Linux builds cache Rust artifacts in named Podman volumes across
+
  runs. To start fully from scratch:
+

+
  ```bash
+
  scripts/release --clean && scripts/release
+
  ```
+

+
  `--clean` wipes `target-release/`, `node_modules/`, all Linux build
+
  volumes, and the container image. To only rebuild the container image
+
  while keeping the cached volumes (e.g. after updating
+
  `Dockerfile.release`):
+

+
  ```bash
+
  scripts/release --rebuild-image
+
  ```
+

+
  After the build, install the macOS DMG and start the app to verify that it works.
+

- Sign the Debian package from `radicle-apt-repo`

  ```bash
  cd radicle-apt-repo
  rad sync && git fetch

-
  curl -fLO \
-
    "https://minio-api.radworks.garden/radworks-releases/radicle-desktop/pre-release/${VERSION}_${SHORT_RELEASE_SHA}/radicle-desktop_${VERSION}_amd64.deb"
-

  podman build -t apt-import scripts
-
  podman run --rm -v ~/work/apt-signing/rudolfs:/src/keys/signing-key -v $(pwd):/src/apt -v ./radicle-desktop_${VERSION}_amd64.deb:/src/tmp.deb apt-import
-

-
  rm -rf ./radicle-desktop_${VERSION}_amd64.deb
+
  podman run --rm \
+
    -v "$SIGNING_KEY:/src/keys/signing-key" \
+
    -v "$(pwd):/src/apt" \
+
    -v "$RADICLE_DESKTOP_DIR/releases/v${VERSION}/radicle-desktop_${VERSION}_amd64.deb:/src/tmp.deb" \
+
    apt-import

  git checkout -b release-v${VERSION}
  git add -A
@@ -113,15 +114,12 @@

- Publish the Debian package to the APT repository from `radicle-apt-repo`

-
    - Upload the signed deb packages to files.radicle.xyz
-
    ```bash
-
    scp -r -i "$(rad path)/keys/radicle" \
-
      dists "release@files.radicle.xyz:/mnt/radicle/files/apt/"
-
    scp -r -i "$(rad path)/keys/radicle" \
-
      pool "release@files.radicle.xyz:/mnt/radicle/files/apt/"
-
    ```
-

-
    TODO: can we make scp skip the files that are already uploaded?
+
  ```bash
+
  scp -r -i "$(rad path)/keys/radicle" \
+
    dists "release@files.radicle.xyz:/mnt/radicle/files/apt/"
+
  scp -r -i "$(rad path)/keys/radicle" \
+
    pool "release@files.radicle.xyz:/mnt/radicle/files/apt/"
+
  ```

- Publish release files from `radicle-desktop`

@@ -129,7 +127,7 @@
  cd radicle-desktop

  scp -i "$(rad path)/keys/radicle" \
-
    release-artifacts/* "release@files.radicle.xyz:/mnt/radicle/files/releases/radicle-desktop/latest/"
+
    releases/v${VERSION}/* "release@files.radicle.xyz:/mnt/radicle/files/releases/radicle-desktop/latest/"
  ```

- Publish the Arch package by pushing changes to the [Arch User Repository][1]
modified eslint.config.js
@@ -165,6 +165,7 @@ export default [
      "isolation/*",
      "node_modules/**/*",
      "target/*",
+
      "target-release/*",
      "crates/radicle-tauri/**/*",
      "crates/radicle-types/**/*",
      "eslint.config.js",
added scripts/appimage-build
@@ -0,0 +1,55 @@
+
#!/bin/bash
+
set -euo pipefail
+
# The build container (Dockerfile.ubuntu) runs as native ARM64, but Tauri's
+
# AppImage bundler downloads and runs linuxdeploy-x86_64.AppImage — an x86_64
+
# binary that bundles the app's shared library dependencies (GTK, WebKit, …)
+
# into the AppDir so the AppImage is self-contained on any x86_64 Linux system.
+
# Without intervention the ARM64 kernel cannot execute it and returns "Exec
+
# format error". binfmt_misc is a Linux kernel feature that maps file magic
+
# bytes to an interpreter; by registering a handler here, the kernel
+
# automatically prepends QEMU to any x86_64 ELF execution, making x86_64
+
# binaries work transparently inside the ARM64 container without any changes to
+
# the code that invokes them.
+
#
+
# Register an x86_64 binfmt_misc handler so the ARM64 kernel transparently
+
# routes x86_64 ELF executables through QEMU. This is needed because Tauri's
+
# AppImage bundler downloads and runs linuxdeploy-x86_64.AppImage.
+
#
+
# binfmt_misc registration format:
+
#   :name:type:offset:magic:mask:interpreter:flags
+
#
+
# The magic matches an x86_64 ELF executable header:
+
#   \x7fELF  — ELF magic
+
#   \x02     — 64-bit (EI_CLASS)
+
#   \x01     — little-endian (EI_DATA)
+
#   \x01     — version
+
#   \x00...  — OS/ABI + padding (bytes 7-15)
+
#   \x02\x00 — ET_EXEC (executable)
+
#   \x3e\x00 — x86-64 machine type
+
#
+
# The mask has \x00 for bytes 7-15 (the padding area). A zero mask bit means
+
# "don't check this byte". This is necessary because AppImages embed the
+
# two-byte marker 'AI' in bytes 8-9 of the ELF padding. The standard x86_64
+
# mask checks those bytes exactly (\xff), so it rejects AppImages. With the
+
# permissive mask both plain ELFs and AppImages are intercepted.
+
#
+
# echo (not printf): the \x.. sequences must reach the kernel binfmt_misc
+
# parser as literal text (e.g. the four characters \, x, 7, f), which the
+
# kernel then interprets as byte values. printf would expand them to raw bytes
+
# first, the parser would misread the entry, and the broken handler would
+
# intercept ARM64 binaries and route them through QEMU, breaking the container.
+
mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc 2>/dev/null || true
+
echo ':qemu-x86_64-appimage:M::\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x3e\x00:\xff\xff\xff\xff\xff\xfe\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xff\xff\xff:/usr/bin/qemu-x86_64-static:' \
+
  > /proc/sys/fs/binfmt_misc/register 2>/dev/null || true
+
export APPIMAGE_EXTRACT_AND_RUN=1
+
# linuxdeploy-plugin-gtk.sh calls pkg-config to find GTK 3; on an ARM64 host
+
# with amd64 multiarch the default search path misses the amd64 .pc files.
+
export PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig
+
# Ubuntu 24.04 installs gtk-query-immodules-3.0 under the t64 directory.
+
# The plugin needs it in PATH; binfmt-misc handles it as x86_64 via QEMU.
+
export PATH=/usr/lib/x86_64-linux-gnu/libgtk-3-0t64:$PATH
+
npm exec -- tauri build \
+
  --config '{"build":{"beforeBuildCommand":""}}' \
+
  --target x86_64-unknown-linux-gnu \
+
  --bundles appimage
+
cp target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage /output/
added scripts/release
@@ -0,0 +1,173 @@
+
#!/usr/bin/env bash
+
#
+
# Build release artifacts for all platforms.
+
#
+
# Usage: scripts/release [OPTIONS]
+
#
+
# Options:
+
#   --only-dmg        Build macOS aarch64 DMG only
+
#   --only-deb        Build Linux amd64 deb only
+
#   --only-appimage   Build Linux amd64 AppImage only
+
#   --rebuild-image   Force rebuild of podman container images
+
#   --clean           Wipe target-release/, node_modules/ and Linux build volumes
+
#   --help            Show this help
+

+
set -euo pipefail
+

+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+

+
# ---- Flags ----
+

+
BUILD_DMG=true
+
BUILD_DEB=true
+
BUILD_APPIMAGE=true
+
REBUILD_IMAGE=false
+
CLEAN=false
+

+
for arg in "$@"; do
+
  case "$arg" in
+
    --only-dmg)      BUILD_DMG=true;  BUILD_DEB=false; BUILD_APPIMAGE=false ;;
+
    --only-deb)      BUILD_DMG=false; BUILD_DEB=true;  BUILD_APPIMAGE=false ;;
+
    --only-appimage) BUILD_DMG=false; BUILD_DEB=false; BUILD_APPIMAGE=true  ;;
+
    --rebuild-image) REBUILD_IMAGE=true ;;
+
    --clean)         CLEAN=true; BUILD_DMG=false; BUILD_DEB=false; BUILD_APPIMAGE=false ;;
+
    --help|-h)
+
      head -20 "$0" | grep '^#' | sed 's/^# \?//'
+
      exit 0
+
      ;;
+
    *) echo "Unknown option: $arg" >&2; exit 1 ;;
+
  esac
+
done
+

+
# ---- Helpers ----
+

+
step() { echo; echo "==> $*"; }
+

+
# Builds a container image if it does not already exist (or --rebuild-image).
+
# $1: image name, $2: dockerfile path (relative to PROJECT_DIR), $3: platform
+
ensure_image() {
+
  local name="$1"
+
  local dockerfile="$2"
+
  local platform="$3"
+
  if [[ "$REBUILD_IMAGE" == true ]] || ! podman image exists "$name" 2>/dev/null; then
+
    echo "    Building container image: $name"
+
    podman build \
+
      --platform "$platform" \
+
      -t "$name" \
+
      -f "$PROJECT_DIR/$dockerfile" \
+
      "$PROJECT_DIR"
+
  fi
+
}
+

+
# ---- Version and output directory ----
+

+
VERSION="$(jq -r '.version' "$PROJECT_DIR/crates/radicle-tauri/tauri.conf.json")"
+
RELEASE_SHA="$(git -C "$PROJECT_DIR" rev-parse HEAD)"
+
SHORT_SHA="$(git -C "$PROJECT_DIR" rev-parse --short=8 HEAD)"
+
RELEASE_DIR="$PROJECT_DIR/releases/v$VERSION"
+

+
step "Building release v$VERSION (${SHORT_SHA})"
+
echo "    Output: $RELEASE_DIR"
+
mkdir -p "$RELEASE_DIR"
+

+
# ---- Clean ----
+

+
if [[ "$CLEAN" == true ]]; then
+
  step "Cleaning build artifacts"
+
  rm -rf "$PROJECT_DIR/target-release"
+
  rm -rf "$PROJECT_DIR/node_modules"
+
  podman volume rm radicle-desktop-linux-target radicle-desktop-linux-nm radicle-desktop-linux-cargo 2>/dev/null || true
+
  podman image rm radicle-desktop-release-builder 2>/dev/null || true
+
  echo "    Cleaned"
+
fi
+

+
# ---- macOS aarch64 DMG ----
+

+
if [[ "$BUILD_DMG" == true ]]; then
+
  step "Building macOS aarch64 DMG"
+
  cd "$PROJECT_DIR"
+
  npm install
+
  CARGO_TARGET_DIR="$PROJECT_DIR/target-release" \
+
    npm exec -- tauri build --bundles dmg
+
  cp "target-release/release/bundle/dmg/Radicle_${VERSION}_aarch64.dmg" \
+
    "$RELEASE_DIR/radicle-desktop-aarch64.dmg"
+
  echo "    radicle-desktop-aarch64.dmg"
+
fi
+

+
# ---- Linux amd64 deb ----
+
#
+
# Runs in the native ARM64 Ubuntu container, which cross-compiles Rust to
+
# x86_64. Build artifacts are cached in named podman volumes so subsequent
+
# runs only recompile what changed.
+
#
+
# When to clear volumes:
+
#   After a Rust toolchain bump or dependency change that confuses incremental
+
#   compilation: podman volume rm radicle-desktop-linux-target radicle-desktop-linux-cargo
+
#   After a major npm dependency change: podman volume rm radicle-desktop-linux-nm
+

+
if [[ "$BUILD_DEB" == true ]]; then
+
  step "Building Linux amd64 deb"
+
  ensure_image "radicle-desktop-release-builder" "Dockerfile.release" "linux/arm64"
+

+
  podman run --rm \
+
    -v "$PROJECT_DIR:/workspace:z" \
+
    -v radicle-desktop-linux-target:/workspace/target:z \
+
    -v radicle-desktop-linux-nm:/workspace/node_modules:z \
+
    -v radicle-desktop-linux-cargo:/root/.cargo/registry:z \
+
    -v "$RELEASE_DIR:/output:z" \
+
    -w /workspace \
+
    -e CI=true \
+
    radicle-desktop-release-builder \
+
    bash -c "
+
      set -euo pipefail
+
      npm ci
+
      npm exec -- tauri build --target x86_64-unknown-linux-gnu --bundles deb
+
      cp target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb /output/
+
    "
+

+
  echo "    radicle-desktop_${VERSION}_amd64.deb"
+
fi
+

+
# ---- Linux amd64 AppImage ----
+
#
+
# See scripts/appimage-build for the full setup. Key points:
+
# - --privileged to mount binfmt_misc for x86_64 emulation via QEMU
+
# - APPIMAGE_EXTRACT_AND_RUN=1 so linuxdeploy-x86_64.AppImage runs without FUSE
+
# - PKG_CONFIG_PATH for the GTK plugin to find amd64 packages
+
# - PATH includes libgtk-3-0t64 dir where gtk-query-immodules-3.0 lives
+
# The script is mounted from the workspace (heredocs pipe to host, not container)
+

+
if [[ "$BUILD_APPIMAGE" == true ]]; then
+
  step "Building Linux amd64 AppImage"
+
  ensure_image "radicle-desktop-release-builder" "Dockerfile.release" "linux/arm64"
+

+
  podman run --rm \
+
    --privileged \
+
    -v "$PROJECT_DIR:/workspace:z" \
+
    -v radicle-desktop-linux-target:/workspace/target:z \
+
    -v radicle-desktop-linux-nm:/workspace/node_modules:z \
+
    -v radicle-desktop-linux-cargo:/root/.cargo/registry:z \
+
    -v "$RELEASE_DIR:/output:z" \
+
    -w /workspace \
+
    -e CI=true \
+
    radicle-desktop-release-builder \
+
    bash /workspace/scripts/appimage-build
+

+
  echo "    radicle-desktop_${VERSION}_amd64.AppImage"
+
fi
+

+
if [[ "$CLEAN" == false ]]; then
+
  # ---- latest.json ----
+

+
  step "Generating latest.json"
+
  printf '{"sha": "%s", "version": "%s"}' "$RELEASE_SHA" "$VERSION" \
+
    > "$RELEASE_DIR/latest.json"
+
  echo "    latest.json"
+

+
  # ---- Summary ----
+

+
  echo
+
  echo "Done. Artifacts in $RELEASE_DIR:"
+
  ls -lh "$RELEASE_DIR"
+
fi
modified tests/support/heartwood-release
@@ -1 +1 @@
-
1.1.0-pre.4

\ No newline at end of file
+
1.1.0

\ No newline at end of file