Skip to content

Release

Release #4

Workflow file for this run

name: Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
draft:
description: Create or update a draft release
required: true
default: true
type: boolean
prerelease:
description: Mark the release as a prerelease
required: true
default: false
type: boolean
release_tag:
description: Optional explicit release tag like v0.1.0
required: false
type: string
platforms:
description: Platform matrix to build
required: true
default: all
type: choice
options:
- all
- linux-windows
- linux
- windows
- macos
unsigned_preview:
description: Build unsigned preview bundles without updater artifacts
required: true
default: false
type: boolean
permissions:
contents: write
concurrency:
group: release-${{ github.ref_name || github.sha }}
cancel-in-progress: false
jobs:
prepare-release:
name: Resolve release metadata
runs-on: ubuntu-latest
outputs:
resolved_tag: ${{ steps.resolve.outputs.resolved_tag }}
resolved_version: ${{ steps.resolve.outputs.resolved_version }}
build_matrix: ${{ steps.matrix.outputs.build_matrix }}
unsigned_preview: ${{ steps.matrix.outputs.unsigned_preview }}
steps:
- uses: actions/checkout@v4
- name: Resolve tag and version
id: resolve
env:
INPUT_RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || '' }}
GITHUB_REF_TYPE: ${{ github.ref_type }}
GITHUB_REF_NAME: ${{ github.ref_name }}
run: |
resolved_tag="$INPUT_RELEASE_TAG"
if [ -z "$resolved_tag" ] && [ "$GITHUB_REF_TYPE" = "tag" ]; then
resolved_tag="$GITHUB_REF_NAME"
fi
if [ -z "$resolved_tag" ]; then
resolved_tag="v$(python -c "import json, pathlib; print(json.loads(pathlib.Path('package.json').read_text())['version'])")"
fi
case "$resolved_tag" in
v*) ;;
*)
echo "Release tag must start with v, got: $resolved_tag" >&2
exit 1
;;
esac
echo "resolved_tag=$resolved_tag" >> "$GITHUB_OUTPUT"
echo "resolved_version=${resolved_tag#v}" >> "$GITHUB_OUTPUT"
- name: Resolve build matrix
id: matrix
env:
INPUT_PLATFORMS: ${{ github.event_name == 'workflow_dispatch' && inputs.platforms || 'all' }}
UNSIGNED_PREVIEW: ${{ github.event_name == 'workflow_dispatch' && inputs.unsigned_preview || false }}
run: |
python - <<'PY'
import json
import os
platform_sets = {
"all": {"macos", "windows", "linux"},
"linux-windows": {"linux", "windows"},
"linux": {"linux"},
"windows": {"windows"},
"macos": {"macos"},
}
selected = os.environ.get("INPUT_PLATFORMS") or "all"
if selected not in platform_sets:
raise SystemExit(f"Unsupported platforms input: {selected}")
targets = [
{
"label": "macOS Apple Silicon",
"platform": "macos",
"os": "macos-latest",
"rust_targets": "aarch64-apple-darwin",
"args": "--target aarch64-apple-darwin",
},
{
"label": "macOS Intel",
"platform": "macos",
"os": "macos-latest",
"rust_targets": "x86_64-apple-darwin",
"args": "--target x86_64-apple-darwin",
},
{
"label": "Windows",
"platform": "windows",
"os": "windows-latest",
"rust_targets": "",
"args": "",
},
{
"label": "Linux",
"platform": "linux",
"os": "ubuntu-22.04",
"rust_targets": "",
"args": "",
},
]
include = [target for target in targets if target["platform"] in platform_sets[selected]]
unsigned_preview = (os.environ.get("UNSIGNED_PREVIEW") or "false").lower() == "true"
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
matrix = json.dumps({"include": include}, separators=(",", ":"))
print(f"build_matrix={matrix}", file=output)
print(f"unsigned_preview={'true' if unsigned_preview else 'false'}", file=output)
PY
- name: Verify version alignment
env:
RESOLVED_VERSION: ${{ steps.resolve.outputs.resolved_version }}
run: |
package_version="$(python -c "import json, pathlib; print(json.loads(pathlib.Path('package.json').read_text())['version'])")"
cargo_version="$(sed -n '/^\[package\]/,/^\[/p' src-tauri/Cargo.toml | sed -n 's/^version = "\(.*\)"/\1/p' | head -n1)"
tauri_version="$(python -c "import json, pathlib; print(json.loads(pathlib.Path('src-tauri/tauri.conf.json').read_text())['version'])")"
printf 'package.json=%s\nCargo.toml=%s\ntauri.conf.json=%s\nresolved=%s\n' \
"$package_version" \
"$cargo_version" \
"$tauri_version" \
"$RESOLVED_VERSION"
test "$package_version" = "$cargo_version"
test "$package_version" = "$tauri_version"
test "$package_version" = "$RESOLVED_VERSION"
- name: Verify updater signing secret
if: ${{ steps.matrix.outputs.unsigned_preview != 'true' }}
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
run: |
if [ -z "$TAURI_SIGNING_PRIVATE_KEY" ]; then
echo "TAURI_SIGNING_PRIVATE_KEY is required because src-tauri/tauri.conf.json has bundle.createUpdaterArtifacts=true." >&2
echo "Add the minisign private key as a repository Actions secret before dispatching the release workflow." >&2
exit 1
fi
build-release:
name: Build ${{ matrix.label }}
needs: prepare-release
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.prepare-release.outputs.build_matrix) }}
steps:
- uses: actions/checkout@v4
- name: Install Linux desktop dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential \
curl \
file \
pkg-config \
libdbus-1-dev \
libfuse2 \
libglib2.0-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
libssl-dev \
libwebkit2gtk-4.1-dev \
libxdo-dev \
librsvg2-dev \
patchelf \
rpm \
wget
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: oven-sh/setup-bun@v2
- name: Install Rust toolchain
if: matrix.rust_targets == ''
uses: dtolnay/rust-toolchain@1.94.1
- name: Install Rust toolchain with target
if: matrix.rust_targets != ''
uses: dtolnay/rust-toolchain@1.94.1
with:
targets: ${{ matrix.rust_targets }}
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri -> target
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Write unsigned preview Tauri override
if: ${{ needs.prepare-release.outputs.unsigned_preview == 'true' }}
run: |
python - <<'PY'
import json
import pathlib
pathlib.Path("src-tauri/ci.unsigned.conf.json").write_text(
json.dumps({"bundle": {"createUpdaterArtifacts": False}}, indent=2) + "\n",
encoding="utf-8",
)
PY
- name: Build and upload release assets
uses: tauri-apps/tauri-action@v0.6.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
tagName: ${{ needs.prepare-release.outputs.resolved_tag }}
releaseName: PathKeep ${{ needs.prepare-release.outputs.resolved_tag }}
releaseBody: |
Automated desktop release for PathKeep.
Download the installer or package for your platform from the assets below.
`latest.json`, `SHA256SUMS.txt`, and `RELEASE-MANIFEST.json` are attached after all matrix builds complete.
See `RELEASE.md` and `TROUBLESHOOTING.md` in the repository for platform support boundaries and operator guidance.
releaseCommitish: ${{ github.sha }}
releaseDraft: ${{ github.event_name == 'workflow_dispatch' && inputs.draft || false }}
prerelease: ${{ github.event_name == 'workflow_dispatch' && inputs.prerelease || github.event_name != 'workflow_dispatch' && contains(github.ref_name, '-') }}
generateReleaseNotes: true
releaseAssetNamePattern: '[name]_[version]_[platform]_[arch][setup][ext]'
args: >-
${{ matrix.args }}
${{ needs.prepare-release.outputs.unsigned_preview == 'true' && '--no-sign --config src-tauri/ci.unsigned.conf.json' || '' }}
release-manifest:
name: Publish checksum manifest
runs-on: ubuntu-latest
needs:
- prepare-release
- build-release
steps:
- uses: actions/checkout@v4
- name: Download release assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RESOLVED_TAG: ${{ needs.prepare-release.outputs.resolved_tag }}
run: |
mkdir -p dist/release
gh release download "$RESOLVED_TAG" \
--repo "$GITHUB_REPOSITORY" \
--dir dist/release
printf '%s' "$RESOLVED_TAG" > dist/release/.resolved-tag
- name: Verify updater manifest exists
if: ${{ needs.prepare-release.outputs.unsigned_preview != 'true' }}
run: test -f dist/release/latest.json
- name: Generate SHA256SUMS.txt
run: |
cd dist/release
find . -type f ! -name 'SHA256SUMS.txt' ! -name '.resolved-tag' -print0 \
| sort -z \
| xargs -0 shasum -a 256 > SHA256SUMS.txt
- name: Generate RELEASE-MANIFEST.json
run: |
python -c "import datetime, hashlib, json, pathlib; root = pathlib.Path('dist/release'); resolved_tag = (root / '.resolved-tag').read_text().strip(); files = [path for path in sorted(root.iterdir()) if path.is_file() and path.name not in {'SHA256SUMS.txt', '.resolved-tag', 'RELEASE-MANIFEST.json'}]; manifest = {'releaseTag': resolved_tag, 'generatedAt': datetime.datetime.now(datetime.timezone.utc).isoformat(), 'files': [{'name': path.name, 'sizeBytes': path.stat().st_size, 'sha256': hashlib.sha256(path.read_bytes()).hexdigest()} for path in files]}; (root / 'RELEASE-MANIFEST.json').write_text(json.dumps(manifest, indent=2) + '\n')"
- name: Upload checksum manifest to the GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
resolved_tag="$(cat dist/release/.resolved-tag)"
gh release upload "$resolved_tag" \
dist/release/SHA256SUMS.txt \
dist/release/RELEASE-MANIFEST.json \
--repo "$GITHUB_REPOSITORY" \
--clobber
- name: Upload checksum workflow artifact
uses: actions/upload-artifact@v4
with:
name: release-manifests
path: |
dist/release/SHA256SUMS.txt
dist/release/RELEASE-MANIFEST.json