mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
* ci(release): make test draft releases traceable and self-cleaning Every PR and master build created a draft named "Release v<version>" with tag test-<github.sha>, so 70+ identical drafts piled up and PR drafts were untraceable (for pull_request events github.sha is the ephemeral merge-commit SHA that resolves to nothing in the repo). - Title test drafts as "v<ver> — PR #<n> @ <sha> [test]" / "v<ver> — master @ <sha> [test]"; tag releases keep "Release v<ver>" - Prepend a context header (PR, head commit, workflow run links) to the auto-generated release notes - Use the PR head SHA and pass target_commitish so generated notes actually cover the PR commits - Use stable tags (test-pr-<n>, test-master) so action-gh-release updates one rolling draft in place instead of creating a new one per push - Mark all non-tag drafts as prerelease - Cancel superseded in-progress PR builds via a concurrency group - Delete a PR's rolling draft when the PR closes (new workflow) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci(release): close review-bot race windows in draft release flow - Move concurrency from workflow level to job level: cancelling a whole run could interrupt action-gh-release mid-asset-replacement and leave the rolling draft incomplete. Build slots still cancel superseded PR work (matrix-aware groups); the release job gets its own serializing, never-cancelling group. - Re-check the live PR state in the release job right before touching the draft, so a build that outlives its PR cannot recreate the draft after cleanup deleted it. - In the cleanup workflow, cancel still-running builds of the closed PR (dead work anyway) and wait for them to settle before deleting. - Emit an explicit empty `body=` output for tag builds instead of a blank-line heredoc. Addresses Codex and Greptile review feedback on #1202. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci(release): grant actions:write so PR-close cleanup can cancel builds gh run cancel needs the actions scope; with only contents: write the cancellation 403s silently and the settle-poll burns its full window. Also skip the cleanup job for fork PRs entirely: they never get a draft and their token is read-only regardless of the permissions block. Addresses Greptile P1 / Codex P2 follow-up on #1202. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci(release): re-assert rolling draft title after asset upload action-gh-release@v2 updates name/body/target_commitish on the normal draft-reuse path, but in a rare race (release listing transiently missing the draft) it uploads assets to the canonical oldest draft without refreshing its metadata. PATCH the title and commitish on the release id the action actually used, so the draft title always names the current head SHA; the body is left alone to preserve generated notes. Addresses Codex round-2 feedback on #1202. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci(release): prune stale assets before updating a rolling draft The release action only replaces same-name assets, so a PR that bumps the app version would leave old-version installers beside the new set in its rolling draft. Delete all existing assets of the matched draft before the upload; the action re-uploads the full current set right after. Published releases are never touched. Addresses Codex round-3 feedback on #1202. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci(release): rebuild full draft metadata after asset upload Extend the post-upload metadata step to also rebuild the body (context header + notes from the same generate-notes API the action uses), not just title/commitish. The rolling draft now ends up with correct metadata regardless of which internal action-gh-release path ran, including the rare canonicalize-duplicate fallback. If notes generation fails, the body is left as the action set it. Addresses Codex round-4 feedback on #1202. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci(release): only cancel pull_request runs when cleaning up a closed PR A manually dispatched build on the same head branch is not the PR's work; filter the cancellation list by event so PR-close cleanup cannot abort it. Addresses Codex round-5 feedback on #1202. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci(release): guard PR-close cleanup against close-reopen races Re-check the live PR state at the start of the cleanup job and again right before deleting the draft, so a PR that is reopened while the cleanup is queued or waiting keeps its rolling draft and its fresh reopened-run builds are not cancelled. Addresses Codex round-6 feedback on #1202. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci(release): keep tag_name when patching rolling draft metadata PATCHing a draft release without tag_name makes GitHub drop the pending tag (the draft turns into untagged-<hash>), so the next run cannot find the rolling draft by tag and creates a duplicate — observed live on this PR's own drafts. Include tag_name in both PATCH payloads of the metadata step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1578 lines
81 KiB
YAML
1578 lines
81 KiB
YAML
name: Build and Make Electron App
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- master
|
|
tags:
|
|
- 'v*.*.*'
|
|
pull_request:
|
|
branches:
|
|
- master
|
|
workflow_dispatch:
|
|
|
|
jobs:
|
|
linux-embedded-mpv-runtime:
|
|
name: Build pinned Linux Embedded MPV runtime
|
|
runs-on: ubuntu-22.04
|
|
timeout-minutes: 120
|
|
# Concurrency lives on the build jobs, not the workflow: cancelling a
|
|
# whole run could interrupt action-gh-release mid-update and leave the
|
|
# rolling draft with missing assets. Build slots cancel superseded PR
|
|
# work; the release job only serializes and is never cancelled.
|
|
concurrency:
|
|
group: ${{ github.workflow }}-build-linux-runtime-${{ github.event.pull_request.number || github.ref }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '22'
|
|
|
|
- name: Resolve Linux runtime toolchain cache key
|
|
id: linux-runtime-cache-key
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
sudo apt-get update
|
|
{
|
|
apt-cache policy \
|
|
binutils build-essential cmake curl git gperf \
|
|
libasound2-dev libdrm-dev libegl-dev libgbm-dev \
|
|
libgl-dev libpulse-dev libva-dev make nasm \
|
|
ninja-build patchelf perl pkg-config python3-pip \
|
|
tar xz-utils
|
|
echo 'meson=1.7.2'
|
|
} > "${RUNNER_TEMP}/linux-runtime-toolchain.txt"
|
|
TOOLCHAIN_SHA256="$(sha256sum "${RUNNER_TEMP}/linux-runtime-toolchain.txt" | cut -d ' ' -f 1)"
|
|
SOURCE_SHA256="${{ hashFiles('tools/embedded-mpv/build-linux-runtime.cjs', 'tools/embedded-mpv/build-linux-runtime.mjs', 'tools/embedded-mpv/generate-linux-runtime-notices.cjs', 'tools/embedded-mpv/linux-runtime-manifest.cjs', 'tools/embedded-mpv/linux-source-archive-contract.cjs', 'tools/embedded-mpv/stage-runtime.mjs', 'tools/packaging/prepare-linux-runtime-source-snapshot.cjs') }}"
|
|
echo "toolchain-sha256=${TOOLCHAIN_SHA256}" >> "${GITHUB_OUTPUT}"
|
|
echo "key=linux-frame-copy-runtime-v5-ubuntu-22.04-${TOOLCHAIN_SHA256}-${SOURCE_SHA256}" >> "${GITHUB_OUTPUT}"
|
|
|
|
- name: Restore pinned Linux runtime and immutable source inputs
|
|
id: linux-runtime-cache
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: |
|
|
vendor/embedded-mpv/linux-x64/include
|
|
vendor/embedded-mpv/linux-x64/lib
|
|
vendor/embedded-mpv/linux-x64/runtime-manifest.json
|
|
dist/linux-frame-copy-runtime-source-inputs
|
|
key: ${{ steps.linux-runtime-cache-key.outputs.key }}
|
|
|
|
- name: Install pinned Linux runtime build dependencies
|
|
if: steps.linux-runtime-cache.outputs.cache-hit != 'true'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
sudo apt-get install --no-install-recommends -y \
|
|
binutils \
|
|
build-essential \
|
|
cmake \
|
|
curl \
|
|
git \
|
|
gperf \
|
|
libasound2-dev \
|
|
libdrm-dev \
|
|
libegl-dev \
|
|
libgbm-dev \
|
|
libgl-dev \
|
|
libpulse-dev \
|
|
libva-dev \
|
|
make \
|
|
nasm \
|
|
ninja-build \
|
|
patchelf \
|
|
perl \
|
|
pkg-config \
|
|
python3-pip \
|
|
tar \
|
|
xz-utils
|
|
python3 -m pip install --user 'meson==1.7.2'
|
|
|
|
- name: Build and stage pinned LGPL Linux runtime
|
|
if: steps.linux-runtime-cache.outputs.cache-hit != 'true'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
export PATH="${HOME}/.local/bin:${PATH}"
|
|
export IPTVNATOR_EMBEDDED_MPV_LINUX_BUILD_ROOT="${RUNNER_TEMP}/linux-frame-copy-runtime-build"
|
|
export RUNTIME_PREFIX="${RUNNER_TEMP}/linux-frame-copy-runtime-prefix"
|
|
|
|
node tools/embedded-mpv/build-linux-runtime.mjs "${RUNTIME_PREFIX}"
|
|
node tools/embedded-mpv/stage-runtime.mjs linux x64 "${RUNTIME_PREFIX}"
|
|
|
|
export SOURCE_INPUT_ROOT="${GITHUB_WORKSPACE}/dist/linux-frame-copy-runtime-source-inputs"
|
|
rm -rf "${SOURCE_INPUT_ROOT}"
|
|
mkdir -p \
|
|
"${SOURCE_INPUT_ROOT}/archives" \
|
|
"${SOURCE_INPUT_ROOT}/git"
|
|
|
|
git -C "${IPTVNATOR_EMBEDDED_MPV_LINUX_BUILD_ROOT}/sources/libplacebo" \
|
|
submodule foreach --recursive git clean -ffdqx
|
|
git -C "${IPTVNATOR_EMBEDDED_MPV_LINUX_BUILD_ROOT}/sources/libplacebo" \
|
|
clean -ffdqx
|
|
cp -a "${IPTVNATOR_EMBEDDED_MPV_LINUX_BUILD_ROOT}/archives/." "${SOURCE_INPUT_ROOT}/archives/"
|
|
cp -a "${IPTVNATOR_EMBEDDED_MPV_LINUX_BUILD_ROOT}/sources/libplacebo" "${SOURCE_INPUT_ROOT}/git/libplacebo"
|
|
node tools/embedded-mpv/generate-linux-runtime-notices.cjs collect \
|
|
--runtime-manifest "${RUNTIME_PREFIX}/runtime-manifest.json" \
|
|
--source-root "${IPTVNATOR_EMBEDDED_MPV_LINUX_BUILD_ROOT}/sources" \
|
|
--output-root "${SOURCE_INPUT_ROOT}/license-inputs"
|
|
|
|
- name: Generate Linux runtime notices and assemble source compliance
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
export RUNTIME_ROOT="${GITHUB_WORKSPACE}/vendor/embedded-mpv/linux-x64"
|
|
export SOURCE_INPUT_ROOT="${GITHUB_WORKSPACE}/dist/linux-frame-copy-runtime-source-inputs"
|
|
export SOURCE_BUNDLE_ROOT="${RUNNER_TEMP}/linux-frame-copy-runtime-sources"
|
|
export LIBPLACEBO_SOURCE_RECORD="${RUNNER_TEMP}/libplacebo-source-record.json"
|
|
|
|
test -f "${RUNTIME_ROOT}/runtime-manifest.json"
|
|
test -d "${SOURCE_INPUT_ROOT}/archives"
|
|
test -d "${SOURCE_INPUT_ROOT}/git/libplacebo"
|
|
test -f "${SOURCE_INPUT_ROOT}/license-inputs/linux-runtime-license-inputs.json"
|
|
|
|
rm -rf "${RUNTIME_ROOT}/notices" "${SOURCE_BUNDLE_ROOT}"
|
|
node tools/embedded-mpv/generate-linux-runtime-notices.cjs generate \
|
|
--runtime-manifest "${RUNTIME_ROOT}/runtime-manifest.json" \
|
|
--license-input-root "${SOURCE_INPUT_ROOT}/license-inputs" \
|
|
--output-root "${RUNTIME_ROOT}/notices"
|
|
|
|
mkdir -p \
|
|
"${SOURCE_BUNDLE_ROOT}/archives" \
|
|
"${SOURCE_BUNDLE_ROOT}/git" \
|
|
"${SOURCE_BUNDLE_ROOT}/license-inputs" \
|
|
"${SOURCE_BUNDLE_ROOT}/metadata" \
|
|
"${SOURCE_BUNDLE_ROOT}/notices" \
|
|
"${SOURCE_BUNDLE_ROOT}/tooling"
|
|
|
|
cp -a "${SOURCE_INPUT_ROOT}/archives/." "${SOURCE_BUNDLE_ROOT}/archives/"
|
|
cp -a "${SOURCE_INPUT_ROOT}/license-inputs/." "${SOURCE_BUNDLE_ROOT}/license-inputs/"
|
|
cp -a "${RUNTIME_ROOT}/notices/." "${SOURCE_BUNDLE_ROOT}/notices/"
|
|
cp "${RUNTIME_ROOT}/runtime-manifest.json" "${SOURCE_BUNDLE_ROOT}/metadata/runtime-manifest.json"
|
|
node tools/packaging/prepare-linux-runtime-source-snapshot.cjs prepare \
|
|
--runtime-manifest "${RUNTIME_ROOT}/runtime-manifest.json" \
|
|
--checkout "${SOURCE_INPUT_ROOT}/git/libplacebo" \
|
|
--output "${SOURCE_BUNDLE_ROOT}/git/libplacebo" \
|
|
--record-output "${LIBPLACEBO_SOURCE_RECORD}"
|
|
cp \
|
|
tools/embedded-mpv/build-linux-runtime.cjs \
|
|
tools/embedded-mpv/build-linux-runtime.mjs \
|
|
tools/embedded-mpv/generate-linux-runtime-notices.cjs \
|
|
tools/embedded-mpv/linux-runtime-manifest.cjs \
|
|
tools/embedded-mpv/linux-source-archive-contract.cjs \
|
|
tools/embedded-mpv/stage-runtime.mjs \
|
|
tools/packaging/prepare-linux-runtime-source-snapshot.cjs \
|
|
"${SOURCE_BUNDLE_ROOT}/tooling/"
|
|
test -f "${SOURCE_BUNDLE_ROOT}/notices/THIRD_PARTY_NOTICES.txt"
|
|
test -f "${SOURCE_BUNDLE_ROOT}/notices/embedded-mpv-notices.json"
|
|
git rev-parse HEAD > "${SOURCE_BUNDLE_ROOT}/metadata/iptvnator-git-revision.txt"
|
|
git diff --binary HEAD > "${SOURCE_BUNDLE_ROOT}/metadata/local-changes.patch"
|
|
node <<'NODE'
|
|
const crypto = require('node:crypto');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const {
|
|
EXPECTED_LIBPLACEBO_V7_360_1_SOURCE_SNAPSHOT_SHA256,
|
|
validateLinuxRuntimeSourceSnapshot,
|
|
} = require('./tools/packaging/prepare-linux-runtime-source-snapshot.cjs');
|
|
|
|
const manifest = JSON.parse(
|
|
fs.readFileSync(path.join(process.env.RUNTIME_ROOT, 'runtime-manifest.json'), 'utf8')
|
|
);
|
|
const archivesDirectory = path.join(process.env.SOURCE_BUNDLE_ROOT, 'archives');
|
|
const archives = fs.readdirSync(archivesDirectory).sort().map((name) => {
|
|
const contents = fs.readFileSync(path.join(archivesDirectory, name));
|
|
return {
|
|
name,
|
|
sha256: crypto.createHash('sha256').update(contents).digest('hex'),
|
|
};
|
|
});
|
|
const expectedArchiveHashes = Object.values(manifest.packages)
|
|
.map(({ sourceSha256 }) => sourceSha256)
|
|
.filter(Boolean)
|
|
.sort();
|
|
const actualArchiveHashes = archives.map(({ sha256 }) => sha256).sort();
|
|
if (
|
|
new Set(expectedArchiveHashes).size !== expectedArchiveHashes.length ||
|
|
new Set(actualArchiveHashes).size !== actualArchiveHashes.length ||
|
|
archives.length !== expectedArchiveHashes.length ||
|
|
JSON.stringify(actualArchiveHashes) !== JSON.stringify(expectedArchiveHashes)
|
|
) {
|
|
throw new Error(
|
|
'Source bundle archives must match the exact unique pinned archive hash set.'
|
|
);
|
|
}
|
|
|
|
const libplacebo = JSON.parse(
|
|
fs.readFileSync(process.env.LIBPLACEBO_SOURCE_RECORD, 'utf8')
|
|
);
|
|
if (
|
|
libplacebo.sourceGitCommit !== manifest.packages.libplacebo.sourceGitCommit ||
|
|
JSON.stringify(libplacebo.sourceSubmodules) !==
|
|
JSON.stringify(manifest.packages.libplacebo.sourceSubmodules)
|
|
) {
|
|
throw new Error('Prepared libplacebo source identity does not match the runtime manifest.');
|
|
}
|
|
validateLinuxRuntimeSourceSnapshot(libplacebo.sourceSnapshot, {
|
|
expectedSha256:
|
|
EXPECTED_LIBPLACEBO_V7_360_1_SOURCE_SNAPSHOT_SHA256,
|
|
});
|
|
|
|
const notices = JSON.parse(
|
|
fs.readFileSync(
|
|
path.join(process.env.SOURCE_BUNDLE_ROOT, 'notices', 'embedded-mpv-notices.json'),
|
|
'utf8'
|
|
)
|
|
);
|
|
const repositoryRevision = fs
|
|
.readFileSync(
|
|
path.join(
|
|
process.env.SOURCE_BUNDLE_ROOT,
|
|
'metadata',
|
|
'iptvnator-git-revision.txt'
|
|
),
|
|
'utf8'
|
|
)
|
|
.trim();
|
|
fs.writeFileSync(
|
|
path.join(process.env.SOURCE_BUNDLE_ROOT, 'metadata', 'source-index.json'),
|
|
`${JSON.stringify(
|
|
{
|
|
schemaVersion: 3,
|
|
repositoryRevision,
|
|
sourcePackages: manifest.packages,
|
|
archives,
|
|
libplacebo,
|
|
legal: {
|
|
manifest: 'notices/embedded-mpv-notices.json',
|
|
noticeFile: notices.noticeFile,
|
|
packages: notices.packages,
|
|
},
|
|
},
|
|
null,
|
|
2
|
|
)}\n`
|
|
);
|
|
NODE
|
|
(
|
|
cd "${SOURCE_BUNDLE_ROOT}/archives"
|
|
sha256sum * > "../metadata/archive-sha256.txt"
|
|
)
|
|
node tools/packaging/prepare-linux-runtime-source-snapshot.cjs assert-vcs-free \
|
|
--directory "${SOURCE_BUNDLE_ROOT}"
|
|
|
|
mkdir -p dist/compliance
|
|
rm -f dist/compliance/linux-frame-copy-runtime-sources.tar.xz
|
|
tar \
|
|
--create \
|
|
--xz \
|
|
--sort=name \
|
|
--mtime='UTC 1970-01-01' \
|
|
--owner=0 \
|
|
--group=0 \
|
|
--numeric-owner \
|
|
--file dist/compliance/linux-frame-copy-runtime-sources.tar.xz \
|
|
--directory "${SOURCE_BUNDLE_ROOT}" \
|
|
.
|
|
rm -f "${RUNTIME_ROOT}/source-archive-binding.json"
|
|
node tools/embedded-mpv/linux-source-archive-contract.cjs create \
|
|
--archive dist/compliance/linux-frame-copy-runtime-sources.tar.xz \
|
|
--repository-revision "$(git rev-parse HEAD)" \
|
|
--output "${RUNTIME_ROOT}/source-archive-binding.json"
|
|
test -s "${RUNTIME_ROOT}/source-archive-binding.json"
|
|
|
|
- name: Upload staged Linux runtime
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: linux-embedded-mpv-runtime
|
|
path: vendor/embedded-mpv/linux-x64
|
|
if-no-files-found: error
|
|
retention-days: 7
|
|
|
|
- name: Upload Linux runtime source compliance
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: linux-frame-copy-runtime-sources
|
|
path: dist/compliance/linux-frame-copy-runtime-sources.tar.xz
|
|
if-no-files-found: error
|
|
retention-days: 7
|
|
|
|
build-cross-platform:
|
|
name: Build on ${{ matrix.os }} ${{ matrix.arch }}
|
|
runs-on: ${{ matrix.runner }}
|
|
timeout-minutes: 120
|
|
concurrency:
|
|
group: ${{ github.workflow }}-build-${{ matrix.os }}-${{ matrix.arch }}-${{ github.event.pull_request.number || github.ref }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
# macOS builds - separate runners to avoid native module conflicts
|
|
- os: macos
|
|
runner: macos-15-intel
|
|
arch: x64
|
|
embedded_mpv_platform: darwin
|
|
embedded_mpv_arch: x64
|
|
embedded_mpv_build_runtime: true
|
|
- os: macos
|
|
runner: macos-latest
|
|
arch: arm64
|
|
embedded_mpv_platform: darwin
|
|
embedded_mpv_arch: arm64
|
|
embedded_mpv_build_runtime: true
|
|
- os: windows
|
|
runner: windows-2022
|
|
arch: x64
|
|
embedded_mpv_platform: win32
|
|
embedded_mpv_arch: x64
|
|
embedded_mpv_build_runtime: false
|
|
|
|
steps: &electron-build-steps
|
|
- name: Checkout code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Install pnpm
|
|
uses: pnpm/action-setup@v4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '22'
|
|
cache: 'pnpm'
|
|
|
|
- name: Install Linux system dependencies
|
|
if: matrix.os == 'linux'
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install --no-install-recommends -y \
|
|
appstream \
|
|
binutils \
|
|
dbus-daemon \
|
|
flatpak \
|
|
flatpak-builder \
|
|
libarchive-tools \
|
|
libegl-dev \
|
|
libgbm-dev \
|
|
libgl-dev \
|
|
libx11-dev \
|
|
libxext-dev \
|
|
mpv \
|
|
pkg-config \
|
|
rpm \
|
|
snapd \
|
|
squashfs-tools \
|
|
xauth \
|
|
xvfb
|
|
|
|
- name: Configure Flatpak build runtime
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'flatpak'
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo
|
|
flatpak install --user -y flathub org.freedesktop.Platform//24.08 org.freedesktop.Sdk//24.08
|
|
|
|
- name: Select Linux packaging targets for CI profile
|
|
if: matrix.os == 'linux'
|
|
run: |
|
|
cp electron-builder.json "${RUNNER_TEMP}/electron-builder.base.json"
|
|
node tools/packaging/configure-linux-frame-copy-build.mjs --profile "${{ matrix.linux_profile }}"
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Download pinned Linux Embedded MPV runtime
|
|
if: matrix.os == 'linux'
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: linux-embedded-mpv-runtime
|
|
path: vendor/embedded-mpv/linux-x64
|
|
|
|
- name: Inject TMDB API key
|
|
# No-op when the secret is unavailable (e.g. fork PRs) — the
|
|
# app then requires a user-provided key for TMDB enrichment.
|
|
env:
|
|
TMDB_API_KEY: ${{ secrets.TMDB_API_KEY }}
|
|
run: node tools/tmdb/inject-tmdb-key.mjs
|
|
|
|
- name: Build frontend
|
|
run: pnpm nx build web --skip-nx-cache
|
|
|
|
- name: Resolve embedded MPV runtime cache key
|
|
# TEMPORARY ARTIFACT TEST: remove `|| github.event_name == 'pull_request' || github.ref == 'refs/heads/master'`
|
|
# after the macOS Embedded MPV artifacts are built and manually tested.
|
|
if: matrix.embedded_mpv_platform && matrix.os != 'linux' && !startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'pull_request' || github.ref == 'refs/heads/master')
|
|
id: embedded-mpv-runtime-cache-key
|
|
shell: bash
|
|
env:
|
|
IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256: ${{ vars.IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 || secrets.IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 || '' }}
|
|
IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256: 6014aa0e6d8e98cdba90f5288295a7105d7d14ab0ca906f51465eeb478d5fea0
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
node <<'NODE'
|
|
const childProcess = require('child_process');
|
|
const crypto = require('crypto');
|
|
const fs = require('fs');
|
|
|
|
const hash = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
|
const targetPlatform = '${{ matrix.embedded_mpv_platform }}';
|
|
const targetArch = '${{ matrix.embedded_mpv_arch }}';
|
|
const sourceHashFiles = [
|
|
'tools/embedded-mpv/stage-runtime.mjs',
|
|
];
|
|
const cacheKeyParts = [
|
|
'embedded-mpv-runtime-v2',
|
|
targetPlatform,
|
|
targetArch,
|
|
process.env.RUNNER_OS,
|
|
];
|
|
let deploymentTarget = '';
|
|
let xcodeVersion = 'none';
|
|
if (targetPlatform === 'darwin') {
|
|
deploymentTarget = process.env.MACOSX_DEPLOYMENT_TARGET || '11.0';
|
|
const safeDeploymentTarget = deploymentTarget.replace(/[^A-Za-z0-9_.-]/g, '-');
|
|
sourceHashFiles.push(
|
|
'tools/embedded-mpv/build-macos-runtime.mjs',
|
|
'tools/embedded-mpv/stage-macos-runtime.mjs'
|
|
);
|
|
try {
|
|
xcodeVersion = childProcess
|
|
.execSync('xcodebuild -version', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
|
|
.replace(/\s+$/g, '')
|
|
.replace(/\n/g, ' ');
|
|
} catch {
|
|
xcodeVersion = 'none';
|
|
}
|
|
cacheKeyParts.push(
|
|
`macos${safeDeploymentTarget}`,
|
|
`xcode${hash(xcodeVersion)}`
|
|
);
|
|
} else if (targetPlatform === 'win32') {
|
|
sourceHashFiles.push('tools/embedded-mpv/stage-windows-runtime-archive.mjs');
|
|
const windowsRuntimeSha256 =
|
|
process.env.IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 ||
|
|
process.env.IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 ||
|
|
'missing';
|
|
cacheKeyParts.push(
|
|
`runtime${hash(windowsRuntimeSha256)}`
|
|
);
|
|
}
|
|
const sourceHash = hash(
|
|
sourceHashFiles.map((filePath) => fs.readFileSync(filePath)).join('\n')
|
|
);
|
|
cacheKeyParts.push(sourceHash);
|
|
const cacheKey = cacheKeyParts.join('-');
|
|
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `deployment-target=${deploymentTarget}\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `xcode-version=${xcodeVersion}\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `key=${cacheKey}\n`);
|
|
console.log(`Embedded MPV runtime cache key: ${cacheKey}`);
|
|
console.log(`Xcode version: ${xcodeVersion}`);
|
|
NODE
|
|
|
|
- name: Restore embedded MPV runtime cache
|
|
# TEMPORARY ARTIFACT TEST: remove `|| github.event_name == 'pull_request' || github.ref == 'refs/heads/master'`
|
|
# after the macOS Embedded MPV artifacts are built and manually tested.
|
|
if: matrix.embedded_mpv_platform && matrix.os != 'linux' && !startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'pull_request' || github.ref == 'refs/heads/master')
|
|
id: embedded-mpv-runtime-cache
|
|
uses: actions/cache/restore@v4
|
|
with:
|
|
path: |
|
|
vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/include
|
|
vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/lib
|
|
vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/runtime-manifest.json
|
|
key: ${{ steps.embedded-mpv-runtime-cache-key.outputs.key }}
|
|
|
|
- name: Clear stale embedded MPV runtime files
|
|
# TEMPORARY ARTIFACT TEST: remove `|| github.event_name == 'pull_request' || github.ref == 'refs/heads/master'`
|
|
# after the macOS Embedded MPV artifacts are built and manually tested.
|
|
if: matrix.embedded_mpv_platform && matrix.os != 'linux' && !startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'pull_request' || github.ref == 'refs/heads/master') && steps.embedded-mpv-runtime-cache.outputs.cache-hit != 'true'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
rm -rf \
|
|
"vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/include" \
|
|
"vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/lib" \
|
|
"vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/runtime-manifest.json"
|
|
|
|
- name: Build embedded MPV runtime (macOS release)
|
|
# TEMPORARY ARTIFACT TEST: remove `|| github.event_name == 'pull_request' || github.ref == 'refs/heads/master'`
|
|
# after the macOS Embedded MPV artifacts are built and manually tested.
|
|
if: matrix.embedded_mpv_build_runtime && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.ref == 'refs/heads/master') && steps.embedded-mpv-runtime-cache.outputs.cache-hit != 'true'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
brew install meson ninja pkg-config nasm autoconf automake libtool xz
|
|
|
|
RUNTIME_PREFIX="${RUNNER_TEMP}/embedded-mpv-runtime/${{ matrix.embedded_mpv_arch }}/prefix"
|
|
pnpm embedded-mpv:build-runtime -- "${{ matrix.embedded_mpv_arch }}" "${RUNTIME_PREFIX}"
|
|
pnpm embedded-mpv:stage-runtime -- "${{ matrix.embedded_mpv_platform }}" "${{ matrix.embedded_mpv_arch }}" "${RUNTIME_PREFIX}"
|
|
|
|
- name: Stage Windows embedded MPV runtime archive
|
|
if: matrix.os == 'windows' && steps.embedded-mpv-runtime-cache.outputs.cache-hit != 'true'
|
|
shell: bash
|
|
env:
|
|
IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_URL: ${{ vars.IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_URL || secrets.IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_URL || '' }}
|
|
IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256: ${{ vars.IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 || secrets.IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 || '' }}
|
|
IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_URL: https://github.com/zhongfly/mpv-winbuild/releases/download/2026-07-17-94335ab87a/mpv-dev-lgpl-x86_64-20260717-git-94335ab87a.7z
|
|
IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256: 6014aa0e6d8e98cdba90f5288295a7105d7d14ab0ca906f51465eeb478d5fea0
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
WINDOWS_RUNTIME_URL="${IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_URL}"
|
|
WINDOWS_RUNTIME_SHA256="${IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256}"
|
|
if [ -z "${WINDOWS_RUNTIME_URL}" ] && [ -z "${WINDOWS_RUNTIME_SHA256}" ]; then
|
|
case "${GITHUB_REF}" in
|
|
refs/tags/v*)
|
|
;;
|
|
*)
|
|
WINDOWS_RUNTIME_URL="${IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_URL}"
|
|
WINDOWS_RUNTIME_SHA256="${IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256}"
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
if [ -z "${WINDOWS_RUNTIME_URL}" ] || [ -z "${WINDOWS_RUNTIME_SHA256}" ]; then
|
|
echo "::error::Windows Embedded MPV CI requires IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_URL and IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 repository variables or secrets."
|
|
exit 1
|
|
fi
|
|
|
|
pnpm embedded-mpv:stage-runtime:windows-archive -- "${WINDOWS_RUNTIME_URL}" "${WINDOWS_RUNTIME_SHA256}"
|
|
|
|
- name: Build backend
|
|
env:
|
|
IPTVNATOR_EMBEDDED_MPV_PLATFORM: ${{ matrix.embedded_mpv_platform || '' }}
|
|
IPTVNATOR_EMBEDDED_MPV_ARCH: ${{ matrix.embedded_mpv_arch || matrix.arch || '' }}
|
|
IPTVNATOR_LINUX_FRAME_COPY_PROFILE: ${{ matrix.linux_profile || '' }}
|
|
IPTVNATOR_REQUIRE_EMBEDDED_MPV: ${{ (matrix.os == 'linux' || matrix.os == 'windows' || (matrix.os == 'macos' && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.ref == 'refs/heads/master'))) && '1' || '0' }}
|
|
run: pnpm run build:backend
|
|
|
|
- name: Verify embedded MPV build output
|
|
# TEMPORARY ARTIFACT TEST: remove `|| github.event_name == 'pull_request' || github.ref == 'refs/heads/master'`
|
|
# after the macOS Embedded MPV artifacts are built and manually tested.
|
|
if: matrix.os == 'linux' || matrix.os == 'windows' || (matrix.os == 'macos' && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.ref == 'refs/heads/master'))
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
echo "::group::Native build output"
|
|
find apps/electron-backend/native/build/Release -maxdepth 3 -type f | sort
|
|
echo "::endgroup::"
|
|
|
|
echo "::group::Dist native output"
|
|
find dist/apps/electron-backend/native -maxdepth 3 -type f | sort
|
|
echo "::endgroup::"
|
|
|
|
test -f apps/electron-backend/native/build/Release/embedded_mpv.node
|
|
test -f dist/apps/electron-backend/native/embedded_mpv.node
|
|
test -f dist/apps/electron-backend/native/embedded-mpv-runtime.json
|
|
case "${{ matrix.embedded_mpv_platform }}" in
|
|
darwin)
|
|
test -f dist/apps/electron-backend/native/lib/libmpv.2.dylib || test -f dist/apps/electron-backend/native/lib/libmpv.dylib
|
|
;;
|
|
win32)
|
|
test -f dist/apps/electron-backend/native/lib/mpv-2.dll || test -f dist/apps/electron-backend/native/lib/libmpv-2.dll || test -f dist/apps/electron-backend/native/lib/mpv.dll || test -f dist/apps/electron-backend/native/lib/libmpv.dll
|
|
# Frame-copy engine artifacts ship on Windows. The helper
|
|
# resolves the mpv DLL from its own directory, so the DLL
|
|
# must sit beside it at the native/ top level too.
|
|
test -f dist/apps/electron-backend/native/iptvnator_mpv_helper.exe
|
|
test -f dist/apps/electron-backend/native/embedded_mpv_frame_reader.node
|
|
find dist/apps/electron-backend/native -maxdepth 1 \( -name 'mpv-2.dll' -o -name 'libmpv-2.dll' -o -name 'mpv.dll' -o -name 'libmpv.dll' \) -print -quit | grep -q .
|
|
;;
|
|
linux)
|
|
node -e "const { execFileSync } = require('node:child_process'); const manifest = require('./dist/apps/electron-backend/native/embedded-mpv-runtime.json'); const revision = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(); if (manifest.origin !== 'linux-frame-copy-build' || manifest.sourceRuntimeValidated !== true || manifest.sourceArchive?.schemaVersion !== 1 || manifest.sourceArchive?.name !== 'linux-frame-copy-runtime-sources.tar.xz' || !/^[a-f0-9]{64}$/.test(manifest.sourceArchive?.sha256 ?? '') || manifest.sourceArchive?.repositoryRevision !== revision) { throw new Error('Linux embedded MPV build manifest must describe the validated source runtime and exact source archive.'); }"
|
|
test -f dist/apps/electron-backend/native/lib/libmpv.so.2
|
|
test -f dist/apps/electron-backend/native/iptvnator_mpv_helper
|
|
test -f dist/apps/electron-backend/native/embedded_mpv_frame_reader.node
|
|
if readelf -d dist/apps/electron-backend/native/embedded_mpv.node | grep -Eq 'Shared library:.*libmpv\.so'; then
|
|
echo "::error::Linux embedded MPV addon must not link directly to libmpv"
|
|
readelf -d dist/apps/electron-backend/native/embedded_mpv.node
|
|
exit 1
|
|
fi
|
|
if readelf -d dist/apps/electron-backend/native/embedded_mpv_frame_reader.node | grep -Eq 'Shared library:.*libmpv\.so'; then
|
|
echo "::error::Linux frame reader must not link directly to libmpv"
|
|
readelf -d dist/apps/electron-backend/native/embedded_mpv_frame_reader.node
|
|
exit 1
|
|
fi
|
|
HELPER_DYNAMIC="$(readelf -d dist/apps/electron-backend/native/iptvnator_mpv_helper)"
|
|
if ! printf '%s\n' "${HELPER_DYNAMIC}" | grep -Eq 'Shared library: \[libmpv\.so\.2\]'; then
|
|
echo "::error::Linux frame-copy helper must need libmpv.so.2"
|
|
printf '%s\n' "${HELPER_DYNAMIC}"
|
|
exit 1
|
|
fi
|
|
if ! printf '%s\n' "${HELPER_DYNAMIC}" | grep -Fq 'Library runpath: [$ORIGIN/lib]'; then
|
|
echo "::error::Linux frame-copy helper must keep only the relative runtime path"
|
|
printf '%s\n' "${HELPER_DYNAMIC}"
|
|
exit 1
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
- name: Validate AppStream metadata
|
|
if: matrix.os == 'linux'
|
|
run: appstreamcli validate --pedantic --no-net apps/electron-backend/linux/com.fourgray.iptvnator.metainfo.xml
|
|
|
|
- name: Build website
|
|
if: matrix.os == 'linux'
|
|
run: pnpm nx build website --skip-nx-cache
|
|
|
|
- name: Verify AppStream website assets
|
|
if: matrix.os == 'linux'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
for screenshot in player playlists epg settings; do
|
|
if ! find dist/apps/website -path "*/appstream/${screenshot}.png" -print -quit | grep -q .; then
|
|
echo "::error::Missing AppStream website asset for ${screenshot}.png"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
- name: Override macOS arch in electron-builder.json
|
|
if: matrix.os == 'macos'
|
|
run: |
|
|
# Replace the mac arch array with just the target architecture
|
|
node -e "
|
|
const fs = require('fs');
|
|
const pkg = JSON.parse(fs.readFileSync('electron-builder.json', 'utf8'));
|
|
const targets = Array.isArray(pkg.mac?.target) ? pkg.mac.target : [pkg.mac.target];
|
|
for (const target of targets) {
|
|
if (target && typeof target === 'object') {
|
|
target.arch = ['${{ matrix.arch }}'];
|
|
}
|
|
}
|
|
fs.writeFileSync('electron-builder.json', JSON.stringify(pkg, null, 4));
|
|
"
|
|
|
|
- name: Override Windows arch in electron-builder.json
|
|
if: matrix.os == 'windows'
|
|
run: |
|
|
node -e "
|
|
const fs = require('fs');
|
|
const pkg = JSON.parse(fs.readFileSync('electron-builder.json', 'utf8'));
|
|
const targets = Array.isArray(pkg.win?.target) ? pkg.win.target : [];
|
|
for (const target of targets) {
|
|
if (target && typeof target === 'object') {
|
|
target.arch = ['${{ matrix.embedded_mpv_arch }}'];
|
|
}
|
|
}
|
|
fs.writeFileSync('electron-builder.json', JSON.stringify(pkg, null, 4) + '\n');
|
|
"
|
|
|
|
- name: Validate macOS signing configuration
|
|
if: matrix.os == 'macos' && github.event_name != 'pull_request'
|
|
shell: bash
|
|
env:
|
|
CSC_NAME: ${{ vars.CSC_NAME }}
|
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
|
APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY }}
|
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
|
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
run: |
|
|
if [ -z "${CSC_NAME}" ]; then
|
|
echo "::error::Missing CSC_NAME repository variable for deterministic macOS code signing."
|
|
exit 1
|
|
fi
|
|
|
|
if [ -z "${CSC_LINK}" ] || [ -z "${CSC_KEY_PASSWORD}" ]; then
|
|
echo "::error::Missing CSC_LINK or CSC_KEY_PASSWORD secret for macOS code signing."
|
|
exit 1
|
|
fi
|
|
|
|
has_api_key_credentials=false
|
|
if [ -n "${APPLE_API_KEY_CONTENT}" ] || [ -n "${APPLE_API_KEY_ID}" ] || [ -n "${APPLE_API_ISSUER}" ]; then
|
|
if [ -z "${APPLE_API_KEY_CONTENT}" ] || [ -z "${APPLE_API_KEY_ID}" ] || [ -z "${APPLE_API_ISSUER}" ]; then
|
|
echo "::error::APPLE_API_KEY, APPLE_API_KEY_ID, and APPLE_API_ISSUER must all be set for App Store Connect API key notarization."
|
|
exit 1
|
|
fi
|
|
has_api_key_credentials=true
|
|
fi
|
|
|
|
has_apple_id_credentials=false
|
|
if [ -n "${APPLE_ID}" ] || [ -n "${APPLE_APP_SPECIFIC_PASSWORD}" ] || [ -n "${APPLE_TEAM_ID}" ]; then
|
|
if [ -z "${APPLE_ID}" ] || [ -z "${APPLE_APP_SPECIFIC_PASSWORD}" ] || [ -z "${APPLE_TEAM_ID}" ]; then
|
|
echo "::error::APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, and APPLE_TEAM_ID must all be set for Apple ID notarization."
|
|
exit 1
|
|
fi
|
|
has_apple_id_credentials=true
|
|
fi
|
|
|
|
if [ "${has_api_key_credentials}" = false ] && [ "${has_apple_id_credentials}" = false ]; then
|
|
echo "::error::Missing notarization credentials. Configure either APPLE_API_KEY + APPLE_API_KEY_ID + APPLE_API_ISSUER, or APPLE_ID + APPLE_APP_SPECIFIC_PASSWORD + APPLE_TEAM_ID."
|
|
exit 1
|
|
fi
|
|
|
|
- name: Prepare macOS notarization credentials
|
|
if: matrix.os == 'macos' && github.event_name != 'pull_request'
|
|
shell: bash
|
|
env:
|
|
APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY }}
|
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
|
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
run: |
|
|
if [ -n "${APPLE_API_KEY_CONTENT}" ]; then
|
|
APPLE_API_KEY_PATH="${RUNNER_TEMP}/AuthKey_${APPLE_API_KEY_ID}.p8"
|
|
printf '%s' "${APPLE_API_KEY_CONTENT}" > "${APPLE_API_KEY_PATH}"
|
|
chmod 600 "${APPLE_API_KEY_PATH}"
|
|
echo "APPLE_API_KEY=${APPLE_API_KEY_PATH}" >> "${GITHUB_ENV}"
|
|
echo "APPLE_API_KEY_ID=${APPLE_API_KEY_ID}" >> "${GITHUB_ENV}"
|
|
echo "APPLE_API_ISSUER=${APPLE_API_ISSUER}" >> "${GITHUB_ENV}"
|
|
echo "APPLE_ID=" >> "${GITHUB_ENV}"
|
|
echo "APPLE_APP_SPECIFIC_PASSWORD=" >> "${GITHUB_ENV}"
|
|
echo "APPLE_TEAM_ID=" >> "${GITHUB_ENV}"
|
|
exit 0
|
|
fi
|
|
|
|
if [ -n "${APPLE_ID}" ]; then
|
|
echo "APPLE_API_KEY=" >> "${GITHUB_ENV}"
|
|
echo "APPLE_API_KEY_ID=" >> "${GITHUB_ENV}"
|
|
echo "APPLE_API_ISSUER=" >> "${GITHUB_ENV}"
|
|
echo "APPLE_ID=${APPLE_ID}" >> "${GITHUB_ENV}"
|
|
echo "APPLE_APP_SPECIFIC_PASSWORD=${APPLE_APP_SPECIFIC_PASSWORD}" >> "${GITHUB_ENV}"
|
|
echo "APPLE_TEAM_ID=${APPLE_TEAM_ID}" >> "${GITHUB_ENV}"
|
|
fi
|
|
|
|
- name: Make Electron app (macOS)
|
|
if: matrix.os == 'macos' && github.event_name != 'pull_request'
|
|
env:
|
|
CSC_NAME: ${{ vars.CSC_NAME }}
|
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
|
DEBUG: electron-builder,electron-notarize*
|
|
IPTVNATOR_EMBEDDED_MPV_PLATFORM: ${{ matrix.embedded_mpv_platform }}
|
|
IPTVNATOR_EMBEDDED_MPV_ARCH: ${{ matrix.embedded_mpv_arch }}
|
|
# TEMPORARY MASTER ARTIFACT TEST: remove `|| github.ref == 'refs/heads/master'`
|
|
# after the macOS Embedded MPV artifacts are built and manually tested.
|
|
IPTVNATOR_REQUIRE_EMBEDDED_MPV: ${{ (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master') && '1' || '0' }}
|
|
run: pnpm run make:app -- --publishPolicy=never
|
|
|
|
- name: Verify signed macOS app
|
|
if: matrix.os == 'macos' && github.event_name != 'pull_request'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
case "${{ matrix.arch }}" in
|
|
x64)
|
|
APP_PATH="dist/executables/mac/IPTVnator.app"
|
|
;;
|
|
arm64)
|
|
APP_PATH="dist/executables/mac-arm64/IPTVnator.app"
|
|
;;
|
|
*)
|
|
echo "::error::Unsupported macOS arch: ${{ matrix.arch }}"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
print_debug_attrs() {
|
|
echo "::group::Extended attributes"
|
|
xattr -lr "${APP_PATH}" | sed -n '1,120p' || true
|
|
echo "::endgroup::"
|
|
}
|
|
|
|
trap print_debug_attrs ERR
|
|
|
|
if [ ! -d "${APP_PATH}" ]; then
|
|
echo "::error::Expected app bundle not found at ${APP_PATH}"
|
|
exit 1
|
|
fi
|
|
|
|
SIGNATURE_INFO="$(codesign -dv --verbose=4 "${APP_PATH}" 2>&1)"
|
|
printf '%s\n' "${SIGNATURE_INFO}"
|
|
|
|
if printf '%s\n' "${SIGNATURE_INFO}" | grep -q 'Signature=adhoc'; then
|
|
echo "::error::macOS app is still ad-hoc signed."
|
|
exit 1
|
|
fi
|
|
|
|
if printf '%s\n' "${SIGNATURE_INFO}" | grep -q 'TeamIdentifier=not set'; then
|
|
echo "::error::macOS app is missing a TeamIdentifier."
|
|
exit 1
|
|
fi
|
|
|
|
codesign --verify --deep --strict --verbose=4 "${APP_PATH}"
|
|
spctl -a -vvv --type execute "${APP_PATH}"
|
|
xcrun stapler validate "${APP_PATH}"
|
|
|
|
- name: Make Electron app
|
|
if: matrix.os != 'macos' || github.event_name == 'pull_request'
|
|
env:
|
|
IPTVNATOR_EMBEDDED_MPV_PLATFORM: ${{ matrix.embedded_mpv_platform || '' }}
|
|
IPTVNATOR_EMBEDDED_MPV_ARCH: ${{ matrix.embedded_mpv_arch || matrix.arch || '' }}
|
|
IPTVNATOR_LINUX_FRAME_COPY_PROFILE: ${{ matrix.linux_profile || '' }}
|
|
# TEMPORARY PR TEST: change this back to '0' after manually
|
|
# testing the macOS PR artifact with Embedded MPV included.
|
|
IPTVNATOR_REQUIRE_EMBEDDED_MPV: ${{ (matrix.os == 'linux' || matrix.os == 'windows' || (matrix.os == 'macos' && github.event_name == 'pull_request')) && '1' || '0' }}
|
|
run: pnpm run make:app -- --publishPolicy=never
|
|
|
|
- name: Verify packaged worker layout
|
|
shell: bash
|
|
env:
|
|
PACKAGE_OS: ${{ matrix.os }}
|
|
PACKAGE_ARCH: ${{ matrix.arch || matrix.embedded_mpv_arch || '' }}
|
|
IPTVNATOR_LINUX_FRAME_COPY_PROFILE: ${{ matrix.linux_profile || '' }}
|
|
# TEMPORARY ARTIFACT TEST: remove `|| github.event_name == 'pull_request' || github.ref == 'refs/heads/master'`
|
|
# after the macOS Embedded MPV artifacts are built and manually tested.
|
|
IPTVNATOR_REQUIRE_EMBEDDED_MPV: ${{ (matrix.os == 'linux' || matrix.os == 'windows' || (matrix.os == 'macos' && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.ref == 'refs/heads/master'))) && '1' || '0' }}
|
|
run: pnpm run verify:package-layout -- "$PACKAGE_OS" "$PACKAGE_ARCH"
|
|
|
|
- name: Make marker-only foreign-architecture DEB packages
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'system'
|
|
shell: bash
|
|
env:
|
|
IPTVNATOR_EMBEDDED_MPV_PLATFORM: linux
|
|
IPTVNATOR_EMBEDDED_MPV_ARCH: x64
|
|
IPTVNATOR_LINUX_FRAME_COPY_PROFILE: ''
|
|
IPTVNATOR_REQUIRE_EMBEDDED_MPV: '1'
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
for foreign_arch in armv7l arm64; do
|
|
rm -rf dist/executables-linux-foreign
|
|
cp "${RUNNER_TEMP}/electron-builder.base.json" electron-builder.json
|
|
node tools/packaging/configure-linux-frame-copy-build.mjs \
|
|
--foreign-deb \
|
|
--foreign-arch "${foreign_arch}"
|
|
pnpm nx run electron-backend:make \
|
|
--arch="${foreign_arch}" \
|
|
--outputPath=dist/executables-linux-foreign \
|
|
--publishPolicy=never
|
|
mapfile -t foreign_debs < <(
|
|
find dist/executables-linux-foreign -maxdepth 1 -type f -name '*.deb' -print
|
|
)
|
|
test "${#foreign_debs[@]}" -eq 1
|
|
case "${foreign_arch}" in
|
|
armv7l) expected_deb_arch=armhf ;;
|
|
arm64) expected_deb_arch=arm64 ;;
|
|
*)
|
|
echo "::error::Unexpected foreign DEB build architecture ${foreign_arch}"
|
|
exit 1
|
|
;;
|
|
esac
|
|
actual_deb_arch="$(dpkg-deb --field "${foreign_debs[0]}" Architecture)"
|
|
test "${actual_deb_arch}" = "${expected_deb_arch}"
|
|
mv "${foreign_debs[0]}" dist/executables/
|
|
done
|
|
|
|
- name: Verify DEB payloads and x64 system runtime
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'system'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
found=false
|
|
for artifact in dist/executables/*.deb; do
|
|
test -f "${artifact}" || continue
|
|
found=true
|
|
case "$(dpkg-deb --field "${artifact}" Architecture)" in
|
|
amd64)
|
|
docker run --rm \
|
|
--volume "${GITHUB_WORKSPACE}:/workspace:ro" \
|
|
--volume "$(realpath "${artifact}"):/artifact.deb:ro" \
|
|
--workdir /workspace \
|
|
ubuntu:24.04 \
|
|
bash -euo pipefail -c '
|
|
apt-get update
|
|
DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \
|
|
binutils libegl1 libgbm1 libgl1 libgl1-mesa-dri libmpv2 \
|
|
nodejs squashfs-tools xauth xvfb
|
|
xvfb-run -a env LIBGL_ALWAYS_SOFTWARE=1 \
|
|
node tools/packaging/verify-linux-frame-copy-runtime.mjs \
|
|
--artifact /artifact.deb --profile system
|
|
'
|
|
;;
|
|
arm64|armhf)
|
|
node tools/packaging/verify-linux-frame-copy-runtime.mjs \
|
|
--artifact "${artifact}" --profile system
|
|
;;
|
|
*)
|
|
echo "::error::Unexpected DEB architecture in ${artifact}"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
test "${found}" = true
|
|
|
|
- name: Verify RPM payload and x64 system runtime
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'system'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
artifact="$(find dist/executables -maxdepth 1 -type f -name '*.rpm' -print -quit)"
|
|
test -n "${artifact}"
|
|
docker run --rm \
|
|
--volume "${GITHUB_WORKSPACE}:/workspace:ro" \
|
|
--volume "$(realpath "${artifact}"):/artifact.rpm:ro" \
|
|
--workdir /workspace \
|
|
fedora:latest \
|
|
bash -euo pipefail -c '
|
|
dnf install -y \
|
|
binutils bsdtar libglvnd-egl libglvnd-glx mesa-dri-drivers \
|
|
mesa-libgbm mpv-libs nodejs rpm xorg-x11-server-Xvfb \
|
|
xorg-x11-xauth
|
|
xvfb-run -a env LIBGL_ALWAYS_SOFTWARE=1 \
|
|
node tools/packaging/verify-linux-frame-copy-runtime.mjs \
|
|
--artifact /artifact.rpm --profile system
|
|
'
|
|
|
|
- name: Verify Pacman payload and x64 system runtime
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'system'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
artifact="$(find dist/executables -maxdepth 1 -type f \( -name '*.pacman' -o -name '*.pkg.tar.*' \) -print -quit)"
|
|
test -n "${artifact}"
|
|
docker run --rm \
|
|
--volume "${GITHUB_WORKSPACE}:/workspace:ro" \
|
|
--volume "$(realpath "${artifact}"):/artifact.pacman:ro" \
|
|
--workdir /workspace \
|
|
archlinux:latest \
|
|
bash -euo pipefail -c '
|
|
pacman -Syu --noconfirm \
|
|
binutils libarchive libglvnd mesa mpv nodejs xorg-server-xvfb \
|
|
xorg-xauth
|
|
xvfb-run -a env LIBGL_ALWAYS_SOFTWARE=1 \
|
|
node tools/packaging/verify-linux-frame-copy-runtime.mjs \
|
|
--artifact /artifact.pacman --profile system
|
|
'
|
|
|
|
- name: Verify AppImage payloads and bundled runtime
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'portable'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
found=false
|
|
for artifact in dist/executables/*.AppImage; do
|
|
test -f "${artifact}" || continue
|
|
found=true
|
|
xvfb-run -a env LIBGL_ALWAYS_SOFTWARE=1 \
|
|
node tools/packaging/verify-linux-frame-copy-runtime.mjs \
|
|
--artifact "${artifact}" --profile portable
|
|
done
|
|
test "${found}" = true
|
|
|
|
- name: Verify Snap payloads and strict-confinement runtime
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'portable'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
found=false
|
|
installed_x64=false
|
|
for artifact in dist/executables/*.snap; do
|
|
test -f "${artifact}" || continue
|
|
found=true
|
|
verification="$(
|
|
xvfb-run -a env LIBGL_ALWAYS_SOFTWARE=1 \
|
|
node tools/packaging/verify-linux-frame-copy-runtime.mjs \
|
|
--artifact "${artifact}" --profile portable \
|
|
2>&1 | tee /dev/stderr
|
|
)"
|
|
if printf '%s\n' "${verification}" | grep -Fq 'Verified snap x64 Linux'; then
|
|
snap list mesa-core22 >/dev/null 2>&1 || sudo snap install mesa-core22
|
|
snap list gnome-3-28-1804 >/dev/null 2>&1 || sudo snap install gnome-3-28-1804
|
|
sudo snap install --dangerous "${artifact}"
|
|
installed_x64=true
|
|
fi
|
|
done
|
|
test "${found}" = true
|
|
test "${installed_x64}" = true
|
|
sudo snap connect iptvnator:graphics-core22 mesa-core22:graphics-core22
|
|
sudo snap connect iptvnator:gnome-3-28-1804 gnome-3-28-1804:gnome-3-28-1804
|
|
sudo snap disconnect iptvnator:graphics-core22 mesa-core22:graphics-core22
|
|
snap connections iptvnator | awk \
|
|
'$2 == "iptvnator:graphics-core22" && $3 == "-" { found=1 } END { exit !found }'
|
|
set +e
|
|
disconnected_probe="$(
|
|
xvfb-run -a env LIBGL_ALWAYS_SOFTWARE=1 \
|
|
snap run iptvnator --embedded-mpv-runtime-probe 2>&1
|
|
)"
|
|
disconnected_status=$?
|
|
set -e
|
|
printf '%s\n' "${disconnected_probe}"
|
|
test "${disconnected_status}" -eq 1
|
|
printf '%s\n' "${disconnected_probe}" | \
|
|
grep -Fx '{"usable":false,"reason":"snap-graphics-provider-unavailable"}'
|
|
sudo snap connect iptvnator:graphics-core22 mesa-core22:graphics-core22
|
|
snap connections iptvnator | awk \
|
|
'$2 == "iptvnator:graphics-core22" && $3 == "mesa-core22:graphics-core22" { found=1 } END { exit !found }'
|
|
snap connections iptvnator | awk \
|
|
'$2 == "iptvnator:gnome-3-28-1804" && $3 == "gnome-3-28-1804:gnome-3-28-1804" { found=1 } END { exit !found }'
|
|
snap connections iptvnator | awk \
|
|
'$1 == "shared-memory" && $2 == "iptvnator:shared-memory" && $3 == ":shared-memory" { found=1 } END { exit !found }'
|
|
xvfb-run -a env \
|
|
LIBGL_ALWAYS_SOFTWARE=1 \
|
|
IPTVNATOR_TRACE_PLAYER=1 \
|
|
EGL_LOG_LEVEL=debug \
|
|
LIBGL_DEBUG=verbose \
|
|
__EGL_VENDOR_LIBRARY_FILENAMES=/tmp/hostile-egl-vendor.json \
|
|
GBM_BACKEND=/tmp/hostile-gbm \
|
|
MESA_LOADER_DRIVER_OVERRIDE=/tmp/hostile-dri \
|
|
LIBVA_DRIVER_NAME=/tmp/hostile-va \
|
|
VDPAU_DRIVER_PATH=/tmp/hostile-vdpau \
|
|
VK_DRIVER_FILES=/tmp/hostile-vulkan-driver.json \
|
|
VK_ICD_FILENAMES=/tmp/hostile-vulkan-icd.json \
|
|
VK_ADD_DRIVER_FILES=/tmp/hostile-vulkan-add-driver.json \
|
|
VK_ADD_LAYER_PATH=/tmp/hostile-vulkan-layers \
|
|
VK_IMPLICIT_LAYER_PATH=/tmp/hostile-vulkan-implicit-layers \
|
|
VK_ADD_IMPLICIT_LAYER_PATH=/tmp/hostile-vulkan-add-implicit-layers \
|
|
XDG_CONFIG_HOME=/tmp/hostile-xdg-config-home \
|
|
XDG_CONFIG_DIRS=/tmp/hostile-xdg-config-dirs \
|
|
XDG_DATA_HOME=/tmp/hostile-xdg-data-home \
|
|
XDG_DATA_DIRS=/tmp/hostile-xdg-data-dirs \
|
|
snap run iptvnator --embedded-mpv-runtime-probe
|
|
|
|
- name: Run packaged x64 frame-copy and fallback smoke
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'portable'
|
|
env:
|
|
IPTVNATOR_E2E_REQUIRE_PACKAGED_FRAME_COPY: '1'
|
|
IPTVNATOR_E2E_PACKAGED_EXECUTABLE: ${{ github.workspace }}/dist/executables/linux-unpacked/iptvnator
|
|
LIBGL_ALWAYS_SOFTWARE: '1'
|
|
run: |
|
|
xvfb-run -a pnpm nx run \
|
|
electron-backend-e2e:packaged-frame-copy-smoke \
|
|
--skip-nx-cache
|
|
|
|
- name: Diagnose packaged x64 frame-copy hardware path
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'portable'
|
|
continue-on-error: true
|
|
env:
|
|
IPTVNATOR_E2E_REQUIRE_PACKAGED_FRAME_COPY: '1'
|
|
IPTVNATOR_E2E_PACKAGED_EXECUTABLE: ${{ github.workspace }}/dist/executables/linux-unpacked/iptvnator
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
if [ ! -e /dev/dri/renderD128 ]; then
|
|
echo "::notice::No /dev/dri/renderD128 is available; skipping the non-blocking hardware-path diagnostic."
|
|
exit 0
|
|
fi
|
|
ls -la /dev/dri
|
|
xvfb-run -a pnpm nx run \
|
|
electron-backend-e2e:packaged-frame-copy-smoke \
|
|
--skip-nx-cache
|
|
|
|
- name: Save embedded MPV runtime cache
|
|
# TEMPORARY ARTIFACT TEST: remove `|| github.ref == 'refs/heads/master'`
|
|
# after the macOS Embedded MPV artifacts are built and manually tested.
|
|
if: matrix.embedded_mpv_platform && matrix.os != 'linux' && !startsWith(github.ref, 'refs/tags/v') && github.repository == '4gray/iptvnator' && github.event_name != 'pull_request' && github.ref == 'refs/heads/master' && steps.embedded-mpv-runtime-cache.outputs.cache-hit != 'true'
|
|
uses: actions/cache/save@v4
|
|
with:
|
|
path: |
|
|
vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/include
|
|
vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/lib
|
|
vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/runtime-manifest.json
|
|
key: ${{ steps.embedded-mpv-runtime-cache-key.outputs.key }}
|
|
|
|
- name: Verify Flatpak payload, launcher, and sandboxed runtime
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'flatpak'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
FLATPAK_BUNDLE="$(find dist/executables -maxdepth 1 -name '*.flatpak' | head -n 1)"
|
|
if [ -z "${FLATPAK_BUNDLE}" ]; then
|
|
echo "::error::Flatpak bundle not found in dist/executables"
|
|
exit 1
|
|
fi
|
|
|
|
xvfb-run -a env LIBGL_ALWAYS_SOFTWARE=1 \
|
|
node tools/packaging/verify-linux-frame-copy-runtime.mjs \
|
|
--artifact "${FLATPAK_BUNDLE}" --profile flatpak
|
|
flatpak install --user --noninteractive -y "${FLATPAK_BUNDLE}"
|
|
xvfb-run -a env LIBGL_ALWAYS_SOFTWARE=1 \
|
|
flatpak run --command=sh com.fourgray.iptvnator -c '
|
|
set -euo pipefail
|
|
|
|
test -f /app/share/metainfo/com.fourgray.iptvnator.metainfo.xml
|
|
test -L /app/bin/iptvnator
|
|
|
|
LAUNCHER_PATH="$(readlink -f /app/bin/iptvnator)"
|
|
test -f "${LAUNCHER_PATH}"
|
|
test -x "${LAUNCHER_PATH}"
|
|
if [ -e "${LAUNCHER_PATH}.bin" ] || [ -L "${LAUNCHER_PATH}.bin" ]; then
|
|
echo "::error::Flatpak must not contain ${LAUNCHER_PATH}.bin"
|
|
exit 1
|
|
fi
|
|
ELF_MAGIC="$(od -An -tx1 -N4 "${LAUNCHER_PATH}" | tr -d "[:space:]")"
|
|
test "${ELF_MAGIC}" = "7f454c46"
|
|
'
|
|
set +e
|
|
PROBE_OUTPUT="$(
|
|
xvfb-run -a dbus-run-session -- flatpak run \
|
|
--env=LIBGL_ALWAYS_SOFTWARE=1 \
|
|
com.fourgray.iptvnator \
|
|
--embedded-mpv-runtime-probe 2>&1
|
|
)"
|
|
PROBE_STATUS=$?
|
|
set -e
|
|
|
|
PROBE_OUTPUT_LIMIT=16384
|
|
printf '%s\n' "${PROBE_OUTPUT:0:PROBE_OUTPUT_LIMIT}"
|
|
if [ "${#PROBE_OUTPUT}" -gt "${PROBE_OUTPUT_LIMIT}" ]; then
|
|
echo "::warning::Flatpak runtime probe output was truncated to ${PROBE_OUTPUT_LIMIT} characters."
|
|
fi
|
|
if [[ "${PROBE_OUTPUT}" == *"not an ELF file"* ]] ||
|
|
[[ "${PROBE_OUTPUT}" == *"Zypak needs to be called directly"* ]]; then
|
|
echo "::error::Flatpak launched a wrapper instead of the Electron ELF."
|
|
exit 1
|
|
fi
|
|
if [ "${PROBE_STATUS}" -ne 0 ]; then
|
|
echo "::error::Flatpak application runtime probe failed with status ${PROBE_STATUS}."
|
|
exit "${PROBE_STATUS}"
|
|
fi
|
|
|
|
- name: Upload artifacts (macOS)
|
|
if: matrix.os == 'macos'
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: macos-${{ matrix.arch }}-artifacts
|
|
path: |
|
|
dist/executables/**/*.dmg
|
|
dist/executables/**/*.zip
|
|
dist/executables/**/latest-mac.yml
|
|
dist/executables/**/*.blockmap
|
|
retention-days: 7
|
|
|
|
- name: Upload system-runtime Linux artifacts
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'system'
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: linux-system-artifacts
|
|
path: |
|
|
dist/executables/*.deb
|
|
dist/executables/*.rpm
|
|
dist/executables/*.pacman
|
|
dist/executables/*.pkg.tar.*
|
|
retention-days: 7
|
|
|
|
- name: Upload portable-runtime Linux artifacts
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'portable'
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: linux-portable-artifacts
|
|
path: |
|
|
dist/executables/*.AppImage
|
|
dist/executables/*.snap
|
|
dist/executables/**/latest-linux*.yml
|
|
dist/executables/**/*.blockmap
|
|
retention-days: 7
|
|
|
|
- name: Upload Flatpak-runtime Linux artifacts
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'flatpak'
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: linux-flatpak-artifacts
|
|
path: |
|
|
dist/executables/**/*.flatpak
|
|
retention-days: 7
|
|
|
|
- name: Upload artifacts (Windows)
|
|
if: matrix.os == 'windows'
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: windows-artifacts
|
|
path: |
|
|
dist/executables/**/*.exe
|
|
dist/executables/**/*.msi
|
|
dist/executables/**/*.zip
|
|
dist/executables/**/latest.yml
|
|
dist/executables/**/*.blockmap
|
|
retention-days: 7
|
|
|
|
build-linux:
|
|
name: Build on ${{ matrix.os }} ${{ matrix.arch }} (${{ matrix.linux_profile }})
|
|
needs: linux-embedded-mpv-runtime
|
|
runs-on: ${{ matrix.runner }}
|
|
timeout-minutes: 120
|
|
concurrency:
|
|
group: ${{ github.workflow }}-build-${{ matrix.os }}-${{ matrix.linux_profile }}-${{ github.event.pull_request.number || github.ref }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- os: linux
|
|
runner: ubuntu-22.04
|
|
arch: x64
|
|
linux_profile: system
|
|
embedded_mpv_platform: linux
|
|
embedded_mpv_arch: x64
|
|
embedded_mpv_build_runtime: false
|
|
- os: linux
|
|
runner: ubuntu-22.04
|
|
arch: x64
|
|
linux_profile: portable
|
|
embedded_mpv_platform: linux
|
|
embedded_mpv_arch: x64
|
|
embedded_mpv_build_runtime: false
|
|
- os: linux
|
|
runner: ubuntu-24.04
|
|
arch: x64
|
|
linux_profile: flatpak
|
|
embedded_mpv_platform: linux
|
|
embedded_mpv_arch: x64
|
|
embedded_mpv_build_runtime: false
|
|
|
|
steps: *electron-build-steps
|
|
|
|
create-release:
|
|
name: Create Draft Release
|
|
needs:
|
|
- build-cross-platform
|
|
- build-linux
|
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
|
|
runs-on: ubuntu-latest
|
|
concurrency:
|
|
group: ${{ github.workflow }}-release-${{ github.event.pull_request.number || github.ref }}
|
|
cancel-in-progress: false
|
|
permissions:
|
|
contents: write
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Download all artifacts
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
path: artifacts
|
|
|
|
- name: Display structure of downloaded files
|
|
run: ls -R artifacts
|
|
|
|
- name: Merge macOS updater metadata
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
node <<'NODE'
|
|
const fs = require('fs');
|
|
|
|
const candidates = [
|
|
'artifacts/macos-x64-artifacts/latest-mac.yml',
|
|
'artifacts/macos-arm64-artifacts/latest-mac.yml',
|
|
].filter((filePath) => fs.existsSync(filePath));
|
|
|
|
if (candidates.length === 0) {
|
|
console.log('No macOS update metadata found; skipping merge.');
|
|
process.exit(0);
|
|
}
|
|
|
|
function extractFilesBlock(text, filePath) {
|
|
const lines = text.split(/\r?\n/);
|
|
const start = lines.findIndex((line) => line === 'files:');
|
|
|
|
if (start === -1) {
|
|
throw new Error(`Missing files block in ${filePath}`);
|
|
}
|
|
|
|
const block = [];
|
|
|
|
for (let index = start + 1; index < lines.length; index += 1) {
|
|
const line = lines[index];
|
|
|
|
if (/^\S/.test(line) && line.trim() !== '') {
|
|
break;
|
|
}
|
|
|
|
if (line.trim() !== '') {
|
|
block.push(line);
|
|
}
|
|
}
|
|
|
|
return block;
|
|
}
|
|
|
|
function splitFileEntries(block) {
|
|
const entries = [];
|
|
let current = [];
|
|
|
|
for (const line of block) {
|
|
if (/^\s*-\s+url:/.test(line) && current.length > 0) {
|
|
entries.push(current);
|
|
current = [];
|
|
}
|
|
|
|
current.push(line);
|
|
}
|
|
|
|
if (current.length > 0) {
|
|
entries.push(current);
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
function replaceFilesBlock(text, mergedBlock) {
|
|
const lines = text.split(/\r?\n/);
|
|
const start = lines.findIndex((line) => line === 'files:');
|
|
let end = lines.length;
|
|
|
|
for (let index = start + 1; index < lines.length; index += 1) {
|
|
const line = lines[index];
|
|
|
|
if (/^\S/.test(line) && line.trim() !== '') {
|
|
end = index;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return [
|
|
...lines.slice(0, start + 1),
|
|
...mergedBlock,
|
|
...lines.slice(end),
|
|
].join('\n').replace(/\n*$/, '\n');
|
|
}
|
|
|
|
const mergedEntries = [];
|
|
const seenUrls = new Set();
|
|
|
|
for (const filePath of candidates) {
|
|
const text = fs.readFileSync(filePath, 'utf8');
|
|
const entries = splitFileEntries(extractFilesBlock(text, filePath));
|
|
|
|
for (const entry of entries) {
|
|
const urlLine = entry.find((line) => /^\s*-\s+url:/.test(line));
|
|
const url = urlLine?.replace(/^\s*-\s+url:\s*/, '').trim();
|
|
|
|
if (!url || seenUrls.has(url)) {
|
|
continue;
|
|
}
|
|
|
|
seenUrls.add(url);
|
|
mergedEntries.push(...entry);
|
|
}
|
|
}
|
|
|
|
const merged = replaceFilesBlock(
|
|
fs.readFileSync(candidates[0], 'utf8'),
|
|
mergedEntries
|
|
);
|
|
|
|
fs.writeFileSync('artifacts/latest-mac.yml', merged);
|
|
console.log(`Merged ${candidates.length} macOS update metadata files.`);
|
|
NODE
|
|
|
|
- name: Get version from package.json
|
|
id: package-version
|
|
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
|
|
|
# Test drafts (PR/master) get a self-describing title plus a context
|
|
# header linking the PR, real head commit, and workflow run. A stable
|
|
# tag per PR (test-pr-<n>) / branch (test-master) makes the action
|
|
# update one rolling draft in place instead of piling up a new draft
|
|
# for every push. PR builds must not use github.sha here: that is the
|
|
# ephemeral merge-commit SHA, which resolves to nothing in the repo.
|
|
- name: Compose release metadata
|
|
id: release-meta
|
|
shell: bash
|
|
env:
|
|
VERSION: ${{ steps.package-version.outputs.version }}
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
IS_TAG_BUILD: ${{ startsWith(github.ref, 'refs/tags/') }}
|
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
PR_URL: ${{ github.event.pull_request.html_url }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
SOURCE_BRANCH: ${{ github.head_ref || github.ref_name }}
|
|
REPO_URL: ${{ github.server_url }}/${{ github.repository }}
|
|
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
SHORT_SHA="${HEAD_SHA:0:7}"
|
|
SAFE_BRANCH="${SOURCE_BRANCH//\//-}"
|
|
|
|
if [ "${IS_TAG_BUILD}" = "true" ]; then
|
|
NAME="Release v${VERSION}"
|
|
TAG="${GITHUB_REF_NAME}"
|
|
BODY=""
|
|
elif [ "${EVENT_NAME}" = "pull_request" ]; then
|
|
NAME="v${VERSION} — PR #${PR_NUMBER} @ ${SHORT_SHA} [test]"
|
|
TAG="test-pr-${PR_NUMBER}"
|
|
BODY="$(printf '🧪 Test build for PR [#%s](%s) — %s\n\nCommit [`%s`](%s/commit/%s) · branch `%s` · [workflow run](%s)' \
|
|
"${PR_NUMBER}" "${PR_URL}" "${PR_TITLE}" \
|
|
"${SHORT_SHA}" "${REPO_URL}" "${HEAD_SHA}" "${SOURCE_BRANCH}" "${RUN_URL}")"
|
|
else
|
|
NAME="v${VERSION} — ${SAFE_BRANCH} @ ${SHORT_SHA} [test]"
|
|
TAG="test-${SAFE_BRANCH}"
|
|
BODY="$(printf '🧪 Test build from `%s` — commit [`%s`](%s/commit/%s) · [workflow run](%s)' \
|
|
"${SOURCE_BRANCH}" "${SHORT_SHA}" "${REPO_URL}" "${HEAD_SHA}" "${RUN_URL}")"
|
|
fi
|
|
|
|
{
|
|
echo "name=${NAME}"
|
|
echo "tag=${TAG}"
|
|
echo "commitish=${HEAD_SHA}"
|
|
} >> "${GITHUB_OUTPUT}"
|
|
|
|
if [ -n "${BODY}" ]; then
|
|
{
|
|
echo "body<<RELEASE_BODY_EOF"
|
|
echo "${BODY}"
|
|
echo "RELEASE_BODY_EOF"
|
|
} >> "${GITHUB_OUTPUT}"
|
|
else
|
|
echo "body=" >> "${GITHUB_OUTPUT}"
|
|
fi
|
|
|
|
# A PR can be closed while this workflow is still running; the
|
|
# cleanup workflow deletes the PR draft on close. Re-check the live
|
|
# PR state right before touching the draft so a late-finishing run
|
|
# cannot recreate a draft for a closed PR.
|
|
- name: Check PR is still open
|
|
if: github.event_name == 'pull_request'
|
|
id: pr-state
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
echo "state=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${{ github.event.pull_request.number }}" --jq '.state')" >> "${GITHUB_OUTPUT}"
|
|
|
|
# The rolling draft keeps assets across runs and the release action
|
|
# only replaces same-name files. If the app version changes between
|
|
# pushes, old-version installers would linger beside the new set,
|
|
# so drop every existing asset first — the action re-uploads the
|
|
# full current set right after. Only drafts are pruned; published
|
|
# releases are never touched.
|
|
- name: Prune stale draft assets
|
|
if: github.event_name != 'pull_request' || steps.pr-state.outputs.state == 'open'
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
RELEASE_TAG: ${{ steps.release-meta.outputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
release_id="$(gh api "repos/${GITHUB_REPOSITORY}/releases?per_page=100" --paginate |
|
|
jq -s --arg tag "${RELEASE_TAG}" \
|
|
'add | [.[] | select(.draft and .tag_name == $tag)][0].id // empty')"
|
|
|
|
if [ -z "${release_id}" ]; then
|
|
echo "No existing draft for ${RELEASE_TAG}; nothing to prune."
|
|
exit 0
|
|
fi
|
|
|
|
gh api "repos/${GITHUB_REPOSITORY}/releases/${release_id}/assets?per_page=100" --paginate --jq '.[].id' |
|
|
xargs -r -n1 -I{} gh api -X DELETE "repos/${GITHUB_REPOSITORY}/releases/assets/{}"
|
|
|
|
echo "Pruned assets from draft ${release_id} (${RELEASE_TAG})."
|
|
|
|
- name: Create Draft Release
|
|
id: draft-release
|
|
if: github.event_name != 'pull_request' || steps.pr-state.outputs.state == 'open'
|
|
uses: softprops/action-gh-release@v2
|
|
with:
|
|
draft: true
|
|
prerelease: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
|
name: ${{ steps.release-meta.outputs.name }}
|
|
tag_name: ${{ steps.release-meta.outputs.tag }}
|
|
target_commitish: ${{ steps.release-meta.outputs.commitish }}
|
|
body: ${{ steps.release-meta.outputs.body }}
|
|
generate_release_notes: true
|
|
files: |
|
|
artifacts/macos-x64-artifacts/*-x64.dmg
|
|
artifacts/macos-x64-artifacts/*-x64.zip
|
|
artifacts/macos-x64-artifacts/*.blockmap
|
|
artifacts/macos-arm64-artifacts/*-arm64.dmg
|
|
artifacts/macos-arm64-artifacts/*-arm64.zip
|
|
artifacts/macos-arm64-artifacts/*.blockmap
|
|
artifacts/latest-mac.yml
|
|
artifacts/linux-system-artifacts/*.deb
|
|
artifacts/linux-system-artifacts/*.rpm
|
|
artifacts/linux-system-artifacts/*.pacman
|
|
artifacts/linux-system-artifacts/*.pkg.tar.*
|
|
artifacts/linux-portable-artifacts/*.AppImage
|
|
artifacts/linux-portable-artifacts/*.snap
|
|
artifacts/linux-portable-artifacts/latest-linux*.yml
|
|
artifacts/linux-portable-artifacts/*.blockmap
|
|
artifacts/linux-flatpak-artifacts/*.flatpak
|
|
artifacts/linux-frame-copy-runtime-sources/linux-frame-copy-runtime-sources.tar.xz
|
|
artifacts/windows-artifacts/*-setup.exe
|
|
artifacts/windows-artifacts/*.msi
|
|
artifacts/windows-artifacts/*.zip
|
|
artifacts/windows-artifacts/latest.yml
|
|
artifacts/windows-artifacts/*.blockmap
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
# Rare action-gh-release path: when the release listing transiently
|
|
# misses the rolling draft, the action creates a duplicate, deletes
|
|
# it in favor of the canonical (oldest) draft, and uploads assets
|
|
# there WITHOUT refreshing that draft's metadata. Rebuild the full
|
|
# metadata (title, commitish, and body = context header + notes
|
|
# from the same generate-notes API the action uses) on the release
|
|
# id the action actually used, so the draft ends up correct no
|
|
# matter which internal path ran. If notes generation fails, the
|
|
# body is left as the action set it and only title/commitish are
|
|
# re-asserted.
|
|
- name: Ensure draft metadata is current
|
|
if: steps.draft-release.outputs.id != ''
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
|
RELEASE_TAG: ${{ steps.release-meta.outputs.tag }}
|
|
RELEASE_NAME: ${{ steps.release-meta.outputs.name }}
|
|
RELEASE_COMMITISH: ${{ steps.release-meta.outputs.commitish }}
|
|
RELEASE_BODY: ${{ steps.release-meta.outputs.body }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
GENERATED_NOTES="$(gh api -X POST "repos/${GITHUB_REPOSITORY}/releases/generate-notes" \
|
|
-f tag_name="${RELEASE_TAG}" \
|
|
-f target_commitish="${RELEASE_COMMITISH}" \
|
|
--jq '.body' || true)"
|
|
|
|
# tag_name MUST be included in every PATCH: updating a draft
|
|
# without it makes GitHub drop the pending tag (the draft
|
|
# becomes "untagged-<hash>"), which breaks the rolling-draft
|
|
# lookup on the next run.
|
|
if [ -z "${GENERATED_NOTES}" ]; then
|
|
jq -n \
|
|
--arg tag "${RELEASE_TAG}" \
|
|
--arg name "${RELEASE_NAME}" \
|
|
--arg commitish "${RELEASE_COMMITISH}" \
|
|
'{tag_name: $tag, name: $name, target_commitish: $commitish}' |
|
|
gh api -X PATCH "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" --input - > /dev/null
|
|
exit 0
|
|
fi
|
|
|
|
if [ -n "${RELEASE_BODY}" ]; then
|
|
FULL_BODY="$(printf '%s\n\n%s' "${RELEASE_BODY}" "${GENERATED_NOTES}")"
|
|
else
|
|
FULL_BODY="${GENERATED_NOTES}"
|
|
fi
|
|
|
|
jq -n \
|
|
--arg tag "${RELEASE_TAG}" \
|
|
--arg name "${RELEASE_NAME}" \
|
|
--arg commitish "${RELEASE_COMMITISH}" \
|
|
--arg body "${FULL_BODY}" \
|
|
'{tag_name: $tag, name: $name, target_commitish: $commitish, body: ($body | .[0:120000])}' |
|
|
gh api -X PATCH "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" --input - > /dev/null
|