Skip to content

Bump version to 0.2.7 #20

Bump version to 0.2.7

Bump version to 0.2.7 #20

Workflow file for this run

name: Release
on:
push:
branches: [main]
tags:
- "release-[0-9]+.[0-9]+.[0-9]+*"
permissions:
contents: write
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.decide.outputs.version }}
tag: ${{ steps.decide.outputs.tag }}
should_release: ${{ steps.decide.outputs.should_release }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Determine version and tag
id: decide
shell: bash
run: |
set -euo pipefail
extract_toml_version() {
# Read the first `version = "X.Y.Z"` line from the [package] table.
awk -F'"' '/^version = /{print $2; exit}' "$1"
}
DESKTOP_VER=$(extract_toml_version src-tauri/Cargo.toml)
CLI_VER=$(extract_toml_version crates/ralph-cli/Cargo.toml)
CONF_VER=$(python3 -c 'import json; print(json.load(open("src-tauri/tauri.conf.json"))["version"])')
PKG_VER=$(python3 -c 'import json; print(json.load(open("package.json"))["version"])')
# src-tauri/Cargo.toml is canonical. Every other version string must
# agree, otherwise the built artifacts would misrepresent themselves
# and the updater's latest.json would disagree with the installed
# binary's reported version.
fail=0
for pair in "src-tauri/Cargo.toml=$DESKTOP_VER" \
"crates/ralph-cli/Cargo.toml=$CLI_VER" \
"src-tauri/tauri.conf.json=$CONF_VER" \
"package.json=$PKG_VER"; do
file="${pair%%=*}"
ver="${pair#*=}"
if [[ "$ver" != "$DESKTOP_VER" ]]; then
echo "::error file=$file::version mismatch: $file is '$ver', expected '$DESKTOP_VER' (from src-tauri/Cargo.toml)"
fail=1
fi
done
if [[ "$fail" -eq 1 ]]; then
exit 1
fi
VERSION="$DESKTOP_VER"
TAG="release-${VERSION}"
if [[ "$GITHUB_REF" == refs/tags/release-* ]]; then
# Direct tag push — build + release unconditionally.
SHOULD_RELEASE=true
else
# Push to main — only release if this version hasn't been tagged.
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
echo "Tag $TAG already exists — version unchanged, skipping release"
SHOULD_RELEASE=false
else
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "$TAG" -m "Release $VERSION"
git push origin "$TAG"
SHOULD_RELEASE=true
fi
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "should_release=$SHOULD_RELEASE" >> "$GITHUB_OUTPUT"
build-cli:
needs: prepare
if: needs.prepare.outputs.should_release == 'true'
strategy:
matrix:
include:
- target: aarch64-apple-darwin
os: macos-latest
artifact: ralph-macos-arm64
- target: x86_64-apple-darwin
os: macos-latest
artifact: ralph-macos-x86_64
- target: x86_64-pc-windows-msvc
os: windows-latest
artifact: ralph-windows-x86_64
- target: aarch64-pc-windows-msvc
os: windows-latest
artifact: ralph-windows-arm64
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
artifact: ralph-linux-x86_64
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
artifact: ralph-linux-arm64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install cross-compilation tools (Linux ARM64)
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
- name: Build
run: cargo build --release --target ${{ matrix.target }} -p ralph-cli
- name: Prepare artifact (Unix)
if: runner.os != 'Windows'
run: |
cp target/${{ matrix.target }}/release/ralph ${{ matrix.artifact }}
chmod +x ${{ matrix.artifact }}
- name: Prepare artifact (Windows)
if: runner.os == 'Windows'
run: cp target/${{ matrix.target }}/release/ralph.exe ${{ matrix.artifact }}.exe
- name: Sign and notarize (macOS)
if: runner.os == 'macOS'
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }}
ARTIFACT: ${{ matrix.artifact }}
run: |
set -euo pipefail
# Create a temporary keychain and import the signing certificate
KEYCHAIN_PATH="$RUNNER_TEMP/signing.keychain-db"
KEYCHAIN_PASSWORD=$(uuidgen)
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
CERT_PATH="$RUNNER_TEMP/cert.p12"
echo "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_PATH"
security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')
# Sign the binary with hardened runtime and a secure timestamp
codesign --force --options runtime --timestamp \
--sign "$APPLE_SIGNING_IDENTITY" "$ARTIFACT"
codesign --verify --verbose "$ARTIFACT"
# Notarize: zip the binary and submit to the notary service.
# Bare binaries can't be stapled, so Gatekeeper does an online check
# on first run instead.
export APPLE_API_KEY_PATH="$RUNNER_TEMP/AuthKey.p8"
echo "$APPLE_API_KEY_BASE64" | base64 --decode > "$APPLE_API_KEY_PATH"
ZIP_PATH="$RUNNER_TEMP/$ARTIFACT.zip"
/usr/bin/ditto -c -k --keepParent "$ARTIFACT" "$ZIP_PATH"
./scripts/notarize.sh "$ZIP_PATH"
# Cleanup
security delete-keychain "$KEYCHAIN_PATH"
rm -f "$CERT_PATH" "$APPLE_API_KEY_PATH" "$ZIP_PATH"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: ${{ matrix.artifact }}${{ runner.os == 'Windows' && '.exe' || '' }}
build-desktop:
needs: prepare
if: needs.prepare.outputs.should_release == 'true'
strategy:
matrix:
include:
- os: macos-latest
target: aarch64-apple-darwin
artifact: ralph-desktop-macos-arm64
updater_platform: darwin-aarch64
- os: windows-latest
target: x86_64-pc-windows-msvc
artifact: ralph-desktop-windows-x86_64
updater_platform: windows-x86_64
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
artifact: ralph-desktop-linux-x86_64
updater_platform: linux-x86_64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install frontend dependencies
run: npm ci
- name: Install Linux dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: Build Tauri app
id: tauri
uses: tauri-apps/tauri-action@v0
env:
# Signing only — notarization is done manually in the next step
# because Tauri's built-in notarization uses `notarytool --wait`
# which hangs on transient network errors in CI.
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
KEYCHAIN_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
# Updater signing — required to produce .sig files and latest.json.
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
args: --target ${{ matrix.target }}
includeUpdaterJson: true
- name: Notarize and staple DMG (macOS)
if: runner.os == 'macOS'
env:
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }}
run: |
set -euo pipefail
export APPLE_API_KEY_PATH="$RUNNER_TEMP/AuthKey.p8"
echo "$APPLE_API_KEY_BASE64" | base64 --decode > "$APPLE_API_KEY_PATH"
DMG=$(ls target/${{ matrix.target }}/release/bundle/dmg/*.dmg | head -n1)
echo "Notarizing $DMG"
./scripts/notarize.sh "$DMG"
xcrun stapler staple "$DMG"
xcrun stapler validate "$DMG"
rm -f "$APPLE_API_KEY_PATH"
- name: Collect updater metadata
shell: bash
env:
TAG_NAME: ${{ needs.prepare.outputs.tag }}
VERSION: ${{ needs.prepare.outputs.version }}
BUNDLE_DIR: target/${{ matrix.target }}/release/bundle
UPDATER_PLATFORM: ${{ matrix.updater_platform }}
run: |
set -euo pipefail
# Find the updater bundle + its .sig produced by Tauri. Each OS
# has exactly one updater target: .app.tar.gz (mac), .nsis .exe
# (windows), .AppImage (linux).
case "$UPDATER_PLATFORM" in
darwin-*)
BUNDLE=$(ls "$BUNDLE_DIR"/macos/*.app.tar.gz | head -n1)
;;
windows-*)
BUNDLE=$(ls "$BUNDLE_DIR"/nsis/*-setup.exe | head -n1)
;;
linux-*)
BUNDLE=$(ls "$BUNDLE_DIR"/appimage/*.AppImage | head -n1)
;;
esac
SIG_FILE="${BUNDLE}.sig"
SIG=$(cat "$SIG_FILE")
ASSET_NAME=$(basename "$BUNDLE")
URL="https://github.com/KitStream/ralph/releases/download/${TAG_NAME}/${ASSET_NAME}"
mkdir -p updater-meta
cat > "updater-meta/${UPDATER_PLATFORM}.json" <<EOF
{
"platform": "${UPDATER_PLATFORM}",
"version": "${VERSION}",
"signature": "${SIG//$'\n'/\\n}",
"url": "${URL}",
"bundle": "${BUNDLE}"
}
EOF
- name: Upload desktop artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: |
target/${{ matrix.target }}/release/bundle/dmg/*.dmg
target/${{ matrix.target }}/release/bundle/nsis/*.exe
target/${{ matrix.target }}/release/bundle/nsis/*.exe.sig
target/${{ matrix.target }}/release/bundle/msi/*.msi
target/${{ matrix.target }}/release/bundle/msi/*.msi.sig
target/${{ matrix.target }}/release/bundle/deb/*.deb
target/${{ matrix.target }}/release/bundle/appimage/*.AppImage
target/${{ matrix.target }}/release/bundle/appimage/*.AppImage.sig
target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz
target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz.sig
- name: Upload updater metadata
uses: actions/upload-artifact@v4
with:
name: updater-meta-${{ matrix.updater_platform }}
path: updater-meta/*.json
release:
needs: [prepare, build-cli, build-desktop]
if: needs.prepare.outputs.should_release == 'true'
runs-on: ubuntu-latest
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Assemble updater latest.json
env:
VERSION: ${{ needs.prepare.outputs.version }}
run: |
set -euo pipefail
PUB_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
python3 - <<PY
import json, glob, os
platforms = {}
version = os.environ["VERSION"]
for path in glob.glob("artifacts/updater-meta-*/*.json"):
with open(path) as f:
meta = json.load(f)
platforms[meta["platform"]] = {
"signature": meta["signature"],
"url": meta["url"],
}
out = {
"version": version,
"notes": "See release notes on GitHub.",
"pub_date": "$PUB_DATE",
"platforms": platforms,
}
os.makedirs("latest", exist_ok=True)
with open("latest/latest.json", "w") as f:
json.dump(out, f, indent=2)
print(json.dumps(out, indent=2))
PY
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.prepare.outputs.tag }}
name: v${{ needs.prepare.outputs.version }}
generate_release_notes: true
body: |
## macOS notes
The desktop app (`.dmg`) and CLI binaries are signed and notarized
with a Developer ID. macOS will check Apple's notary service
online on first launch — no manual `xattr` workaround needed.
files: |
artifacts/ralph-*/**/*
latest/latest.json