Release #67
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/Codex Monitor.app" \ | |
| CodexMonitor.zip | |
| xcrun notarytool submit CodexMonitor.zip \ | |
| --keychain-profile "$NOTARY_PROFILE_NAME" \ | |
| --wait | |
| xcrun stapler staple \ | |
| "src-tauri/target/release/bundle/macos/Codex Monitor.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/Codex Monitor.app" | |
| ditto "src-tauri/target/release/bundle/macos/Codex Monitor.app" \ | |
| "release-artifacts/dmg-root/Codex Monitor.app" | |
| ditto -c -k --keepParent \ | |
| "src-tauri/target/release/bundle/macos/Codex Monitor.app" \ | |
| release-artifacts/CodexMonitor.zip | |
| hdiutil create -volname "Codex Monitor" \ | |
| -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/Codex Monitor.app.tar.gz" \ | |
| -C src-tauri/target/release/bundle/macos "Codex Monitor.app" | |
| npm run tauri signer sign -- \ | |
| -f "$HOME/.tauri/codexmonitor.key" \ | |
| -p "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" \ | |
| "src-tauri/target/release/bundle/macos/Codex Monitor.app.tar.gz" | |
| cp "src-tauri/target/release/bundle/macos/Codex Monitor.app.tar.gz" \ | |
| release-artifacts/CodexMonitor.app.tar.gz | |
| cp "src-tauri/target/release/bundle/macos/Codex Monitor.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: linux bundles (${{ 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 rpm | |
| - 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 and RPM | |
| run: | | |
| set -euo pipefail | |
| export TAURI_SIGNING_PRIVATE_KEY | |
| TAURI_SIGNING_PRIVATE_KEY="$(cat "$HOME/.tauri/codexmonitor.key")" | |
| npm run tauri -- build --bundles appimage,rpm | |
| - name: Validate Linux bundle outputs | |
| run: | | |
| set -euo pipefail | |
| mapfile -t appimages < <(find src-tauri/target/release/bundle/appimage -type f -name '*.AppImage*' | sort) | |
| mapfile -t rpms < <(find src-tauri/target/release/bundle/rpm -type f -name '*.rpm' | sort) | |
| if [ ${#appimages[@]} -eq 0 ]; then | |
| echo "No AppImage output found" | |
| exit 1 | |
| fi | |
| if [ ${#rpms[@]} -eq 0 ]; then | |
| echo "No RPM output found" | |
| exit 1 | |
| fi | |
| - name: Upload Linux bundles | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: linux-bundles-${{ matrix.arch }} | |
| path: | | |
| src-tauri/target/release/bundle/appimage/*.AppImage* | |
| src-tauri/target/release/bundle/rpm/*.rpm | |
| 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 bundles | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: linux-bundles-* | |
| path: release-artifacts | |
| merge-multiple: true | |
| - name: Validate RPM artifacts | |
| run: | | |
| set -euo pipefail | |
| sudo apt-get update | |
| sudo apt-get install -y rpm | |
| mapfile -t rpms < <(find release-artifacts -type f -name '*.rpm' | sort) | |
| if [ ${#rpms[@]} -eq 0 ]; then | |
| echo "No RPM artifacts found" | |
| find release-artifacts -type f | sort | |
| exit 1 | |
| fi | |
| for rpm_file in "${rpms[@]}"; do | |
| rpm -qip "$rpm_file" | |
| rpm -qp --requires "$rpm_file" | |
| done | |
| - 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 | |
| ) | |
| shopt -s nullglob globstar | |
| appimages=(release-artifacts/**/*.AppImage*) | |
| mapfile -t rpms < <(find release-artifacts -type f -name '*.rpm' | sort) | |
| if [ ${#rpms[@]} -eq 0 ]; then | |
| echo "No RPM artifacts found for release upload" | |
| find release-artifacts -type f | sort | |
| exit 1 | |
| fi | |
| 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 \ | |
| "${appimages[@]}" \ | |
| "${rpms[@]}" \ | |
| 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}" |