Release #59
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: | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| jobs: | |
| build_macos: | |
| runs-on: macos-latest | |
| environment: release | |
| env: | |
| CODESIGN_IDENTITY: ${{ vars.CODESIGN_IDENTITY }} | |
| NOTARY_PROFILE_NAME: ${{ vars.NOTARY_PROFILE_NAME }} | |
| APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| TAURI_SIGNING_PRIVATE_KEY_B64: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_B64 }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Rust cache | |
| uses: swatinem/rust-cache@v2 | |
| with: | |
| workspaces: './src-tauri -> target' | |
| - name: Install CMake | |
| run: brew install cmake | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Import signing certificate | |
| env: | |
| APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| KEYCHAIN=build.keychain | |
| CERT_PATH=cert.p12 | |
| echo "$APPLE_CERTIFICATE_P12" | base64 --decode > "$CERT_PATH" 2>/dev/null || \ | |
| echo "$APPLE_CERTIFICATE_P12" | base64 -D > "$CERT_PATH" | |
| security create-keychain -p "" "$KEYCHAIN" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN" | |
| security unlock-keychain -p "" "$KEYCHAIN" | |
| security import "$CERT_PATH" -k "$KEYCHAIN" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign | |
| security list-keychains -d user -s "$KEYCHAIN" | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "" "$KEYCHAIN" | |
| - name: Configure notarytool credentials | |
| env: | |
| APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} | |
| APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }} | |
| APPLE_API_PRIVATE_KEY_B64: ${{ secrets.APPLE_API_PRIVATE_KEY_B64 }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p private_keys | |
| echo "$APPLE_API_PRIVATE_KEY_B64" | base64 --decode > private_keys/AuthKey.p8 | |
| xcrun notarytool store-credentials "$NOTARY_PROFILE_NAME" \ | |
| --key-id "$APPLE_API_KEY_ID" \ | |
| --issuer "$APPLE_API_ISSUER_ID" \ | |
| --key "private_keys/AuthKey.p8" | |
| - name: Write Tauri signing key | |
| run: | | |
| set -euo pipefail | |
| mkdir -p "$HOME/.tauri" | |
| echo "$TAURI_SIGNING_PRIVATE_KEY_B64" | base64 --decode > "$HOME/.tauri/codexmonitor.key" | |
| - name: Build app bundle | |
| run: | | |
| set -euo pipefail | |
| export TAURI_SIGNING_PRIVATE_KEY | |
| TAURI_SIGNING_PRIVATE_KEY="$(cat "$HOME/.tauri/codexmonitor.key")" | |
| npm run tauri -- build --bundles app | |
| - name: Bundle OpenSSL and re-sign | |
| run: | | |
| set -euo pipefail | |
| CODESIGN_IDENTITY="$CODESIGN_IDENTITY" \ | |
| scripts/macos-fix-openssl.sh | |
| - name: Notarize and staple | |
| run: | | |
| set -euo pipefail | |
| ditto -c -k --keepParent \ | |
| src-tauri/target/release/bundle/macos/CodexMonitor.app \ | |
| CodexMonitor.zip | |
| xcrun notarytool submit CodexMonitor.zip \ | |
| --keychain-profile "$NOTARY_PROFILE_NAME" \ | |
| --wait | |
| xcrun stapler staple \ | |
| src-tauri/target/release/bundle/macos/CodexMonitor.app | |
| - name: Package artifacts | |
| run: | | |
| set -euo pipefail | |
| VERSION=$(python3 - <<'PY' | |
| import json | |
| from pathlib import Path | |
| data = json.loads(Path("src-tauri/tauri.conf.json").read_text()) | |
| print(data["version"]) | |
| PY | |
| ) | |
| mkdir -p release-artifacts release-artifacts/dmg-root | |
| rm -rf release-artifacts/dmg-root/CodexMonitor.app | |
| ditto src-tauri/target/release/bundle/macos/CodexMonitor.app \ | |
| release-artifacts/dmg-root/CodexMonitor.app | |
| ditto -c -k --keepParent \ | |
| src-tauri/target/release/bundle/macos/CodexMonitor.app \ | |
| release-artifacts/CodexMonitor.zip | |
| hdiutil create -volname "CodexMonitor" \ | |
| -srcfolder release-artifacts/dmg-root \ | |
| -ov -format UDZO \ | |
| release-artifacts/CodexMonitor_${VERSION}_aarch64.dmg | |
| COPYFILE_DISABLE=1 tar -czf \ | |
| src-tauri/target/release/bundle/macos/CodexMonitor.app.tar.gz \ | |
| -C src-tauri/target/release/bundle/macos CodexMonitor.app | |
| npm run tauri signer sign -- \ | |
| -f "$HOME/.tauri/codexmonitor.key" \ | |
| -p "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" \ | |
| src-tauri/target/release/bundle/macos/CodexMonitor.app.tar.gz | |
| cp src-tauri/target/release/bundle/macos/CodexMonitor.app.tar.gz \ | |
| release-artifacts/CodexMonitor.app.tar.gz | |
| cp src-tauri/target/release/bundle/macos/CodexMonitor.app.tar.gz.sig \ | |
| release-artifacts/CodexMonitor.app.tar.gz.sig | |
| - name: Upload macOS artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: macos-artifacts | |
| path: | | |
| release-artifacts/CodexMonitor.zip | |
| release-artifacts/CodexMonitor_*_aarch64.dmg | |
| release-artifacts/CodexMonitor.app.tar.gz | |
| release-artifacts/CodexMonitor.app.tar.gz.sig | |
| build_linux: | |
| name: appimage (${{ matrix.arch }}) | |
| runs-on: ${{ matrix.platform }} | |
| environment: release | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| TAURI_SIGNING_PRIVATE_KEY_B64: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_B64 }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: ubuntu-24.04 | |
| arch: x86_64 | |
| - platform: ubuntu-24.04-arm | |
| arch: aarch64 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: install dependencies (linux only) | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y cmake libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2 xdg-utils libasound2-dev | |
| - name: setup node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: lts/* | |
| cache: npm | |
| - name: install Rust stable | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Rust cache | |
| uses: swatinem/rust-cache@v2 | |
| with: | |
| workspaces: './src-tauri -> target' | |
| - name: install frontend dependencies | |
| run: npm ci | |
| - name: Write Tauri signing key | |
| run: | | |
| set -euo pipefail | |
| mkdir -p "$HOME/.tauri" | |
| echo "$TAURI_SIGNING_PRIVATE_KEY_B64" | base64 --decode > "$HOME/.tauri/codexmonitor.key" | |
| - name: build AppImage | |
| run: | | |
| set -euo pipefail | |
| export TAURI_SIGNING_PRIVATE_KEY | |
| TAURI_SIGNING_PRIVATE_KEY="$(cat "$HOME/.tauri/codexmonitor.key")" | |
| npm run tauri -- build --bundles appimage | |
| - name: Upload AppImage | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: appimage-${{ matrix.arch }} | |
| path: src-tauri/target/release/bundle/appimage/*.AppImage* | |
| release: | |
| runs-on: ubuntu-latest | |
| environment: release | |
| needs: | |
| - build_macos | |
| - build_linux | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Download macOS artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: macos-artifacts | |
| path: release-artifacts | |
| - name: Download Linux AppImages | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: appimage-* | |
| path: release-artifacts | |
| merge-multiple: true | |
| - name: Build latest.json | |
| run: | | |
| set -euo pipefail | |
| VERSION=$(python3 - <<'PY' | |
| import json | |
| from pathlib import Path | |
| data = json.loads(Path("src-tauri/tauri.conf.json").read_text()) | |
| print(data["version"]) | |
| PY | |
| ) | |
| SIGNATURE=$(cat release-artifacts/CodexMonitor.app.tar.gz.sig) | |
| LAST_TAG=$(git tag --sort=-version:refname \ | |
| | grep -v "^v${VERSION}$" \ | |
| | head -n 1 || true) | |
| RANGE_END="${GITHUB_SHA}" | |
| if [ -n "$LAST_TAG" ]; then | |
| git log "${LAST_TAG}..${RANGE_END}" --pretty=format:"%s" > release-artifacts/release-commits.txt | |
| else | |
| git log "${RANGE_END}" --pretty=format:"%s" > release-artifacts/release-commits.txt | |
| fi | |
| python3 - <<'PY' | |
| import re | |
| from pathlib import Path | |
| lines = Path("release-artifacts/release-commits.txt").read_text().splitlines() | |
| pattern = re.compile(r"^(feat|fix|perf)(?:\([^)]*\))?:\s*(.+)$", re.IGNORECASE) | |
| groups = {"feat": [], "fix": [], "perf": []} | |
| for line in lines: | |
| match = pattern.match(line.strip()) | |
| if not match: | |
| continue | |
| kind = match.group(1).lower() | |
| message = match.group(2).strip() | |
| if message: | |
| groups[kind].append(message) | |
| sections = [ | |
| ("## New Features", "feat"), | |
| ("## Fixes", "fix"), | |
| ("## Performance Improvements", "perf"), | |
| ] | |
| output = [] | |
| for title, key in sections: | |
| items = groups[key] | |
| if not items: | |
| continue | |
| output.append(title) | |
| output.extend([f"- {item}" for item in items]) | |
| output.append("") | |
| notes = "\n".join(output).strip() | |
| if not notes: | |
| notes = "- No user-facing changes." | |
| Path("release-artifacts/release-notes.md").write_text(notes + "\n") | |
| PY | |
| python3 - <<PY | |
| import json | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| notes = Path("release-artifacts/release-notes.md").read_text().strip() | |
| artifacts_dir = Path("release-artifacts") | |
| platforms = { | |
| "darwin-aarch64": { | |
| "url": "https://github.com/Dimillian/CodexMonitor/releases/download/v${VERSION}/CodexMonitor.app.tar.gz", | |
| "signature": "${SIGNATURE}", | |
| } | |
| } | |
| appimages = list(artifacts_dir.rglob("*.AppImage.tar.gz")) | |
| if not appimages: | |
| appimages = list(artifacts_dir.rglob("*.AppImage")) | |
| if not appimages: | |
| raise SystemExit("No AppImage artifacts found for latest.json") | |
| def detect_arch(name): | |
| lowered = name.lower() | |
| if "aarch64" in lowered or "arm64" in lowered: | |
| return "aarch64" | |
| if "x86_64" in lowered or "amd64" in lowered: | |
| return "x86_64" | |
| return None | |
| for appimage in appimages: | |
| arch = detect_arch(appimage.name) | |
| if not arch: | |
| continue | |
| sig_path = appimage.with_suffix(appimage.suffix + ".sig") | |
| if not sig_path.exists(): | |
| raise SystemExit(f"Missing signature for {appimage.name}") | |
| platforms[f"linux-{arch}"] = { | |
| "url": f"https://github.com/Dimillian/CodexMonitor/releases/download/v${VERSION}/{appimage.name}", | |
| "signature": sig_path.read_text().strip(), | |
| } | |
| payload = { | |
| "version": "${VERSION}", | |
| "notes": notes, | |
| "pub_date": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), | |
| "platforms": platforms, | |
| } | |
| Path("release-artifacts/latest.json").write_text(json.dumps(payload, indent=2) + "\n") | |
| PY | |
| - name: Create GitHub release | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| VERSION=$(python3 - <<'PY' | |
| import json | |
| from pathlib import Path | |
| data = json.loads(Path("src-tauri/tauri.conf.json").read_text()) | |
| print(data["version"]) | |
| PY | |
| ) | |
| gh release create "v${VERSION}" \ | |
| --title "v${VERSION}" \ | |
| --notes-file release-artifacts/release-notes.md \ | |
| --target "$GITHUB_SHA" \ | |
| release-artifacts/CodexMonitor.zip \ | |
| release-artifacts/CodexMonitor_*_aarch64.dmg \ | |
| release-artifacts/CodexMonitor.app.tar.gz \ | |
| release-artifacts/CodexMonitor.app.tar.gz.sig \ | |
| release-artifacts/*.AppImage* \ | |
| release-artifacts/latest.json | |
| - name: Bump version and open PR | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| VERSION=$(python3 - <<'PY' | |
| import json | |
| from pathlib import Path | |
| data = json.loads(Path("src-tauri/tauri.conf.json").read_text()) | |
| print(data["version"]) | |
| PY | |
| ) | |
| NEXT_VERSION=$(python3 - <<'PY' "$VERSION" | |
| import sys | |
| version = sys.argv[1] | |
| parts = version.split(".") | |
| if len(parts) != 3: | |
| raise SystemExit("Expected version like 0.X.Y") | |
| major, minor, patch = (int(p) for p in parts) | |
| print(f"{major}.{minor}.{patch + 1}") | |
| PY | |
| ) | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| npm version "$NEXT_VERSION" --no-git-tag-version | |
| python3 - <<PY | |
| import json | |
| from pathlib import Path | |
| path = Path("src-tauri/tauri.conf.json") | |
| data = json.loads(path.read_text()) | |
| data["version"] = "$NEXT_VERSION" | |
| path.write_text(json.dumps(data, indent=2) + "\n") | |
| PY | |
| git checkout -b "chore/bump-version-${NEXT_VERSION}" | |
| git add package.json package-lock.json src-tauri/tauri.conf.json | |
| git commit -m "chore: bump version to ${NEXT_VERSION}" | |
| git push origin "chore/bump-version-${NEXT_VERSION}" | |
| gh pr create \ | |
| --title "chore: bump version to ${NEXT_VERSION}" \ | |
| --body "Post-release version bump to ${NEXT_VERSION}." \ | |
| --base main \ | |
| --head "chore/bump-version-${NEXT_VERSION}" |