Skip to content

Release

Release #67

Workflow file for this run

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}"