-
-
Notifications
You must be signed in to change notification settings - Fork 0
488 lines (424 loc) · 18.3 KB
/
release.yml
File metadata and controls
488 lines (424 loc) · 18.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
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 \
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 \
meson ninja-build \
libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libavfilter-dev \
libass-dev libpulse-dev libasound2-dev \
libplacebo-dev \
libx11-dev libxss-dev libxext-dev libxpresent-dev libxrandr-dev \
libwayland-dev
- name: Build libmpv from source
run: |
./scripts/build-libmpv.sh linux
echo "LD_LIBRARY_PATH=$(pwd)/libs/linux:${LD_LIBRARY_PATH:-}" >> "$GITHUB_ENV"
- 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
# ── Sync main back to dev after release ─────────────────────────────────
sync-dev:
runs-on: ubuntu-latest
needs: [publish-latest-json]
if: always() && needs.publish-latest-json.result == 'success'
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_AUTOMATION_PAT || github.token }}
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Merge main into dev
env:
GH_TOKEN: ${{ secrets.RELEASE_AUTOMATION_PAT || github.token }}
run: |
set -euo pipefail
# Check if dev branch exists before fetching (fetch fails if branch is missing)
if ! git ls-remote --heads origin dev | grep -q .; then
echo "::notice::No dev branch found — skipping sync."
exit 0
fi
git fetch origin main dev
git checkout dev
git reset --hard origin/dev
# Check if main has anything new for dev
AHEAD=$(git rev-list --count origin/dev..origin/main)
if [ "$AHEAD" = "0" ]; then
echo "dev is already up to date with main."
exit 0
fi
# Try fast-forward merge first, fall back to merge commit
if git merge origin/main --ff-only 2>/dev/null; then
echo "Fast-forwarded dev to main."
git push origin dev
elif git merge origin/main --no-edit -m "chore: sync main back to dev after release"; then
echo "Merged main into dev (merge commit)."
git push origin dev
else
echo "::warning::Auto-merge of main into dev failed (conflicts). Creating PR instead."
# Create a PR for manual resolution
BRANCH="chore/sync-main-to-dev-$(date -u +%Y%m%d-%H%M%S)-${GITHUB_RUN_ID}"
git merge --abort || true
git checkout -b "$BRANCH" origin/main
git push -u origin "$BRANCH"
gh pr create \
--base dev \
--head "$BRANCH" \
--title "chore: sync main back to dev after release" \
--body "Automated post-release sync. Main has changes (version bump + release tag) that need to be merged back into dev. Resolve any conflicts and merge."
fi
- name: Summary
run: echo "### main synced back to dev" >> "$GITHUB_STEP_SUMMARY"