name: Release # Build the agent for every supported target on a version tag, leave each build # as a run artifact, and then — only after every target has built — attach the # archives to the release. Linux is a fully static musl binary (built with # cargo-zigbuild) so one artifact runs on any distro and can be repackaged # however a distro prefers. # # Targeted at Gitea Actions: artifacts use @v3 (the v4 backend is not available # on Gitea/GHES), and the release is filled through Gitea's own API with curl # rather than `gh`, which speaks only to github.com. on: push: tags: ["v*"] permissions: contents: write env: CARGO_TERM_COLOR: always CARGO_NET_RETRY: "10" jobs: build: name: ${{ matrix.asset }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - os: ubuntu-latest target: x86_64-unknown-linux-musl asset: tsunagi-linux-x86_64 archive: tar.gz zig: true deb: true deb_arch: amd64 pkg_arch: x86_64 gnu_target: x86_64-unknown-linux-gnu - os: ubuntu-latest target: aarch64-unknown-linux-musl asset: tsunagi-linux-aarch64 archive: tar.gz zig: true deb: true deb_arch: arm64 pkg_arch: aarch64 # 32-bit ARM with hardware float (ARMv7, `v7l`): single-board and edge # devices. Statically linked like the others. - os: ubuntu-latest target: armv7-unknown-linux-musleabihf asset: tsunagi-linux-armv7 archive: tar.gz zig: true deb: true deb_arch: armhf pkg_arch: armv7h - os: macos-latest target: aarch64-apple-darwin asset: tsunagi-macos-aarch64 archive: tar.gz zig: false - os: windows-latest target: x86_64-pc-windows-msvc asset: tsunagi-windows-x86_64 archive: zip zig: false steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - uses: Swatinem/rust-cache@v2 with: key: ${{ matrix.target }} # aws-lc-sys (iroh's rustls backend) and the bundled sqlite are C, and the # musl targets have no prebuilt bindings, so bindgen needs libclang and the # C build needs cmake. zig is the cross C compiler cargo-zigbuild drives. - name: Install musl build and packaging tooling if: matrix.zig run: sudo apt-get update && sudo apt-get install -y --no-install-recommends cmake clang libclang-dev libarchive-tools zstd - name: Install zig if: matrix.zig uses: mlugg/setup-zig@v2 with: # Pin a real, archived Zig release. Left unpinned the action resolves # to the newest version it knows of (a yet-unreleased 0.16.0-dev), # whose tarball 404s on ziglang.org while the community mirrors flake. version: 0.14.1 - name: Install cargo-zigbuild if: matrix.zig uses: taiki-e/install-action@v2 with: tool: cargo-zigbuild - name: Build shell: bash run: | set -euo pipefail if [[ "${{ matrix.zig }}" == "true" ]]; then # zig's linker ignores the `-O1` GNU-ld optimization flag Rust passes # and says so on stderr, which rustc now surfaces as a linker_messages # warning. It is cosmetic — the binary is correct — so quiet just that # one lint to keep the musl build log clean. A real link error still # fails the build. export RUSTFLAGS="${RUSTFLAGS:-} -A linker_messages" cargo zigbuild --release --locked --target "${{ matrix.target }}" --bin tsng else cargo build --release --locked --target "${{ matrix.target }}" --bin tsng fi # The tray GUI. On Linux it is built against glibc (an old baseline for # portability), not musl: a GUI must dlopen libGL/X11/Wayland at runtime, # which a static musl binary cannot. macOS and Windows build it natively. - name: Build the tray GUI shell: bash run: | set -euo pipefail if [[ "${{ runner.os }}" == "Linux" ]]; then if [[ -n "${{ matrix.gnu_target }}" ]]; then # tray-icon links GTK and libappindicator through pkg-config, which # has no cross story under zig, so the Linux GUI is built natively # (x86_64 only) against the runner's glibc. sudo apt-get update sudo apt-get install -y --no-install-recommends \ libgtk-3-dev libayatana-appindicator3-dev libxdo-dev cargo build --release --locked \ --target "${{ matrix.gnu_target }}" --bin tsunagi-tray else echo "no GUI build for ${{ matrix.target }}" fi else cargo build --release --locked --target "${{ matrix.target }}" --bin tsunagi-tray fi - name: Package shell: bash run: | set -euo pipefail version="${GITHUB_REF_NAME#v}" stage="dist/pkg/${{ matrix.asset }}" mkdir -p "$stage" bin="tsng" [[ "${{ runner.os }}" == "Windows" ]] && bin="tsng.exe" cp "target/${{ matrix.target }}/release/${bin}" "$stage/" # The tray GUI alongside it, where it was built (Linux: x86_64 only). tray="tsunagi-tray" [[ "${{ runner.os }}" == "Windows" ]] && tray="tsunagi-tray.exe" if [[ "${{ runner.os }}" == "Linux" ]]; then if [[ -n "${{ matrix.gnu_target }}" ]]; then cp "target/${{ matrix.gnu_target }}/release/${tray}" "$stage/" fi else cp "target/${{ matrix.target }}/release/${tray}" "$stage/" fi # The top-level README, plus the per-OS install notes and integration # files, so the archive carries what the platform needs to set up. cp README.md "$stage/" case "${{ runner.os }}" in Linux) cp dist/linux/tsunagi.service dist/linux/install.sh \ dist/linux/50-tsunagi-resolved.rules "$stage/" cp dist/linux/README.md "$stage/INSTALL.md" ;; macOS) cp dist/macos/cy.hexor.tsunagi.plist "$stage/" cp dist/macos/README.md "$stage/INSTALL.md" # The tray as an application bundle, for the Homebrew cask. bash dist/macos/build-app.sh \ "target/${{ matrix.target }}/release/tsunagi-tray" \ "$version" "$stage/Tsunagi.app" ;; Windows) cp dist/windows/README.md "$stage/INSTALL.md" ;; esac archive="${{ matrix.asset }}-${version}.${{ matrix.archive }}" if [[ "${{ matrix.archive }}" == "zip" ]]; then (cd dist/pkg && 7z a "../../${archive}" "${{ matrix.asset }}") else tar -C dist/pkg -czf "${archive}" "${{ matrix.asset }}" fi # The Windows installer: the agent as a service, Wintun, the tray and the # PATH entry in one setup program. Wintun is the signed library from # wintun.net, pinned by hash so a changed download fails the build. - name: Build the Windows installer if: runner.os == 'Windows' shell: pwsh run: | $ErrorActionPreference = 'Stop' $version = $env:GITHUB_REF_NAME.TrimStart('v') Invoke-WebRequest -Uri https://www.wintun.net/builds/wintun-0.14.1.zip -OutFile wintun.zip $hash = (Get-FileHash wintun.zip -Algorithm SHA256).Hash if ($hash -ne '07C256185D6EE3652E09FA55C0B673E2624B565E02C4B9091C79CA7D2F24EF51') { throw "wintun.zip has an unexpected hash: $hash" } Expand-Archive wintun.zip -DestinationPath wintun-dl choco install innosetup -y --no-progress $iscc = Join-Path ${env:ProgramFiles(x86)} 'Inno Setup 6\ISCC.exe' $stage = Join-Path $PWD 'dist/pkg/${{ matrix.asset }}' & $iscc "/DAppVersion=$version" "/DSourceDir=$stage" ` "/DWintunDir=$(Join-Path $PWD 'wintun-dl/wintun/bin/amd64')" ` "/O$PWD" dist/windows/tsunagi.iss if ($LASTEXITCODE -ne 0) { throw "ISCC failed with $LASTEXITCODE" } # A dependency-free .deb for the x86_64 and aarch64 Linux builds: it drops # the static binary, the systemd service and the polkit rule. Built with # dpkg-deb (present on the runner); see dist/deb/build-deb.sh. - name: Build the Debian package if: matrix.deb shell: bash run: | set -euo pipefail version="${GITHUB_REF_NAME#v}" bash dist/deb/build-deb.sh \ "target/${{ matrix.target }}/release/tsng" \ "${{ matrix.deb_arch }}" \ "$version" \ "tsunagi_${version}_${{ matrix.deb_arch }}.deb" # A dependency-free Arch package (.pkg.tar.zst), assembled with bsdtar and # zstd (no makepkg); same binary, service and polkit rule. See # dist/arch/build-pkg.sh. - name: Build the Arch package if: matrix.pkg_arch shell: bash run: | set -euo pipefail version="${GITHUB_REF_NAME#v}" pkgver="${version//-/_}" bash dist/arch/build-pkg.sh \ "target/${{ matrix.target }}/release/tsng" \ "${{ matrix.pkg_arch }}" \ "$version" \ "tsunagi-${pkgver}-1-${{ matrix.pkg_arch }}.pkg.tar.zst" # The GUI variants: the same agent plus the tray binary and a desktop # launcher, named tsunagi-gui (conflicting with the headless packages). - name: Build the GUI packages if: matrix.gnu_target shell: bash run: | set -euo pipefail version="${GITHUB_REF_NAME#v}" pkgver="${version//-/_}" tray="target/${{ matrix.gnu_target }}/release/tsunagi-tray" bash dist/deb/build-deb.sh \ "target/${{ matrix.target }}/release/tsng" \ "${{ matrix.deb_arch }}" "$version" \ "tsunagi-gui_${version}_${{ matrix.deb_arch }}.deb" "$tray" bash dist/arch/build-pkg.sh \ "target/${{ matrix.target }}/release/tsng" \ "${{ matrix.pkg_arch }}" "$version" \ "tsunagi-gui-${pkgver}-1-${{ matrix.pkg_arch }}.pkg.tar.zst" "$tray" # @v3 on purpose: Gitea does not support the v4 artifact backend. This is # what makes the archive (and the .deb, where built) show up under the run # and hands it to `publish`. - name: Upload the build outputs as run artifacts uses: actions/upload-artifact@v3 with: name: ${{ matrix.asset }} path: | ${{ matrix.asset }}-*.* *.deb *.pkg.tar.zst tsunagi-setup-*.exe if-no-files-found: error publish: name: Publish release needs: build runs-on: ubuntu-latest steps: - name: Download every build artifact uses: actions/download-artifact@v3 with: path: artifacts - name: Show what was collected run: find artifacts -type f - name: Create the release and upload the archives shell: bash env: # Gitea injects this with access to the current repository. If the # release calls below return 403, the repo/runner is configured to # hand Actions a read-only token — grant it write access to releases. TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -u tag="${GITHUB_REF_NAME}" # Gitea sets GITHUB_API_URL to its own API root (…/api/v1); fall back # to building it from the server URL if it is absent. base="${GITHUB_API_URL:-${GITHUB_SERVER_URL}/api/v1}" api="${base}/repos/${GITHUB_REPOSITORY}" auth="Authorization: token ${TOKEN}" echo "release API: $api" prerelease=false case "$tag" in *-*) prerelease=true ;; esac # The release id for this tag, if it already exists. code=$(curl -sS -o resp.json -w '%{http_code}' -H "$auth" "$api/releases/tags/$tag" || true) echo "get release by tag: HTTP $code" id=$(grep -o '"id":[0-9]*' resp.json 2>/dev/null | head -n1 | cut -d: -f2 || true) if [ -z "$id" ]; then code=$(curl -sS -o resp.json -w '%{http_code}' -X POST -H "$auth" \ -H 'Content-Type: application/json' \ -d "{\"tag_name\":\"$tag\",\"name\":\"tsunagi $tag\",\"prerelease\":$prerelease}" \ "$api/releases" || true) echo "create release: HTTP $code" id=$(grep -o '"id":[0-9]*' resp.json 2>/dev/null | head -n1 | cut -d: -f2 || true) if [ -z "$id" ]; then echo "could not create the release; response was:" >&2 cat resp.json >&2 || true exit 1 fi fi echo "release id: $id" # download-artifact@v3 nests each artifact in a directory named after # its upload, with the single archive inside. The read loop runs in # this shell (process substitution, not a pipe) so a failed upload # actually fails the job. rc=0 while IFS= read -r -d '' f; do name=$(basename "$f") code=$(curl -sS -o up.json -w '%{http_code}' -X POST -H "$auth" \ -F "attachment=@${f}" \ "$api/releases/$id/assets?name=${name}" || true) echo "upload $name: HTTP $code" if [ "$code" -lt 200 ] || [ "$code" -ge 300 ]; then cat up.json >&2 || true rc=1 fi done < <(find artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.deb' -o -name '*.pkg.tar.zst' -o -name 'tsunagi-setup-*.exe' \) -print0) exit "$rc"