mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
Port the embedded mpv frame-copy pipeline to Windows with WGL rendering and named shared memory. Includes packaging validation, platform gates, tests, and architecture documentation.
860 lines
43 KiB
YAML
860 lines
43 KiB
YAML
name: Build and Make Electron App
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- master
|
|
tags:
|
|
- 'v*.*.*'
|
|
pull_request:
|
|
branches:
|
|
- master
|
|
workflow_dispatch:
|
|
|
|
jobs:
|
|
build:
|
|
name: Build on ${{ matrix.os }} ${{ matrix.arch }}
|
|
runs-on: ${{ matrix.runner }}
|
|
timeout-minutes: 120
|
|
strategy:
|
|
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
|
|
# Linux and Windows
|
|
- os: linux
|
|
runner: ubuntu-22.04
|
|
linux_profile: standard
|
|
embedded_mpv_platform: linux
|
|
embedded_mpv_arch: x64
|
|
embedded_mpv_build_runtime: false
|
|
- os: linux
|
|
runner: ubuntu-24.04
|
|
linux_profile: flatpak
|
|
embedded_mpv_platform: linux
|
|
embedded_mpv_arch: x64
|
|
embedded_mpv_build_runtime: false
|
|
- os: windows
|
|
runner: windows-2022
|
|
embedded_mpv_platform: win32
|
|
embedded_mpv_arch: x64
|
|
embedded_mpv_build_runtime: false
|
|
|
|
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 rpm libarchive-tools flatpak flatpak-builder appstream libx11-dev libxext-dev libmpv-dev mpv pkg-config libegl-dev libgl-dev libopengl-dev libgbm-dev
|
|
|
|
# Configure Flatpak
|
|
# 1. Add the Flathub repository (source of runtimes)
|
|
flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo
|
|
|
|
# 2. Install the standard Freedesktop Platform and SDK (required by electron-builder)
|
|
# We install version 24.08 as a safe default, electron-builder might pick what it needs
|
|
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: |
|
|
node -e "
|
|
const fs = require('fs');
|
|
const path = 'electron-builder.json';
|
|
const config = JSON.parse(fs.readFileSync(path, 'utf8'));
|
|
const targets = Array.isArray(config.linux?.target) ? config.linux.target : [];
|
|
const profile = '${{ matrix.linux_profile }}';
|
|
|
|
if (profile === 'standard') {
|
|
config.linux.target = targets.filter((entry) => String(entry.target).toLowerCase() !== 'flatpak');
|
|
} else if (profile === 'flatpak') {
|
|
config.linux.target = targets.filter((entry) => String(entry.target).toLowerCase() === 'flatpak');
|
|
}
|
|
|
|
fs.writeFileSync(path, JSON.stringify(config, null, 4) + '\n');
|
|
"
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- 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 && !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: a26e28fa55c15ac5b6209d8a9f9c3c8cb39649631a53d3c4774beeae559a078c
|
|
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 && !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 && !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-06-14-7d245fd100/mpv-dev-lgpl-x86_64-20260614-git-7d245fd100.7z
|
|
IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256: a26e28fa55c15ac5b6209d8a9f9c3c8cb39649631a53d3c4774beeae559a078c
|
|
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: Stage Linux embedded MPV build inputs
|
|
if: matrix.os == 'linux'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
RUNTIME_PREFIX="${RUNNER_TEMP}/embedded-mpv-runtime/linux-x64/prefix"
|
|
rm -rf "${RUNTIME_PREFIX}"
|
|
mkdir -p "${RUNTIME_PREFIX}/include"
|
|
|
|
cp -a /usr/include/mpv "${RUNTIME_PREFIX}/include/"
|
|
|
|
LIBMPV_DEV_VERSION="$(dpkg-query -W -f='${Version}' libmpv-dev)"
|
|
MPV_VERSION="$(dpkg-query -W -f='${Version}' mpv)"
|
|
export RUNTIME_PREFIX LIBMPV_DEV_VERSION MPV_VERSION
|
|
node <<'NODE'
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const manifest = {
|
|
linuxBackend: 'process-isolated mpv --wid',
|
|
buildInputs: {
|
|
libmpvDevPackage: process.env.LIBMPV_DEV_VERSION,
|
|
mpvPackage: process.env.MPV_VERSION,
|
|
},
|
|
sourceDistribution:
|
|
'Linux CI build inputs come from Ubuntu runner packages. Runtime playback uses the system mpv executable; IPTVnator does not bundle or load libmpv in the Electron process on Linux.',
|
|
};
|
|
|
|
fs.writeFileSync(
|
|
path.join(process.env.RUNTIME_PREFIX, 'runtime-manifest.json'),
|
|
`${JSON.stringify(manifest, null, 2)}\n`
|
|
);
|
|
NODE
|
|
|
|
pnpm embedded-mpv:stage-runtime -- linux x64 "${RUNTIME_PREFIX}"
|
|
|
|
- name: Build backend
|
|
env:
|
|
IPTVNATOR_EMBEDDED_MPV_PLATFORM: ${{ matrix.embedded_mpv_platform || '' }}
|
|
IPTVNATOR_EMBEDDED_MPV_ARCH: ${{ matrix.embedded_mpv_arch || matrix.arch || '' }}
|
|
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 manifest = require('./dist/apps/electron-backend/native/embedded-mpv-runtime.json'); if (manifest.origin !== 'external-mpv-process') { throw new Error('Linux embedded MPV manifest must use external-mpv-process origin.'); }"
|
|
if find dist/apps/electron-backend/native/lib -name 'libmpv.so*' -print -quit 2>/dev/null | grep -q .; then
|
|
echo "::error::Linux embedded MPV packages must not bundle libmpv"
|
|
find dist/apps/electron-backend/native/lib -name 'libmpv.so*' -print
|
|
exit 1
|
|
fi
|
|
if ldd dist/apps/electron-backend/native/embedded_mpv.node | grep -q 'libmpv'; then
|
|
echo "::error::Linux embedded MPV addon must not link directly to libmpv"
|
|
ldd dist/apps/electron-backend/native/embedded_mpv.node
|
|
exit 1
|
|
fi
|
|
# The frame-copy helper is the inverse: a separate process
|
|
# that MUST link libmpv (dev-mode engine; stripped from
|
|
# packages until the bundled-runtime staging lands).
|
|
# test -f, not -x: the webpack dist asset copy drops file
|
|
# modes; consumers restore the bit (after-pack) or require
|
|
# it via the X_OK support probe.
|
|
test -f dist/apps/electron-backend/native/iptvnator_mpv_helper
|
|
if ! ldd dist/apps/electron-backend/native/iptvnator_mpv_helper | grep -q 'libmpv'; then
|
|
echo "::error::Linux frame-copy helper must link libmpv"
|
|
ldd dist/apps/electron-backend/native/iptvnator_mpv_helper
|
|
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 || '' }}
|
|
# 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 || '' }}
|
|
# 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: 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: Smoke test packaged Flatpak launcher
|
|
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
|
|
|
|
flatpak install --user --noninteractive -y "${FLATPAK_BUNDLE}"
|
|
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 -f "${LAUNCHER_PATH}.bin"
|
|
grep -q '\''readlink -f "$SCRIPT_PATH"'\'' "${LAUNCHER_PATH}"
|
|
grep -q '\''exec "$SCRIPT_DIR/iptvnator.bin"'\'' "${LAUNCHER_PATH}"
|
|
'
|
|
|
|
- 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 artifacts (Linux)
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'standard'
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: linux-artifacts
|
|
path: |
|
|
dist/executables/**/*.deb
|
|
dist/executables/**/*.rpm
|
|
dist/executables/**/*.snap
|
|
dist/executables/**/*.AppImage
|
|
dist/executables/**/*.tar.gz
|
|
dist/executables/**/*.pacman
|
|
dist/executables/**/latest-linux*.yml
|
|
dist/executables/**/*.blockmap
|
|
retention-days: 7
|
|
|
|
- name: Upload artifacts (Flatpak)
|
|
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
|
|
|
|
create-release:
|
|
name: Create Draft Release
|
|
needs: build
|
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
|
|
runs-on: ubuntu-latest
|
|
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
|
|
|
|
- name: Create Draft Release
|
|
uses: softprops/action-gh-release@v2
|
|
with:
|
|
draft: true
|
|
prerelease: ${{ github.event_name == 'pull_request' }}
|
|
name: Release v${{ steps.package-version.outputs.version }}
|
|
tag_name: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || format('test-{0}', github.sha) }}
|
|
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-artifacts/*.AppImage
|
|
artifacts/linux-artifacts/*.deb
|
|
artifacts/linux-artifacts/*.rpm
|
|
artifacts/linux-artifacts/*.snap
|
|
artifacts/linux-artifacts/*.tar.gz
|
|
artifacts/linux-artifacts/*.pacman
|
|
artifacts/linux-artifacts/latest-linux*.yml
|
|
artifacts/linux-artifacts/*.blockmap
|
|
artifacts/linux-flatpak-artifacts/*.flatpak
|
|
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 }}
|
|
|
|
publish-snap:
|
|
name: Publish to Snapcraft Store
|
|
needs: build
|
|
runs-on: ubuntu-latest
|
|
if: startsWith(github.ref, 'refs/tags/v')
|
|
env:
|
|
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.snapcraft_token }}
|
|
|
|
steps:
|
|
- name: Download snap artifact
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: linux-artifacts
|
|
path: artifacts
|
|
|
|
- name: Setup Snapcraft
|
|
uses: samuelmeuli/action-snapcraft@v3
|
|
|
|
- name: Publish all snaps to edge channel
|
|
run: |
|
|
# Find and publish all snap files
|
|
for SNAP_FILE in artifacts/*.snap; do
|
|
if [ -f "$SNAP_FILE" ]; then
|
|
echo "Publishing: $SNAP_FILE"
|
|
snapcraft upload --release=edge "$SNAP_FILE"
|
|
fi
|
|
done
|