From 09cf66d0070c1545cb88de7859b4c0d319f2b4d7 Mon Sep 17 00:00:00 2001 From: JetSquirrel Date: Thu, 24 Sep 2026 22:22:58 +0800 Subject: [PATCH] Release for Linux and Windows too, and build both on every pull request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release gains two jobs beside the macOS one: a Linux tarball built on Ubuntu 22.04 (for a glibc old enough for most distributions) with the system libraries gpui-kit's own CI installs, and a Windows zip of ducklocal.exe — unsigned, there being no Windows certificate. The release job waits for all three and publishes all three; a platform that fails fails the release. The locating step now finds each package and checks it is the package, not the artifact archive wrapping it: for the zip, that it holds ducklocal.exe. Neither platform had ever been compiled, so CI builds both on every pull request, before a tag can find out. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/ci.yml | 33 +++++++ .github/workflows/release.yml | 158 ++++++++++++++++++++++++++-------- 2 files changed, 157 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb37265..aa55f4a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,3 +28,36 @@ jobs: - name: Run tests run: cargo test --locked + + build: + # The release ships Linux and Windows builds too; nothing else compiles + # them, so a change that breaks either shows up here, on the pull request, + # rather than on a release tag. Build only: the tests run on macOS above. + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-22.04 + - os: windows-latest + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo artifacts + uses: Swatinem/rust-cache@v2 + + # The packages gpui-kit's own CI installs for its Linux build. + - name: Install system libraries + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + gcc g++ clang libfontconfig-dev libwayland-dev \ + libxkbcommon-x11-dev libx11-xcb-dev libssl-dev libzstd-dev \ + libvulkan1 + + - name: Build + run: cargo build --locked diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c11c401..caa3850 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,10 +14,9 @@ env: CARGO_TERM_COLOR: always jobs: - build: - # The app is macOS-only: bundle.sh ships a .app and package-macos.sh - # turns it into a signed, notarized dmg. There is no Windows or Linux - # artifact to build yet. + build-macos: + # bundle.sh ships a .app and package-macos.sh turns it into a signed, + # notarized dmg: the one platform with an installer and a signature. runs-on: macos-14 steps: @@ -88,10 +87,87 @@ jobs: name: ducklocal-macos-arm64.dmg path: ducklocal-macos-arm64.dmg + build-linux: + # A tarball of the binary. Ubuntu 22.04 rather than latest, so the glibc + # it links against is old enough for most current distributions. The + # packages are the ones gpui-kit's own CI installs for its Linux build. + runs-on: ubuntu-22.04 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache Cargo dependencies + uses: Swatinem/rust-cache@v2 + with: + key: x86_64-unknown-linux-gnu + + - name: Install system libraries + run: | + sudo apt-get update + sudo apt-get install -y \ + gcc g++ clang libfontconfig-dev libwayland-dev \ + libxkbcommon-x11-dev libx11-xcb-dev libssl-dev libzstd-dev \ + libvulkan1 + + - name: Build release binary + run: cargo build --release --locked + + - name: Package + run: | + set -euo pipefail + DIR=ducklocal-linux-x86_64 + mkdir "$DIR" + cp target/release/ducklocal LICENSE README.md "$DIR"/ + tar -czf "$DIR.tar.gz" "$DIR" + + - name: Upload artifact + uses: actions/upload-artifact@v7 + with: + name: ducklocal-linux-x86_64.tar.gz + path: ducklocal-linux-x86_64.tar.gz + + build-windows: + # A zip of the executable. It is not code-signed — there is no Windows + # certificate — so SmartScreen warns on first run. + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache Cargo dependencies + uses: Swatinem/rust-cache@v2 + with: + key: x86_64-pc-windows-msvc + + - name: Build release binary + run: cargo build --release --locked + + - name: Package + shell: pwsh + run: | + $dir = "ducklocal-windows-x86_64" + New-Item -ItemType Directory $dir | Out-Null + Copy-Item target/release/ducklocal.exe, LICENSE, README.md $dir + Compress-Archive -Path $dir -DestinationPath "$dir.zip" + + - name: Upload artifact + uses: actions/upload-artifact@v7 + with: + name: ducklocal-windows-x86_64.zip + path: ducklocal-windows-x86_64.zip + release: - needs: build - # No `if: always()`: a macOS job that failed to sign or notarize must - # fail the whole release, not ship a half-empty one. + needs: [build-macos, build-linux, build-windows] + # No `if: always()`: a platform that failed to build — or, for macOS, to + # sign or notarize — fails the whole release, not ship a partial one. runs-on: ubuntu-latest permissions: contents: write @@ -100,39 +176,53 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Download artifact + - name: Download artifacts uses: actions/download-artifact@v8 with: - name: ducklocal-macos-arm64.dmg path: artifacts - - name: Locate the dmg - id: dmg + - name: Locate the packages + id: packages run: | # download-artifact@v8 decides at download time whether to unpack - # the artifact, and its "download all" mode adds a per-artifact - # subdirectory on top of that — so the dmg can end up either at - # artifacts/ or artifacts//. Find it instead of - # assuming, and print what is there when it is missing. + # each artifact, and its "download all" mode adds a per-artifact + # subdirectory on top of that — so a package can end up at + # artifacts/ or artifacts//. Find each one instead + # of assuming, and check it is the package rather than the artifact + # archive that wraps it: publishing that would ship a zip with the + # wrong contents under the right name. set -euo pipefail - FOUND=$(find artifacts -type f -name 'ducklocal-macos-arm64.dmg' | head -1) - if [ -z "$FOUND" ]; then - echo "::error::no dmg under artifacts/" - find artifacts -type f | head -20 - exit 1 - fi - - # A zip header here means the download skipped decompression and - # named the archive after the artifact: publishing that would ship - # a 31 MB zip with a .dmg extension. - if [ "$(head -c 2 "$FOUND" | tr -d '\0')" = "PK" ]; then - echo "::error::$FOUND is a zip archive, not a dmg" - exit 1 - fi - - echo "found $FOUND" - file "$FOUND" || true - echo "path=$FOUND" >> "$GITHUB_OUTPUT" + files="" + for name in ducklocal-macos-arm64.dmg ducklocal-linux-x86_64.tar.gz ducklocal-windows-x86_64.zip; do + found=$(find artifacts -type f -name "$name" | head -1) + if [ -z "$found" ]; then + echo "::error::no $name under artifacts/" + find artifacts -type f | head -20 + exit 1 + fi + case "$name" in + *.zip) + # A real package holds the executable; a wrapping artifact + # archive holds the package. + unzip -l "$found" | grep -q 'ducklocal.exe' \ + || { echo "::error::$found does not contain ducklocal.exe"; exit 1; } + ;; + *) + if [ "$(head -c 2 "$found" | LC_ALL=C tr -d '\0')" = "PK" ]; then + echo "::error::$found is a zip archive, not the package" + exit 1 + fi + ;; + esac + echo "found $found" + file "$found" || true + files="$files$found"$'\n' + done + { + echo "files<> "$GITHUB_OUTPUT" - name: Determine version id: version @@ -152,6 +242,6 @@ jobs: prerelease: ${{ contains(steps.version.outputs.version, '-') }} generate_release_notes: true fail_on_unmatched_files: true - files: ${{ steps.dmg.outputs.path }} + files: ${{ steps.packages.outputs.files }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}