Skip to content

v3.2.0 (#79)

v3.2.0 (#79) #17

Workflow file for this run

name: Release & Publish
on:
push:
tags:
- "v*"
env:
CICD_INTERMEDIATES_DIR: "_cicd-intermediates"
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: link.exe
jobs:
crate_metadata:
name: Extract crate metadata
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Extract crate information
id: crate_metadata
run: |
cargo metadata --no-deps --format-version 1 | jq -r '"name=" + .packages[0].name' | tee -a $GITHUB_OUTPUT
cargo metadata --no-deps --format-version 1 | jq -r '"version=" + .packages[0].version' | tee -a $GITHUB_OUTPUT
cargo metadata --no-deps --format-version 1 | jq -r '"maintainer=" + .packages[0].authors[0]' | tee -a $GITHUB_OUTPUT
cargo metadata --no-deps --format-version 1 | jq -r '"homepage=" + .packages[0].homepage' | tee -a $GITHUB_OUTPUT
outputs:
name: ${{ steps.crate_metadata.outputs.name }}
version: ${{ steps.crate_metadata.outputs.version }}
maintainer: ${{ steps.crate_metadata.outputs.maintainer }}
homepage: ${{ steps.crate_metadata.outputs.homepage }}
build:
name: ${{ matrix.job.target }} (${{ matrix.job.os }})
runs-on: ${{ matrix.job.os }}
needs: crate_metadata
strategy:
fail-fast: false
matrix:
job:
- target: aarch64-unknown-linux-musl
os: ubuntu-latest
dpkg_arch: arm64
use-cross: true
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
dpkg_arch: arm64
use-cross: true
# For now, skip 32-bit targets (`i686` and `arm`). ARM64 (e.g. Apple Silicon) is still built (above and below).
#
# - target: arm-unknown-linux-gnueabihf
# os: ubuntu-latest
# dpkg_arch: armhf
# use-cross: true
# - target: arm-unknown-linux-musleabihf
# os: ubuntu-latest
# dpkg_arch: musl-linux-armhf
# use-cross: true
# - target: i686-pc-windows-msvc
# os: windows-2025
# - target: i686-unknown-linux-gnu
# os: ubuntu-latest
# dpkg_arch: i686
# use-cross: true
# - target: i686-unknown-linux-musl
# os: ubuntu-latest
# dpkg_arch: musl-linux-i686
# use-cross: true
- target: x86_64-apple-darwin
os: macos-15-intel
- target: aarch64-apple-darwin
os: macos-15
- target: x86_64-pc-windows-msvc
os: windows-2025
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
dpkg_arch: amd64
use-cross: true
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
dpkg_arch: musl-linux-amd64
use-cross: true
env:
BUILD_CMD: cargo
permissions:
contents: write
id-token: write
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Fetch secrets from Infisical
if: startsWith(matrix.job.os, 'windows-') || startsWith(matrix.job.os, 'macos-')
uses: Infisical/secrets-action@v1.0.15
with:
method: oidc
identity-id: ${{ vars.INFISICAL_IDENTITY_ID }}
env-slug: prod
project-slug: ${{ vars.INFISICAL_PROJECT_SLUG }}
- name: Authenticate with GCP
if: startsWith(matrix.job.os, 'windows-')
uses: google-github-actions/auth@v3
with:
project_id: ${{ vars.GCP_PROJECT_ID }}
workload_identity_provider: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}
- name: Install prerequisites
shell: bash
run: |
# Install cross-compiler toolchains on for ARM.
case ${{ matrix.job.target }} in
arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;;
aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;;
esac
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly
components: rustc-codegen-cranelift-preview
targets: ${{ matrix.job.target }}
- name: Install cross
if: matrix.job.use-cross
uses: taiki-e/install-action@cross
- name: Overwrite build command env variable
if: matrix.job.use-cross
shell: bash
run: echo "BUILD_CMD=cross" >> $GITHUB_ENV
- name: Show version information (Rust, cargo, GCC)
shell: bash
run: |
gcc --version || true
rustup -V
rustup toolchain list
rustup default
cargo -V
rustc -V
- name: Import macOS code signing certificate
if: startsWith(matrix.job.os, 'macos-')
shell: bash
run: |
# Create variables
CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
echo -n "${{ env.APPLE_CODE_SIGNING_CERT }}" | base64 --decode -o $CERTIFICATE_PATH
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security import $CERTIFICATE_PATH -P '${{ env.APPLE_CODE_SIGNING_CERT_PASSWORD }}' -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
echo "APPLE_SIGNING_IDENTITY=$(security find-identity -v -p codesigning | grep "Piebald LLC" | awk -F '"' '{print $2}')" >> $GITHUB_ENV
# Write API key and set path in environment
API_KEY_PATH=$RUNNER_TEMP/apple-api-key
echo -n "${{ env.APP_STORE_CONNECT_API_KEY }}" | base64 --decode -o $API_KEY_PATH
echo "APPLE_API_KEY_PATH=$API_KEY_PATH" >> $GITHUB_ENV
- name: Build
shell: bash
run: $BUILD_CMD build --locked --release --target=${{ matrix.job.target }}
- name: Set binary name & path
id: bin
shell: bash
run: |
# Figure out suffix of binary
EXE_suffix=""
case ${{ matrix.job.target }} in
*-pc-windows-*) EXE_suffix=".exe" ;;
esac;
# Setup paths
BIN_NAME="${{ needs.crate_metadata.outputs.name }}${EXE_suffix}"
BIN_PATH="target/${{ matrix.job.target }}/release/${BIN_NAME}"
# Let subsequent steps know where to find the binary
echo "BIN_PATH=${BIN_PATH}" >> $GITHUB_OUTPUT
echo "BIN_NAME=${BIN_NAME}" >> $GITHUB_OUTPUT
- name: Codesign executable (macOS)
if: startsWith(matrix.job.os, 'macos-')
shell: bash
env:
APPLE_API_ISSUER: ${{ env.APP_STORE_CONNECT_API_KEY_ISSUER }}
APPLE_API_KEY: ${{ env.APP_STORE_CONNECT_API_KEY_ID }}
run: |
codesign --force --options runtime \
--sign "$APPLE_SIGNING_IDENTITY" \
--timestamp \
--identifier ai.piebald.splitrail \
"${{ steps.bin.outputs.BIN_PATH }}"
- name: Create notarization archive (macOS)
if: startsWith(matrix.job.os, 'macos-')
shell: bash
run: |
BIN_DIR=$(dirname "${{ steps.bin.outputs.BIN_PATH }}")
BIN_NAME="${{ steps.bin.outputs.BIN_NAME }}"
case "${{ matrix.job.os }}" in
macos-15-intel) PLATFORM="macos-x64";;
macos-15) PLATFORM="macos-arm64";;
*) echo "Unsupported macOS: ${{ matrix.job.os }}"; exit 1;;
esac
pushd "$BIN_DIR" >/dev/null
zip "${{ needs.crate_metadata.outputs.name }}-${PLATFORM}.zip" "$BIN_NAME"
popd >/dev/null
- name: Notarize archive (macOS)
if: startsWith(matrix.job.os, 'macos-')
shell: bash
env:
APPLE_API_ISSUER: ${{ env.APP_STORE_CONNECT_API_KEY_ISSUER }}
APPLE_API_KEY: ${{ env.APP_STORE_CONNECT_API_KEY_ID }}
run: |
BIN_DIR=$(dirname "${{ steps.bin.outputs.BIN_PATH }}")
case "${{ matrix.job.os }}" in
macos-15-intel) PLATFORM="macos-x64";;
macos-15) PLATFORM="macos-arm64";;
*) echo "Unsupported macOS: ${{ matrix.job.os }}"; exit 1;;
esac
ARCHIVE_PATH="$BIN_DIR/${{ needs.crate_metadata.outputs.name }}-${PLATFORM}.zip"
xcrun notarytool submit "$ARCHIVE_PATH" \
--key "$APPLE_API_KEY_PATH" \
--key-id "$APPLE_API_KEY" \
--issuer "$APPLE_API_ISSUER" \
--wait
- name: Cleanup notarization archive and certificate (macOS)
if: always() && startsWith(matrix.job.os, 'macos-')
shell: bash
run: |
BIN_DIR=$(dirname "${{ steps.bin.outputs.BIN_PATH }}")
case "${{ matrix.job.os }}" in
macos-15-intel) PLATFORM="macos-x64";;
macos-15) PLATFORM="macos-arm64";;
*) echo "Unsupported macOS: ${{ matrix.job.os }}"; exit 1;;
esac
rm -f "$BIN_DIR/${{ needs.crate_metadata.outputs.name }}-${PLATFORM}.zip"
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true
rm -f $RUNNER_TEMP/apple-api-key || true
- name: Decode Windows code signing certificate
if: startsWith(matrix.job.os, 'windows-')
run: |
$certBytes = [System.Convert]::FromBase64String("${{ env.WINDOWS_CODE_SIGNING_CERT }}")
[System.IO.File]::WriteAllBytes("piebald_llc.crt", $certBytes)
- name: Add SignTool to PATH
if: startsWith(matrix.job.os, 'windows-')
uses: kamaranl/add-signtool-action@v1
- name: Install Google Cloud KMS CNG provider
if: startsWith(matrix.job.os, 'windows-')
run: |
Invoke-WebRequest -Uri https://github.com/GoogleCloudPlatform/kms-integrations/releases/download/cng-v1.3/kmscng-1.3-windows-amd64.zip -OutFile kmscng.zip
Expand-Archive -Path kmscng.zip -DestinationPath .
$proc = Start-Process "msiexec" "/i kmscng-1.3-windows-amd64\kmscng.msi /qn" -NoNewWindow -PassThru -Wait
Remove-Item -Path kmscng.zip,kmscng-1.3-windows-amd64 -Recurse -Force
exit $proc.ExitCode
- name: Sign Windows executable
if: startsWith(matrix.job.os, 'windows-')
run: |
signtool sign `
/v /debug /fd sha256 `
/t http://timestamp.digicert.com `
/f piebald_llc.crt `
/csp "Google Cloud KMS Provider" `
/kc ${{ vars.GCP_KMS_KEY }} `
"${{ steps.bin.outputs.BIN_PATH }}"
- name: Create tarball
id: package
shell: bash
run: |
PKG_suffix=".tar.gz" ; case ${{ matrix.job.target }} in *-pc-windows-*) PKG_suffix=".zip" ;; esac;
PKG_BASENAME=${{ needs.crate_metadata.outputs.name }}-v${{ needs.crate_metadata.outputs.version }}-${{ matrix.job.target }}
PKG_NAME=${PKG_BASENAME}${PKG_suffix}
echo "PKG_NAME=${PKG_NAME}" >> $GITHUB_OUTPUT
PKG_STAGING="${{ env.CICD_INTERMEDIATES_DIR }}/package"
ARCHIVE_DIR="${PKG_STAGING}/${PKG_BASENAME}/"
mkdir -p "${ARCHIVE_DIR}"
# Binary
cp "${{ steps.bin.outputs.BIN_PATH }}" "$ARCHIVE_DIR"
# README and LICENSE
cp "README.md" "LICENSE" "$ARCHIVE_DIR"
# base compressed package
pushd "${PKG_STAGING}/" >/dev/null
case ${{ matrix.job.target }} in
*-pc-windows-*) 7z -y a "${PKG_NAME}" "${PKG_BASENAME}"/* | tail -2 ;;
*) tar czf "${PKG_NAME}" "${PKG_BASENAME}"/* ;;
esac;
popd >/dev/null
# Let subsequent steps know where to find the compressed package
echo "PKG_PATH=${PKG_STAGING}/${PKG_NAME}" >> $GITHUB_OUTPUT
- name: Create Debian package
id: debian-package
shell: bash
if: startsWith(matrix.job.os, 'ubuntu')
run: |
DPKG_STAGING="${{ env.CICD_INTERMEDIATES_DIR }}/debian-package"
DPKG_DIR="${DPKG_STAGING}/dpkg"
mkdir -p "${DPKG_DIR}"
DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }}
DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }}-musl
case ${{ matrix.job.target }} in *-musl) DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }}-musl ; DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }} ;; esac;
DPKG_VERSION=${{ needs.crate_metadata.outputs.version }}
DPKG_ARCH="${{ matrix.job.dpkg_arch }}"
DPKG_NAME="${DPKG_BASENAME}_${DPKG_VERSION}_${DPKG_ARCH}.deb"
echo "DPKG_NAME=${DPKG_NAME}" >> $GITHUB_OUTPUT
# Binary
install -Dm755 "${{ steps.bin.outputs.BIN_PATH }}" "${DPKG_DIR}/usr/bin/${{ steps.bin.outputs.BIN_NAME }}"
# README and LICENSE
install -Dm644 "README.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/README.md"
install -Dm644 "LICENSE" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/LICENSE"
cat > "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/copyright" <<EOF
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: ${{ needs.crate_metadata.outputs.name }}
Source: ${{ needs.crate_metadata.outputs.homepage }}
Files: *
Copyright: ${{ needs.crate_metadata.outputs.maintainer }}
Copyright: 2025 ${{ needs.crate_metadata.outputs.maintainer }}
License: MIT
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
.
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
EOF
chmod 644 "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/copyright"
# control file
mkdir -p "${DPKG_DIR}/DEBIAN"
cat > "${DPKG_DIR}/DEBIAN/control" <<EOF
Package: ${DPKG_BASENAME}
Version: ${DPKG_VERSION}
Section: utils
Priority: optional
Maintainer: ${{ needs.crate_metadata.outputs.maintainer }}
Homepage: ${{ needs.crate_metadata.outputs.homepage }}
Architecture: ${DPKG_ARCH}
Provides: ${{ needs.crate_metadata.outputs.name }}
Conflicts: ${DPKG_CONFLICTS}
Description: Blazing fast, single-executable, cross-platform, agentic development monitor. Supports Claude Code / Codex CLI / Gemini CLI / Qwen Code / Cline / Roo Code / Kilo Code / GitHub Copilot / OpenCode / Pi Agent, and integrates with Splitrail Cloud.
EOF
DPKG_PATH="${DPKG_STAGING}/${DPKG_NAME}"
echo "DPKG_PATH=${DPKG_PATH}" >> $GITHUB_OUTPUT
# build dpkg
fakeroot dpkg-deb --build "${DPKG_DIR}" "${DPKG_PATH}"
- name: "Artifact upload: tarball"
uses: actions/upload-artifact@v4
with:
name: ${{ steps.package.outputs.PKG_NAME }}
path: ${{ steps.package.outputs.PKG_PATH }}
- name: "Artifact upload: Debian package"
uses: actions/upload-artifact@v4
if: steps.debian-package.outputs.DPKG_NAME
with:
name: ${{ steps.debian-package.outputs.DPKG_NAME }}
path: ${{ steps.debian-package.outputs.DPKG_PATH }}
publish_release:
name: Publish release
runs-on: ubuntu-latest
needs:
- build
permissions:
contents: write
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Publish archives and packages
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
make_latest: "true"
files: |
artifacts/**
# TODO: Enable after initial release.
# winget:
# name: Publish to Winget
# runs-on: ubuntu-latest
# needs: build
# if: startsWith(github.ref, 'refs/tags/v')
# steps:
# - uses: vedantmgoyal9/winget-releaser@main
# with:
# identifier: piebald.splitrail
# installers-regex: '-pc-windows-msvc\.zip$'
# token: ${{ secrets.WINGET_TOKEN }}