mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
* chore(deps): upgrade Angular to 22.1 and Nx to 23.2 * fix(deps): complete Angular migrations after rebasing on master * fix(ci): use the Node pin for Windows runtime refresh * docs(deps): synchronize the workspace-shell Node requirements
1615 lines
82 KiB
YAML
1615 lines
82 KiB
YAML
name: Build and Make Electron App
|
|
|
|
# Docs-only changes never affect the packaged app, so they skip the build
|
|
# matrix. apps/website/** is intentionally NOT ignored: the Linux build job
|
|
# builds the website to verify AppStream screenshot assets. Tag pushes are
|
|
# unaffected — GitHub does not evaluate paths filters for tags, so v* release
|
|
# builds always run.
|
|
#
|
|
# Known accepted edge case: if a PR built a test-pr-<n> draft and later
|
|
# reverts its code changes so the remaining diff is docs-only, new pushes
|
|
# skip this workflow and the draft keeps assets from the older commit. The
|
|
# draft's title/body name the exact commit they were built from, and
|
|
# cleanup-pr-draft.yml deletes the draft when the PR closes, so the stale
|
|
# window is visible and bounded; refreshing drafts on skipped runs is not
|
|
# worth a separate workflow.
|
|
on:
|
|
push:
|
|
branches:
|
|
- master
|
|
tags:
|
|
- 'v*.*.*'
|
|
paths-ignore:
|
|
- '**/*.md'
|
|
- 'docs/**'
|
|
- '.plans/**'
|
|
- '.codex/**'
|
|
- '.claude/**'
|
|
pull_request:
|
|
branches:
|
|
- master
|
|
paths-ignore:
|
|
- '**/*.md'
|
|
- 'docs/**'
|
|
- '.plans/**'
|
|
- '.codex/**'
|
|
- '.claude/**'
|
|
workflow_dispatch:
|
|
|
|
# Build jobs only read the repo; the create-release job raises itself to
|
|
# contents: write at the job level to manage the rolling draft release.
|
|
permissions:
|
|
contents: read
|
|
|
|
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@v7
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v7
|
|
with:
|
|
node-version-file: '.nvmrc'
|
|
|
|
- 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/download-pinned-source.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@v6
|
|
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/download-pinned-source.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@v7
|
|
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@v7
|
|
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@v7
|
|
|
|
- name: Install pnpm
|
|
uses: pnpm/action-setup@v6.0.10
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v7
|
|
with:
|
|
node-version-file: '.nvmrc'
|
|
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@v8
|
|
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: Inject build commit
|
|
# Shows "<version> (<sha>)" in Settings > About so bug reports
|
|
# from test builds identify the exact commit. PR builds use the
|
|
# head SHA — github.sha would be the ephemeral merge commit.
|
|
env:
|
|
BUILD_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
run: node tools/build/inject-build-commit.mjs
|
|
|
|
- name: Build frontend
|
|
run: pnpm nx build web --skip-nx-cache
|
|
|
|
- name: Resolve pinned Windows Embedded MPV runtime
|
|
if: matrix.os == 'windows'
|
|
id: windows-embedded-mpv-runtime-pin
|
|
shell: bash
|
|
run: node tools/embedded-mpv/windows-runtime-pin.mjs --github-output
|
|
|
|
- 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_PINNED_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256: ${{ steps.windows-embedded-mpv-runtime-pin.outputs.sha256 || '' }}
|
|
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/download-pinned-source.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',
|
|
'tools/embedded-mpv/windows-runtime-pin.json',
|
|
'tools/embedded-mpv/windows-runtime-pin.mjs'
|
|
);
|
|
const windowsRuntimeSha256 =
|
|
process.env.IPTVNATOR_PINNED_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@v6
|
|
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:
|
|
WINDOWS_RUNTIME_URL: ${{ steps.windows-embedded-mpv-runtime-pin.outputs.url }}
|
|
WINDOWS_RUNTIME_SHA256: ${{ steps.windows-embedded-mpv-runtime-pin.outputs.sha256 }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
if [ -z "${WINDOWS_RUNTIME_URL}" ] || [ -z "${WINDOWS_RUNTIME_SHA256}" ]; then
|
|
echo "::error::The checked-in Windows Embedded MPV runtime pin did not resolve a URL and SHA-256 digest."
|
|
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@v6
|
|
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@v7
|
|
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@v7
|
|
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@v7
|
|
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@v7
|
|
with:
|
|
name: linux-flatpak-artifacts
|
|
path: |
|
|
dist/executables/**/*.flatpak
|
|
retention-days: 7
|
|
|
|
- name: Upload artifacts (Windows)
|
|
if: matrix.os == 'windows'
|
|
uses: actions/upload-artifact@v7
|
|
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@v7
|
|
|
|
- name: Download all artifacts
|
|
uses: actions/download-artifact@v8
|
|
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}"
|
|
# The full committed CHANGELOG retains internal notes,
|
|
# while the tag release uses authored public extraction.
|
|
# An internal-only release intentionally has no authored
|
|
# public text. GitHub's generated commit list remains
|
|
# separate below.
|
|
BODY="$(node tools/release/extract-changelog-section.mjs --public "${VERSION}")"
|
|
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@v3
|
|
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
|