Skip to content

Merge pull request #50 from MaxMB15/chore-bump-v0.4.2 #38

Merge pull request #50 from MaxMB15/chore-bump-v0.4.2

Merge pull request #50 from MaxMB15/chore-bump-v0.4.2 #38

Workflow file for this run

name: Release
on:
push:
tags:
- "v*"
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
jobs:
# Create the draft release before platform builds so parallel jobs can upload without racing
# ("release not found" when Linux finished before macOS ran gh release create).
prepare-release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Create draft GitHub release (if missing)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
VERSION=$(python3 -c "import json; print(json.load(open('apps/desktop/src-tauri/tauri.conf.json'))['version'])")
if gh release view "v${VERSION}" --repo "$GITHUB_REPOSITORY" &>/dev/null; then
echo "Release v${VERSION} already exists; skipping create."
else
gh release create "v${VERSION}" \
--repo "$GITHUB_REPOSITORY" \
--title "Max Video Player v${VERSION}" \
--notes "See the [CHANGELOG](https://github.com/MaxMB15/MaxVideoPlayer/blob/main/CHANGELOG.md) for details." \
--draft
fi
release-macos:
needs: [prepare-release]
runs-on: macos-latest
env:
CARGO_HOME: /Users/runner/.cargo
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: ". -> target"
cache-on-failure: true
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install frontend deps
run: npm ci
- name: Install system deps
run: |
# ffmpeg@7 required: mpv 0.40.0 uses FF_PROFILE_* macros removed in ffmpeg 8.x
brew install meson ninja pkg-config ffmpeg@7 libass libplacebo dylibbundler
# Make ffmpeg@7 visible to pkg-config (it is keg-only, not linked by default)
echo "PKG_CONFIG_PATH=$(brew --prefix ffmpeg@7)/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}" >> $GITHUB_ENV
- name: Build libmpv
run: ./scripts/build-libmpv.sh macos
- name: Prepare signing key
run: |
# Tauri CLI requires the raw minisign format (starting with "untrusted comment:").
# The secret may be stored as:
# (a) base64-encoded raw key, or
# (b) raw key with actual newlines, or
# (c) raw key with escaped \n (two chars) instead of real newlines.
# We normalize to raw format and always write to GITHUB_ENV so the key
# is guaranteed to have proper newlines when Tauri reads it.
python3 - << 'EOF'
import base64, os, sys
key = os.environ.get("TAURI_SIGNING_PRIVATE_KEY", "")
print(f"[signing] Key length: {len(key)}, has newlines: {'yes' if chr(10) in key else 'no'}")
if not key.strip():
print("[signing] ERROR: no signing key set.")
sys.exit(1)
# Normalize literal \n (two chars) to actual newlines
key = key.replace("\\n", "\n")
def try_b64(s):
try:
return base64.b64decode(s.strip(), validate=True).decode("utf-8")
except Exception:
return None
if "untrusted comment:" in key:
final_key = key
print("[signing] Raw minisign key detected.")
else:
# Try up to 2 levels of base64 decoding to handle both
# single-encoded and accidentally double-encoded secrets.
current = key
final_key = None
for depth in range(1, 3):
decoded = try_b64(current)
if decoded is None:
print(f"[signing] ERROR: base64 decode failed at depth {depth}.")
sys.exit(1)
if "untrusted comment:" in decoded:
final_key = decoded
print(f"[signing] Key decoded at depth {depth}.")
break
current = decoded
if final_key is None:
print("[signing] ERROR: key is not a valid minisign private key.")
sys.exit(1)
with open(os.environ["GITHUB_ENV"], "a") as f:
f.write("TAURI_SIGNING_PRIVATE_KEY<<__ENDKEY__\n")
f.write(final_key.rstrip("\n") + "\n")
f.write("__ENDKEY__\n")
print("[signing] Key written to GITHUB_ENV.")
EOF
- name: Build Tauri app
# Build without updater artifacts so bundle_dmg.sh doesn't race with
# hdiutil over the .app bundle. We create the updater artifacts manually below.
run: cd apps/desktop && npx tauri build --config '{"bundle":{"createUpdaterArtifacts":false}}'
- name: Create updater artifacts
run: |
set -euo pipefail
VERSION=$(python3 -c "import json; print(json.load(open('apps/desktop/src-tauri/tauri.conf.json'))['version'])")
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
BUNDLE_DIR="target/release/bundle/macos"
UPDATER="$BUNDLE_DIR/MaxVideoPlayer.app.tar.gz"
# Create the .tar.gz updater archive from the built .app
cd "$BUNDLE_DIR"
tar czf MaxVideoPlayer.app.tar.gz MaxVideoPlayer.app
cd -
# tauri signer sign -k always base64-decodes its argument (no raw-format
# detection). Re-encode the raw key as base64 so Tauri can decode it.
B64_KEY=$(python3 -c "import base64,os; print(base64.b64encode(os.environ['TAURI_SIGNING_PRIVATE_KEY'].encode()).decode())")
if [[ -n "${TAURI_SIGNING_PRIVATE_KEY_PASSWORD:-}" ]]; then
cd apps/desktop && npx tauri signer sign \
-k "$B64_KEY" \
-p "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" \
"../../$UPDATER"
cd -
else
cd apps/desktop && npx tauri signer sign \
-k "$B64_KEY" \
--no-password \
"../../$UPDATER"
cd -
fi
- name: Upload macOS updater signature
uses: actions/upload-artifact@v4
with:
name: macos-updater-sig
path: target/release/bundle/macos/MaxVideoPlayer.app.tar.gz.sig
- name: Upload macOS assets to draft release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
VERSION="$VERSION"
DMG="target/release/bundle/dmg/MaxVideoPlayer_${VERSION}_aarch64.dmg"
UPDATER="target/release/bundle/macos/MaxVideoPlayer.app.tar.gz"
UPDATER_VERSIONED="target/release/bundle/macos/MaxVideoPlayer_${VERSION}_aarch64.app.tar.gz"
# Rename updater files so the download URL matches what latest.json expects
cp "$UPDATER" "$UPDATER_VERSIONED"
cp "${UPDATER}.sig" "${UPDATER_VERSIONED}.sig"
gh release upload "v${VERSION}" \
--repo "$GITHUB_REPOSITORY" \
--clobber \
"$DMG" \
"$UPDATER_VERSIONED" \
"${UPDATER_VERSIONED}.sig"
release-linux:
needs: [prepare-release]
runs-on: ubuntu-latest
env:
CARGO_HOME: /home/runner/.cargo
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: ". -> target"
cache-on-failure: true
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install frontend deps
run: rm -f package-lock.json && npm install
- name: Install system deps
run: |
sudo apt-get update
sudo apt-get install -y \
libmpv-dev libgtk-3-dev libwebkit2gtk-4.1-dev \
libjavascriptcoregtk-4.1-dev libsoup-3.0-dev \
libayatana-appindicator3-dev \
libegl-dev libssl-dev pkg-config librsvg2-dev \
patchelf rpm
- name: Prepare signing key
run: |
python3 - << 'EOF'
import base64, os, sys
key = os.environ.get("TAURI_SIGNING_PRIVATE_KEY", "")
if not key.strip():
print("[signing] ERROR: no signing key set.")
sys.exit(1)
key = key.replace("\\n", "\n")
def try_b64(s):
try: return base64.b64decode(s.strip(), validate=True).decode("utf-8")
except Exception: return None
if "untrusted comment:" in key:
final_key = key
else:
current = key
final_key = None
for depth in range(1, 3):
decoded = try_b64(current)
if decoded is None:
print(f"[signing] ERROR: base64 decode failed at depth {depth}.")
sys.exit(1)
if "untrusted comment:" in decoded:
final_key = decoded
break
current = decoded
if final_key is None:
print("[signing] ERROR: key is not a valid minisign private key.")
sys.exit(1)
with open(os.environ["GITHUB_ENV"], "a") as f:
f.write("TAURI_SIGNING_PRIVATE_KEY<<__ENDKEY__\n")
f.write(final_key.rstrip("\n") + "\n")
f.write("__ENDKEY__\n")
print("[signing] Key written to GITHUB_ENV.")
EOF
- name: Build Tauri app
run: cd apps/desktop && npx tauri build --config '{"bundle":{"createUpdaterArtifacts":false}}'
- name: Create updater artifacts
run: |
set -euo pipefail
VERSION=$(python3 -c "import json; print(json.load(open('apps/desktop/src-tauri/tauri.conf.json'))['version'])")
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
BUNDLE_DIR="target/release/bundle"
APPIMAGE=$(find "$BUNDLE_DIR/appimage" -name "*.AppImage" | head -1)
if [[ -n "$APPIMAGE" ]]; then
UPDATER="$BUNDLE_DIR/MaxVideoPlayer_linux.tar.gz"
tar czf "$UPDATER" -C "$(dirname "$APPIMAGE")" "$(basename "$APPIMAGE")"
B64_KEY=$(python3 -c "import base64,os; print(base64.b64encode(os.environ['TAURI_SIGNING_PRIVATE_KEY'].encode()).decode())")
if [[ -n "${TAURI_SIGNING_PRIVATE_KEY_PASSWORD:-}" ]]; then
cd apps/desktop && npx tauri signer sign -k "$B64_KEY" -p "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" "../../$UPDATER"
cd -
else
cd apps/desktop && npx tauri signer sign -k "$B64_KEY" --no-password "../../$UPDATER"
cd -
fi
fi
- name: Upload Linux updater signature
uses: actions/upload-artifact@v4
with:
name: linux-updater-sig
path: target/release/bundle/MaxVideoPlayer_linux.tar.gz.sig
if-no-files-found: warn
- name: Publish to GitHub release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
VERSION="$VERSION"
BUNDLE_DIR="target/release/bundle"
# Collect all Linux artifacts
ARTIFACTS=()
for deb in "$BUNDLE_DIR"/deb/*.deb; do
[[ -f "$deb" ]] && ARTIFACTS+=("$deb")
done
for rpm in "$BUNDLE_DIR"/rpm/*.rpm; do
[[ -f "$rpm" ]] && ARTIFACTS+=("$rpm")
done
for appimage in "$BUNDLE_DIR"/appimage/*.AppImage; do
[[ -f "$appimage" ]] && ARTIFACTS+=("$appimage")
done
# Include updater tar.gz if it exists
UPDATER="$BUNDLE_DIR/MaxVideoPlayer_linux.tar.gz"
if [[ -f "$UPDATER" ]]; then
ARTIFACTS+=("$UPDATER")
[[ -f "${UPDATER}.sig" ]] && ARTIFACTS+=("${UPDATER}.sig")
fi
# Upload to draft release (created by prepare-release job before builds)
if [[ ${#ARTIFACTS[@]} -eq 0 ]]; then
echo "::error::No Linux artifacts found under $BUNDLE_DIR (deb/rpm/appimage)."
exit 1
fi
for artifact in "${ARTIFACTS[@]}"; do
gh release upload "v${VERSION}" "$artifact" \
--repo "$GITHUB_REPOSITORY" \
--clobber
done
# ── Publish latest.json (depends on both platform jobs) ───────────────
publish-latest-json:
runs-on: ubuntu-latest
needs: [release-macos, release-linux]
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Download updater signatures
uses: actions/download-artifact@v4
with:
path: sigs
- name: Build and upload latest.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
VERSION=$(python3 -c "import json; print(json.load(open('apps/desktop/src-tauri/tauri.conf.json'))['version'])")
MACOS_SIG=""
if [[ -f sigs/macos-updater-sig/MaxVideoPlayer.app.tar.gz.sig ]]; then
MACOS_SIG=$(cat sigs/macos-updater-sig/MaxVideoPlayer.app.tar.gz.sig)
fi
LINUX_SIG=""
if [[ -f sigs/linux-updater-sig/MaxVideoPlayer_linux.tar.gz.sig ]]; then
LINUX_SIG=$(cat sigs/linux-updater-sig/MaxVideoPlayer_linux.tar.gz.sig)
fi
python3 - "$VERSION" "$MACOS_SIG" "$LINUX_SIG" << 'PYEOF'
import json, sys
from datetime import datetime, timezone
version, macos_sig, linux_sig = sys.argv[1], sys.argv[2], sys.argv[3]
platforms = {}
if macos_sig:
platforms["darwin-aarch64"] = {
"signature": macos_sig,
"url": f"https://github.com/MaxMB15/MaxVideoPlayer/releases/download/v{version}/MaxVideoPlayer_{version}_aarch64.app.tar.gz"
}
if linux_sig:
platforms["linux-x86_64"] = {
"signature": linux_sig,
"url": f"https://github.com/MaxMB15/MaxVideoPlayer/releases/download/v{version}/MaxVideoPlayer_linux.tar.gz"
}
base_url = f"https://github.com/MaxMB15/MaxVideoPlayer/releases/download/v{version}"
packages = {
"linux-deb-x86_64": f"{base_url}/MaxVideoPlayer_{version}_amd64.deb",
"linux-rpm-x86_64": f"{base_url}/MaxVideoPlayer-{version}-1.x86_64.rpm",
}
data = {
"version": version,
"notes": "See the CHANGELOG for details.",
"pub_date": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"platforms": platforms,
"packages": packages
}
with open("latest.json", "w") as f:
json.dump(data, f, indent=2)
print(f"latest.json written with platforms: {list(platforms.keys())}")
PYEOF
gh release upload "v${VERSION}" latest.json \
--repo "$GITHUB_REPOSITORY" \
--clobber