Release #4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |