mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
* chore(release): author release notes in .changes instead of reconstructing them CHANGELOG.md has been frozen at 0.12.0 since 2023 while the app shipped 0.23.0, semantic-release sat in devDependencies with no config, and the real user-facing notes were a 280-line MDX post written from memory at release time. The gap was never version math — it was authored notes captured while the context is still fresh. Add a `.changes/*.md` note format (type, area, issues, screenshot; no version field, since the release version is chosen deliberately) plus a generator that composes the GitHub release body, the CHANGELOG.md section and a blog-post scaffold from the accumulated notes. Changesets was considered and rejected: it versions multiple published packages, and this repo has exactly one private package. Its `version` step would also rewrite CHANGELOG.md into a flatter format than the blog post and fight the deliberate, updater-constrained version choice. - hand-rolled frontmatter parser over a YAML engine: the schema is closed, so it can reject unknown keys, which is what catches typos - PR numbers are resolved from the commit that added the note, never written by the author - MDX-significant characters in note bodies are escaped so a stray `<` cannot break the website build - blog scaffold ships `draft: true` with explicit TODO headings; the prose is editorial work, only the inventory is mechanical - revive CHANGELOG.md with an honest pointer for 0.13.0-0.23.0 rather than fabricating the missing history - drop the five unused semantic-release/conventional-changelog packages Docs: `.changes/README.md`, plus a "Release Notes For User-Visible Changes" section mirrored in CLAUDE.md and AGENTS.md, and a PR template checkbox for contributors who never read either. Tests: 26 unit tests in tools/release/release-notes.test.mjs covering parsing, validation, grouping and all three renderers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(release): default the notes version to package.json and harden alt escaping Review follow-ups on the release-notes generator. - `--version` now defaults to the root package.json version, so the `release🎶*` package scripts run bare instead of failing on a missing argument. Bumping package.json is the deliberate act that starts a release, which makes it the right single source of truth; `--version` remains as an override for dry runs before the bump. The notice goes to stderr so `--format github` keeps a pipeable stdout. - Escape backslashes before apostrophes when building the MDX `alt` string literal. A note body ending in a backslash previously produced an unterminated string and would have broken the website build. - Document that release posts are one per minor version, in the slug helper, the overwrite error, and `.changes/README.md` — a patch release edits the existing post rather than creating a second one. Tests: +1 regression test for the alt escaping, verified to fail without the fix (27 total, all passing). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(ci): put authored notes into the tag release body, fail-closed Wires the .changes pipeline into the release workflow (Codex review P1 on #1256). Calling the generator from the tag build cannot work — --consume deletes .changes/ before the tag exists — so the tag build reads what the generator already wrote: release-meta now fills BODY from the CHANGELOG.md section matching the tag's version via tools/release/extract-changelog-section.mjs. generate_release_notes stays on, so GitHub's commit list renders below the authored notes; the existing draft-metadata repair step already concatenates RELEASE_BODY with the generated notes, so the rare duplicate-draft path keeps the same layering unchanged. The extractor exits non-zero when the section is missing or empty, failing the release instead of silently shipping PR-title-only notes. A hotfix tag cut without running release:notes:changelog therefore fails at create-release by design; the error message names the exact commands to run. Tests: 5 new extractor tests (32 total in release-tools, all passing); packaging suite (247) re-run green since build-and-make.yaml is one of its inputs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(release): escape all regex metacharacters in the changelog extractor CodeQL flagged the version-to-RegExp interpolation in extract-changelog-section.mjs (regex injection + incomplete escaping): only dots were escaped, and while the CLI validates its argument as bare semver before calling, the exported extractSection() carries no such guarantee on its own. Escape the full metacharacter set so no caller can inject pattern syntax, with tests covering wildcard dots, alternation, `.*` and backslashes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(release): make changelog generation idempotent per version Codex review P2 on #1256: rerunning `release:notes:changelog` for the same version — the normal move after correcting a note before --consume — prepended a second section instead of replacing the first, leaving duplicate release entries. Extract the marker insertion into upsertChangelogSection(): it removes any existing section for the version, then rebuilds around the marker rather than string-replacing into it, so the blank-line count on both sides stays exact on both the fresh-insert and replace paths. The CLI reports when a section was replaced. Tests: 4 new cases (insert, replace-not-duplicate, neighbours untouched, missing marker); 37 total passing. End-to-end rerun verified: one heading, latest date wins, extractor output unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1624 lines
83 KiB
YAML
1624 lines
83 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@v4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '22'
|
|
|
|
- name: Resolve Linux runtime toolchain cache key
|
|
id: linux-runtime-cache-key
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
sudo apt-get update
|
|
{
|
|
apt-cache policy \
|
|
binutils build-essential cmake curl git gperf \
|
|
libasound2-dev libdrm-dev libegl-dev libgbm-dev \
|
|
libgl-dev libpulse-dev libva-dev make nasm \
|
|
ninja-build patchelf perl pkg-config python3-pip \
|
|
tar xz-utils
|
|
echo 'meson=1.7.2'
|
|
} > "${RUNNER_TEMP}/linux-runtime-toolchain.txt"
|
|
TOOLCHAIN_SHA256="$(sha256sum "${RUNNER_TEMP}/linux-runtime-toolchain.txt" | cut -d ' ' -f 1)"
|
|
SOURCE_SHA256="${{ hashFiles('tools/embedded-mpv/build-linux-runtime.cjs', 'tools/embedded-mpv/build-linux-runtime.mjs', 'tools/embedded-mpv/generate-linux-runtime-notices.cjs', 'tools/embedded-mpv/linux-runtime-manifest.cjs', 'tools/embedded-mpv/linux-source-archive-contract.cjs', 'tools/embedded-mpv/stage-runtime.mjs', 'tools/packaging/prepare-linux-runtime-source-snapshot.cjs') }}"
|
|
echo "toolchain-sha256=${TOOLCHAIN_SHA256}" >> "${GITHUB_OUTPUT}"
|
|
echo "key=linux-frame-copy-runtime-v5-ubuntu-22.04-${TOOLCHAIN_SHA256}-${SOURCE_SHA256}" >> "${GITHUB_OUTPUT}"
|
|
|
|
- name: Restore pinned Linux runtime and immutable source inputs
|
|
id: linux-runtime-cache
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: |
|
|
vendor/embedded-mpv/linux-x64/include
|
|
vendor/embedded-mpv/linux-x64/lib
|
|
vendor/embedded-mpv/linux-x64/runtime-manifest.json
|
|
dist/linux-frame-copy-runtime-source-inputs
|
|
key: ${{ steps.linux-runtime-cache-key.outputs.key }}
|
|
|
|
- name: Install pinned Linux runtime build dependencies
|
|
if: steps.linux-runtime-cache.outputs.cache-hit != 'true'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
sudo apt-get install --no-install-recommends -y \
|
|
binutils \
|
|
build-essential \
|
|
cmake \
|
|
curl \
|
|
git \
|
|
gperf \
|
|
libasound2-dev \
|
|
libdrm-dev \
|
|
libegl-dev \
|
|
libgbm-dev \
|
|
libgl-dev \
|
|
libpulse-dev \
|
|
libva-dev \
|
|
make \
|
|
nasm \
|
|
ninja-build \
|
|
patchelf \
|
|
perl \
|
|
pkg-config \
|
|
python3-pip \
|
|
tar \
|
|
xz-utils
|
|
python3 -m pip install --user 'meson==1.7.2'
|
|
|
|
- name: Build and stage pinned LGPL Linux runtime
|
|
if: steps.linux-runtime-cache.outputs.cache-hit != 'true'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
export PATH="${HOME}/.local/bin:${PATH}"
|
|
export IPTVNATOR_EMBEDDED_MPV_LINUX_BUILD_ROOT="${RUNNER_TEMP}/linux-frame-copy-runtime-build"
|
|
export RUNTIME_PREFIX="${RUNNER_TEMP}/linux-frame-copy-runtime-prefix"
|
|
|
|
node tools/embedded-mpv/build-linux-runtime.mjs "${RUNTIME_PREFIX}"
|
|
node tools/embedded-mpv/stage-runtime.mjs linux x64 "${RUNTIME_PREFIX}"
|
|
|
|
export SOURCE_INPUT_ROOT="${GITHUB_WORKSPACE}/dist/linux-frame-copy-runtime-source-inputs"
|
|
rm -rf "${SOURCE_INPUT_ROOT}"
|
|
mkdir -p \
|
|
"${SOURCE_INPUT_ROOT}/archives" \
|
|
"${SOURCE_INPUT_ROOT}/git"
|
|
|
|
git -C "${IPTVNATOR_EMBEDDED_MPV_LINUX_BUILD_ROOT}/sources/libplacebo" \
|
|
submodule foreach --recursive git clean -ffdqx
|
|
git -C "${IPTVNATOR_EMBEDDED_MPV_LINUX_BUILD_ROOT}/sources/libplacebo" \
|
|
clean -ffdqx
|
|
cp -a "${IPTVNATOR_EMBEDDED_MPV_LINUX_BUILD_ROOT}/archives/." "${SOURCE_INPUT_ROOT}/archives/"
|
|
cp -a "${IPTVNATOR_EMBEDDED_MPV_LINUX_BUILD_ROOT}/sources/libplacebo" "${SOURCE_INPUT_ROOT}/git/libplacebo"
|
|
node tools/embedded-mpv/generate-linux-runtime-notices.cjs collect \
|
|
--runtime-manifest "${RUNTIME_PREFIX}/runtime-manifest.json" \
|
|
--source-root "${IPTVNATOR_EMBEDDED_MPV_LINUX_BUILD_ROOT}/sources" \
|
|
--output-root "${SOURCE_INPUT_ROOT}/license-inputs"
|
|
|
|
- name: Generate Linux runtime notices and assemble source compliance
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
export RUNTIME_ROOT="${GITHUB_WORKSPACE}/vendor/embedded-mpv/linux-x64"
|
|
export SOURCE_INPUT_ROOT="${GITHUB_WORKSPACE}/dist/linux-frame-copy-runtime-source-inputs"
|
|
export SOURCE_BUNDLE_ROOT="${RUNNER_TEMP}/linux-frame-copy-runtime-sources"
|
|
export LIBPLACEBO_SOURCE_RECORD="${RUNNER_TEMP}/libplacebo-source-record.json"
|
|
|
|
test -f "${RUNTIME_ROOT}/runtime-manifest.json"
|
|
test -d "${SOURCE_INPUT_ROOT}/archives"
|
|
test -d "${SOURCE_INPUT_ROOT}/git/libplacebo"
|
|
test -f "${SOURCE_INPUT_ROOT}/license-inputs/linux-runtime-license-inputs.json"
|
|
|
|
rm -rf "${RUNTIME_ROOT}/notices" "${SOURCE_BUNDLE_ROOT}"
|
|
node tools/embedded-mpv/generate-linux-runtime-notices.cjs generate \
|
|
--runtime-manifest "${RUNTIME_ROOT}/runtime-manifest.json" \
|
|
--license-input-root "${SOURCE_INPUT_ROOT}/license-inputs" \
|
|
--output-root "${RUNTIME_ROOT}/notices"
|
|
|
|
mkdir -p \
|
|
"${SOURCE_BUNDLE_ROOT}/archives" \
|
|
"${SOURCE_BUNDLE_ROOT}/git" \
|
|
"${SOURCE_BUNDLE_ROOT}/license-inputs" \
|
|
"${SOURCE_BUNDLE_ROOT}/metadata" \
|
|
"${SOURCE_BUNDLE_ROOT}/notices" \
|
|
"${SOURCE_BUNDLE_ROOT}/tooling"
|
|
|
|
cp -a "${SOURCE_INPUT_ROOT}/archives/." "${SOURCE_BUNDLE_ROOT}/archives/"
|
|
cp -a "${SOURCE_INPUT_ROOT}/license-inputs/." "${SOURCE_BUNDLE_ROOT}/license-inputs/"
|
|
cp -a "${RUNTIME_ROOT}/notices/." "${SOURCE_BUNDLE_ROOT}/notices/"
|
|
cp "${RUNTIME_ROOT}/runtime-manifest.json" "${SOURCE_BUNDLE_ROOT}/metadata/runtime-manifest.json"
|
|
node tools/packaging/prepare-linux-runtime-source-snapshot.cjs prepare \
|
|
--runtime-manifest "${RUNTIME_ROOT}/runtime-manifest.json" \
|
|
--checkout "${SOURCE_INPUT_ROOT}/git/libplacebo" \
|
|
--output "${SOURCE_BUNDLE_ROOT}/git/libplacebo" \
|
|
--record-output "${LIBPLACEBO_SOURCE_RECORD}"
|
|
cp \
|
|
tools/embedded-mpv/build-linux-runtime.cjs \
|
|
tools/embedded-mpv/build-linux-runtime.mjs \
|
|
tools/embedded-mpv/generate-linux-runtime-notices.cjs \
|
|
tools/embedded-mpv/linux-runtime-manifest.cjs \
|
|
tools/embedded-mpv/linux-source-archive-contract.cjs \
|
|
tools/embedded-mpv/stage-runtime.mjs \
|
|
tools/packaging/prepare-linux-runtime-source-snapshot.cjs \
|
|
"${SOURCE_BUNDLE_ROOT}/tooling/"
|
|
test -f "${SOURCE_BUNDLE_ROOT}/notices/THIRD_PARTY_NOTICES.txt"
|
|
test -f "${SOURCE_BUNDLE_ROOT}/notices/embedded-mpv-notices.json"
|
|
git rev-parse HEAD > "${SOURCE_BUNDLE_ROOT}/metadata/iptvnator-git-revision.txt"
|
|
git diff --binary HEAD > "${SOURCE_BUNDLE_ROOT}/metadata/local-changes.patch"
|
|
node <<'NODE'
|
|
const crypto = require('node:crypto');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const {
|
|
EXPECTED_LIBPLACEBO_V7_360_1_SOURCE_SNAPSHOT_SHA256,
|
|
validateLinuxRuntimeSourceSnapshot,
|
|
} = require('./tools/packaging/prepare-linux-runtime-source-snapshot.cjs');
|
|
|
|
const manifest = JSON.parse(
|
|
fs.readFileSync(path.join(process.env.RUNTIME_ROOT, 'runtime-manifest.json'), 'utf8')
|
|
);
|
|
const archivesDirectory = path.join(process.env.SOURCE_BUNDLE_ROOT, 'archives');
|
|
const archives = fs.readdirSync(archivesDirectory).sort().map((name) => {
|
|
const contents = fs.readFileSync(path.join(archivesDirectory, name));
|
|
return {
|
|
name,
|
|
sha256: crypto.createHash('sha256').update(contents).digest('hex'),
|
|
};
|
|
});
|
|
const expectedArchiveHashes = Object.values(manifest.packages)
|
|
.map(({ sourceSha256 }) => sourceSha256)
|
|
.filter(Boolean)
|
|
.sort();
|
|
const actualArchiveHashes = archives.map(({ sha256 }) => sha256).sort();
|
|
if (
|
|
new Set(expectedArchiveHashes).size !== expectedArchiveHashes.length ||
|
|
new Set(actualArchiveHashes).size !== actualArchiveHashes.length ||
|
|
archives.length !== expectedArchiveHashes.length ||
|
|
JSON.stringify(actualArchiveHashes) !== JSON.stringify(expectedArchiveHashes)
|
|
) {
|
|
throw new Error(
|
|
'Source bundle archives must match the exact unique pinned archive hash set.'
|
|
);
|
|
}
|
|
|
|
const libplacebo = JSON.parse(
|
|
fs.readFileSync(process.env.LIBPLACEBO_SOURCE_RECORD, 'utf8')
|
|
);
|
|
if (
|
|
libplacebo.sourceGitCommit !== manifest.packages.libplacebo.sourceGitCommit ||
|
|
JSON.stringify(libplacebo.sourceSubmodules) !==
|
|
JSON.stringify(manifest.packages.libplacebo.sourceSubmodules)
|
|
) {
|
|
throw new Error('Prepared libplacebo source identity does not match the runtime manifest.');
|
|
}
|
|
validateLinuxRuntimeSourceSnapshot(libplacebo.sourceSnapshot, {
|
|
expectedSha256:
|
|
EXPECTED_LIBPLACEBO_V7_360_1_SOURCE_SNAPSHOT_SHA256,
|
|
});
|
|
|
|
const notices = JSON.parse(
|
|
fs.readFileSync(
|
|
path.join(process.env.SOURCE_BUNDLE_ROOT, 'notices', 'embedded-mpv-notices.json'),
|
|
'utf8'
|
|
)
|
|
);
|
|
const repositoryRevision = fs
|
|
.readFileSync(
|
|
path.join(
|
|
process.env.SOURCE_BUNDLE_ROOT,
|
|
'metadata',
|
|
'iptvnator-git-revision.txt'
|
|
),
|
|
'utf8'
|
|
)
|
|
.trim();
|
|
fs.writeFileSync(
|
|
path.join(process.env.SOURCE_BUNDLE_ROOT, 'metadata', 'source-index.json'),
|
|
`${JSON.stringify(
|
|
{
|
|
schemaVersion: 3,
|
|
repositoryRevision,
|
|
sourcePackages: manifest.packages,
|
|
archives,
|
|
libplacebo,
|
|
legal: {
|
|
manifest: 'notices/embedded-mpv-notices.json',
|
|
noticeFile: notices.noticeFile,
|
|
packages: notices.packages,
|
|
},
|
|
},
|
|
null,
|
|
2
|
|
)}\n`
|
|
);
|
|
NODE
|
|
(
|
|
cd "${SOURCE_BUNDLE_ROOT}/archives"
|
|
sha256sum * > "../metadata/archive-sha256.txt"
|
|
)
|
|
node tools/packaging/prepare-linux-runtime-source-snapshot.cjs assert-vcs-free \
|
|
--directory "${SOURCE_BUNDLE_ROOT}"
|
|
|
|
mkdir -p dist/compliance
|
|
rm -f dist/compliance/linux-frame-copy-runtime-sources.tar.xz
|
|
tar \
|
|
--create \
|
|
--xz \
|
|
--sort=name \
|
|
--mtime='UTC 1970-01-01' \
|
|
--owner=0 \
|
|
--group=0 \
|
|
--numeric-owner \
|
|
--file dist/compliance/linux-frame-copy-runtime-sources.tar.xz \
|
|
--directory "${SOURCE_BUNDLE_ROOT}" \
|
|
.
|
|
rm -f "${RUNTIME_ROOT}/source-archive-binding.json"
|
|
node tools/embedded-mpv/linux-source-archive-contract.cjs create \
|
|
--archive dist/compliance/linux-frame-copy-runtime-sources.tar.xz \
|
|
--repository-revision "$(git rev-parse HEAD)" \
|
|
--output "${RUNTIME_ROOT}/source-archive-binding.json"
|
|
test -s "${RUNTIME_ROOT}/source-archive-binding.json"
|
|
|
|
- name: Upload staged Linux runtime
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: linux-embedded-mpv-runtime
|
|
path: vendor/embedded-mpv/linux-x64
|
|
if-no-files-found: error
|
|
retention-days: 7
|
|
|
|
- name: Upload Linux runtime source compliance
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: linux-frame-copy-runtime-sources
|
|
path: dist/compliance/linux-frame-copy-runtime-sources.tar.xz
|
|
if-no-files-found: error
|
|
retention-days: 7
|
|
|
|
build-cross-platform:
|
|
name: Build on ${{ matrix.os }} ${{ matrix.arch }}
|
|
runs-on: ${{ matrix.runner }}
|
|
timeout-minutes: 120
|
|
concurrency:
|
|
group: ${{ github.workflow }}-build-${{ matrix.os }}-${{ matrix.arch }}-${{ github.event.pull_request.number || github.ref }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
# macOS builds - separate runners to avoid native module conflicts
|
|
- os: macos
|
|
runner: macos-15-intel
|
|
arch: x64
|
|
embedded_mpv_platform: darwin
|
|
embedded_mpv_arch: x64
|
|
embedded_mpv_build_runtime: true
|
|
- os: macos
|
|
runner: macos-latest
|
|
arch: arm64
|
|
embedded_mpv_platform: darwin
|
|
embedded_mpv_arch: arm64
|
|
embedded_mpv_build_runtime: true
|
|
- os: windows
|
|
runner: windows-2022
|
|
arch: x64
|
|
embedded_mpv_platform: win32
|
|
embedded_mpv_arch: x64
|
|
embedded_mpv_build_runtime: false
|
|
|
|
steps: &electron-build-steps
|
|
- name: Checkout code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Install pnpm
|
|
uses: pnpm/action-setup@v4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '22'
|
|
cache: 'pnpm'
|
|
|
|
- name: Install Linux system dependencies
|
|
if: matrix.os == 'linux'
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install --no-install-recommends -y \
|
|
appstream \
|
|
binutils \
|
|
dbus-daemon \
|
|
flatpak \
|
|
flatpak-builder \
|
|
libarchive-tools \
|
|
libegl-dev \
|
|
libgbm-dev \
|
|
libgl-dev \
|
|
libx11-dev \
|
|
libxext-dev \
|
|
mpv \
|
|
pkg-config \
|
|
rpm \
|
|
snapd \
|
|
squashfs-tools \
|
|
xauth \
|
|
xvfb
|
|
|
|
- name: Configure Flatpak build runtime
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'flatpak'
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo
|
|
flatpak install --user -y flathub org.freedesktop.Platform//24.08 org.freedesktop.Sdk//24.08
|
|
|
|
- name: Select Linux packaging targets for CI profile
|
|
if: matrix.os == 'linux'
|
|
run: |
|
|
cp electron-builder.json "${RUNNER_TEMP}/electron-builder.base.json"
|
|
node tools/packaging/configure-linux-frame-copy-build.mjs --profile "${{ matrix.linux_profile }}"
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Download pinned Linux Embedded MPV runtime
|
|
if: matrix.os == 'linux'
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
name: linux-embedded-mpv-runtime
|
|
path: vendor/embedded-mpv/linux-x64
|
|
|
|
- name: Inject TMDB API key
|
|
# No-op when the secret is unavailable (e.g. fork PRs) — the
|
|
# app then requires a user-provided key for TMDB enrichment.
|
|
env:
|
|
TMDB_API_KEY: ${{ secrets.TMDB_API_KEY }}
|
|
run: node tools/tmdb/inject-tmdb-key.mjs
|
|
|
|
- name: 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 embedded MPV runtime cache key
|
|
# TEMPORARY ARTIFACT TEST: remove `|| github.event_name == 'pull_request' || github.ref == 'refs/heads/master'`
|
|
# after the macOS Embedded MPV artifacts are built and manually tested.
|
|
if: matrix.embedded_mpv_platform && matrix.os != 'linux' && !startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'pull_request' || github.ref == 'refs/heads/master')
|
|
id: embedded-mpv-runtime-cache-key
|
|
shell: bash
|
|
env:
|
|
IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256: ${{ vars.IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 || secrets.IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 || '' }}
|
|
IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256: 6014aa0e6d8e98cdba90f5288295a7105d7d14ab0ca906f51465eeb478d5fea0
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
node <<'NODE'
|
|
const childProcess = require('child_process');
|
|
const crypto = require('crypto');
|
|
const fs = require('fs');
|
|
|
|
const hash = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
|
const targetPlatform = '${{ matrix.embedded_mpv_platform }}';
|
|
const targetArch = '${{ matrix.embedded_mpv_arch }}';
|
|
const sourceHashFiles = [
|
|
'tools/embedded-mpv/stage-runtime.mjs',
|
|
];
|
|
const cacheKeyParts = [
|
|
'embedded-mpv-runtime-v2',
|
|
targetPlatform,
|
|
targetArch,
|
|
process.env.RUNNER_OS,
|
|
];
|
|
let deploymentTarget = '';
|
|
let xcodeVersion = 'none';
|
|
if (targetPlatform === 'darwin') {
|
|
deploymentTarget = process.env.MACOSX_DEPLOYMENT_TARGET || '11.0';
|
|
const safeDeploymentTarget = deploymentTarget.replace(/[^A-Za-z0-9_.-]/g, '-');
|
|
sourceHashFiles.push(
|
|
'tools/embedded-mpv/build-macos-runtime.mjs',
|
|
'tools/embedded-mpv/stage-macos-runtime.mjs'
|
|
);
|
|
try {
|
|
xcodeVersion = childProcess
|
|
.execSync('xcodebuild -version', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
|
|
.replace(/\s+$/g, '')
|
|
.replace(/\n/g, ' ');
|
|
} catch {
|
|
xcodeVersion = 'none';
|
|
}
|
|
cacheKeyParts.push(
|
|
`macos${safeDeploymentTarget}`,
|
|
`xcode${hash(xcodeVersion)}`
|
|
);
|
|
} else if (targetPlatform === 'win32') {
|
|
sourceHashFiles.push('tools/embedded-mpv/stage-windows-runtime-archive.mjs');
|
|
const windowsRuntimeSha256 =
|
|
process.env.IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 ||
|
|
process.env.IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 ||
|
|
'missing';
|
|
cacheKeyParts.push(
|
|
`runtime${hash(windowsRuntimeSha256)}`
|
|
);
|
|
}
|
|
const sourceHash = hash(
|
|
sourceHashFiles.map((filePath) => fs.readFileSync(filePath)).join('\n')
|
|
);
|
|
cacheKeyParts.push(sourceHash);
|
|
const cacheKey = cacheKeyParts.join('-');
|
|
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `deployment-target=${deploymentTarget}\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `xcode-version=${xcodeVersion}\n`);
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `key=${cacheKey}\n`);
|
|
console.log(`Embedded MPV runtime cache key: ${cacheKey}`);
|
|
console.log(`Xcode version: ${xcodeVersion}`);
|
|
NODE
|
|
|
|
- name: Restore embedded MPV runtime cache
|
|
# TEMPORARY ARTIFACT TEST: remove `|| github.event_name == 'pull_request' || github.ref == 'refs/heads/master'`
|
|
# after the macOS Embedded MPV artifacts are built and manually tested.
|
|
if: matrix.embedded_mpv_platform && matrix.os != 'linux' && !startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'pull_request' || github.ref == 'refs/heads/master')
|
|
id: embedded-mpv-runtime-cache
|
|
uses: actions/cache/restore@v4
|
|
with:
|
|
path: |
|
|
vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/include
|
|
vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/lib
|
|
vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/runtime-manifest.json
|
|
key: ${{ steps.embedded-mpv-runtime-cache-key.outputs.key }}
|
|
|
|
- name: Clear stale embedded MPV runtime files
|
|
# TEMPORARY ARTIFACT TEST: remove `|| github.event_name == 'pull_request' || github.ref == 'refs/heads/master'`
|
|
# after the macOS Embedded MPV artifacts are built and manually tested.
|
|
if: matrix.embedded_mpv_platform && matrix.os != 'linux' && !startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'pull_request' || github.ref == 'refs/heads/master') && steps.embedded-mpv-runtime-cache.outputs.cache-hit != 'true'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
rm -rf \
|
|
"vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/include" \
|
|
"vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/lib" \
|
|
"vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/runtime-manifest.json"
|
|
|
|
- name: Build embedded MPV runtime (macOS release)
|
|
# TEMPORARY ARTIFACT TEST: remove `|| github.event_name == 'pull_request' || github.ref == 'refs/heads/master'`
|
|
# after the macOS Embedded MPV artifacts are built and manually tested.
|
|
if: matrix.embedded_mpv_build_runtime && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.ref == 'refs/heads/master') && steps.embedded-mpv-runtime-cache.outputs.cache-hit != 'true'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
brew install meson ninja pkg-config nasm autoconf automake libtool xz
|
|
|
|
RUNTIME_PREFIX="${RUNNER_TEMP}/embedded-mpv-runtime/${{ matrix.embedded_mpv_arch }}/prefix"
|
|
pnpm embedded-mpv:build-runtime -- "${{ matrix.embedded_mpv_arch }}" "${RUNTIME_PREFIX}"
|
|
pnpm embedded-mpv:stage-runtime -- "${{ matrix.embedded_mpv_platform }}" "${{ matrix.embedded_mpv_arch }}" "${RUNTIME_PREFIX}"
|
|
|
|
- name: Stage Windows embedded MPV runtime archive
|
|
if: matrix.os == 'windows' && steps.embedded-mpv-runtime-cache.outputs.cache-hit != 'true'
|
|
shell: bash
|
|
env:
|
|
IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_URL: ${{ vars.IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_URL || secrets.IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_URL || '' }}
|
|
IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256: ${{ vars.IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 || secrets.IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 || '' }}
|
|
IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_URL: https://github.com/zhongfly/mpv-winbuild/releases/download/2026-07-17-94335ab87a/mpv-dev-lgpl-x86_64-20260717-git-94335ab87a.7z
|
|
IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256: 6014aa0e6d8e98cdba90f5288295a7105d7d14ab0ca906f51465eeb478d5fea0
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
WINDOWS_RUNTIME_URL="${IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_URL}"
|
|
WINDOWS_RUNTIME_SHA256="${IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256}"
|
|
if [ -z "${WINDOWS_RUNTIME_URL}" ] && [ -z "${WINDOWS_RUNTIME_SHA256}" ]; then
|
|
case "${GITHUB_REF}" in
|
|
refs/tags/v*)
|
|
;;
|
|
*)
|
|
WINDOWS_RUNTIME_URL="${IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_URL}"
|
|
WINDOWS_RUNTIME_SHA256="${IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256}"
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
if [ -z "${WINDOWS_RUNTIME_URL}" ] || [ -z "${WINDOWS_RUNTIME_SHA256}" ]; then
|
|
echo "::error::Windows Embedded MPV CI requires IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_URL and IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 repository variables or secrets."
|
|
exit 1
|
|
fi
|
|
|
|
pnpm embedded-mpv:stage-runtime:windows-archive -- "${WINDOWS_RUNTIME_URL}" "${WINDOWS_RUNTIME_SHA256}"
|
|
|
|
- name: Build backend
|
|
env:
|
|
IPTVNATOR_EMBEDDED_MPV_PLATFORM: ${{ matrix.embedded_mpv_platform || '' }}
|
|
IPTVNATOR_EMBEDDED_MPV_ARCH: ${{ matrix.embedded_mpv_arch || matrix.arch || '' }}
|
|
IPTVNATOR_LINUX_FRAME_COPY_PROFILE: ${{ matrix.linux_profile || '' }}
|
|
IPTVNATOR_REQUIRE_EMBEDDED_MPV: ${{ (matrix.os == 'linux' || matrix.os == 'windows' || (matrix.os == 'macos' && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.ref == 'refs/heads/master'))) && '1' || '0' }}
|
|
run: pnpm run build:backend
|
|
|
|
- name: Verify embedded MPV build output
|
|
# TEMPORARY ARTIFACT TEST: remove `|| github.event_name == 'pull_request' || github.ref == 'refs/heads/master'`
|
|
# after the macOS Embedded MPV artifacts are built and manually tested.
|
|
if: matrix.os == 'linux' || matrix.os == 'windows' || (matrix.os == 'macos' && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.ref == 'refs/heads/master'))
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
echo "::group::Native build output"
|
|
find apps/electron-backend/native/build/Release -maxdepth 3 -type f | sort
|
|
echo "::endgroup::"
|
|
|
|
echo "::group::Dist native output"
|
|
find dist/apps/electron-backend/native -maxdepth 3 -type f | sort
|
|
echo "::endgroup::"
|
|
|
|
test -f apps/electron-backend/native/build/Release/embedded_mpv.node
|
|
test -f dist/apps/electron-backend/native/embedded_mpv.node
|
|
test -f dist/apps/electron-backend/native/embedded-mpv-runtime.json
|
|
case "${{ matrix.embedded_mpv_platform }}" in
|
|
darwin)
|
|
test -f dist/apps/electron-backend/native/lib/libmpv.2.dylib || test -f dist/apps/electron-backend/native/lib/libmpv.dylib
|
|
;;
|
|
win32)
|
|
test -f dist/apps/electron-backend/native/lib/mpv-2.dll || test -f dist/apps/electron-backend/native/lib/libmpv-2.dll || test -f dist/apps/electron-backend/native/lib/mpv.dll || test -f dist/apps/electron-backend/native/lib/libmpv.dll
|
|
# Frame-copy engine artifacts ship on Windows. The helper
|
|
# resolves the mpv DLL from its own directory, so the DLL
|
|
# must sit beside it at the native/ top level too.
|
|
test -f dist/apps/electron-backend/native/iptvnator_mpv_helper.exe
|
|
test -f dist/apps/electron-backend/native/embedded_mpv_frame_reader.node
|
|
find dist/apps/electron-backend/native -maxdepth 1 \( -name 'mpv-2.dll' -o -name 'libmpv-2.dll' -o -name 'mpv.dll' -o -name 'libmpv.dll' \) -print -quit | grep -q .
|
|
;;
|
|
linux)
|
|
node -e "const { execFileSync } = require('node:child_process'); const manifest = require('./dist/apps/electron-backend/native/embedded-mpv-runtime.json'); const revision = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(); if (manifest.origin !== 'linux-frame-copy-build' || manifest.sourceRuntimeValidated !== true || manifest.sourceArchive?.schemaVersion !== 1 || manifest.sourceArchive?.name !== 'linux-frame-copy-runtime-sources.tar.xz' || !/^[a-f0-9]{64}$/.test(manifest.sourceArchive?.sha256 ?? '') || manifest.sourceArchive?.repositoryRevision !== revision) { throw new Error('Linux embedded MPV build manifest must describe the validated source runtime and exact source archive.'); }"
|
|
test -f dist/apps/electron-backend/native/lib/libmpv.so.2
|
|
test -f dist/apps/electron-backend/native/iptvnator_mpv_helper
|
|
test -f dist/apps/electron-backend/native/embedded_mpv_frame_reader.node
|
|
if readelf -d dist/apps/electron-backend/native/embedded_mpv.node | grep -Eq 'Shared library:.*libmpv\.so'; then
|
|
echo "::error::Linux embedded MPV addon must not link directly to libmpv"
|
|
readelf -d dist/apps/electron-backend/native/embedded_mpv.node
|
|
exit 1
|
|
fi
|
|
if readelf -d dist/apps/electron-backend/native/embedded_mpv_frame_reader.node | grep -Eq 'Shared library:.*libmpv\.so'; then
|
|
echo "::error::Linux frame reader must not link directly to libmpv"
|
|
readelf -d dist/apps/electron-backend/native/embedded_mpv_frame_reader.node
|
|
exit 1
|
|
fi
|
|
HELPER_DYNAMIC="$(readelf -d dist/apps/electron-backend/native/iptvnator_mpv_helper)"
|
|
if ! printf '%s\n' "${HELPER_DYNAMIC}" | grep -Eq 'Shared library: \[libmpv\.so\.2\]'; then
|
|
echo "::error::Linux frame-copy helper must need libmpv.so.2"
|
|
printf '%s\n' "${HELPER_DYNAMIC}"
|
|
exit 1
|
|
fi
|
|
if ! printf '%s\n' "${HELPER_DYNAMIC}" | grep -Fq 'Library runpath: [$ORIGIN/lib]'; then
|
|
echo "::error::Linux frame-copy helper must keep only the relative runtime path"
|
|
printf '%s\n' "${HELPER_DYNAMIC}"
|
|
exit 1
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
- name: Validate AppStream metadata
|
|
if: matrix.os == 'linux'
|
|
run: appstreamcli validate --pedantic --no-net apps/electron-backend/linux/com.fourgray.iptvnator.metainfo.xml
|
|
|
|
- name: Build website
|
|
if: matrix.os == 'linux'
|
|
run: pnpm nx build website --skip-nx-cache
|
|
|
|
- name: Verify AppStream website assets
|
|
if: matrix.os == 'linux'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
for screenshot in player playlists epg settings; do
|
|
if ! find dist/apps/website -path "*/appstream/${screenshot}.png" -print -quit | grep -q .; then
|
|
echo "::error::Missing AppStream website asset for ${screenshot}.png"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
- name: Override macOS arch in electron-builder.json
|
|
if: matrix.os == 'macos'
|
|
run: |
|
|
# Replace the mac arch array with just the target architecture
|
|
node -e "
|
|
const fs = require('fs');
|
|
const pkg = JSON.parse(fs.readFileSync('electron-builder.json', 'utf8'));
|
|
const targets = Array.isArray(pkg.mac?.target) ? pkg.mac.target : [pkg.mac.target];
|
|
for (const target of targets) {
|
|
if (target && typeof target === 'object') {
|
|
target.arch = ['${{ matrix.arch }}'];
|
|
}
|
|
}
|
|
fs.writeFileSync('electron-builder.json', JSON.stringify(pkg, null, 4));
|
|
"
|
|
|
|
- name: Override Windows arch in electron-builder.json
|
|
if: matrix.os == 'windows'
|
|
run: |
|
|
node -e "
|
|
const fs = require('fs');
|
|
const pkg = JSON.parse(fs.readFileSync('electron-builder.json', 'utf8'));
|
|
const targets = Array.isArray(pkg.win?.target) ? pkg.win.target : [];
|
|
for (const target of targets) {
|
|
if (target && typeof target === 'object') {
|
|
target.arch = ['${{ matrix.embedded_mpv_arch }}'];
|
|
}
|
|
}
|
|
fs.writeFileSync('electron-builder.json', JSON.stringify(pkg, null, 4) + '\n');
|
|
"
|
|
|
|
- name: Validate macOS signing configuration
|
|
if: matrix.os == 'macos' && github.event_name != 'pull_request'
|
|
shell: bash
|
|
env:
|
|
CSC_NAME: ${{ vars.CSC_NAME }}
|
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
|
APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY }}
|
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
|
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
run: |
|
|
if [ -z "${CSC_NAME}" ]; then
|
|
echo "::error::Missing CSC_NAME repository variable for deterministic macOS code signing."
|
|
exit 1
|
|
fi
|
|
|
|
if [ -z "${CSC_LINK}" ] || [ -z "${CSC_KEY_PASSWORD}" ]; then
|
|
echo "::error::Missing CSC_LINK or CSC_KEY_PASSWORD secret for macOS code signing."
|
|
exit 1
|
|
fi
|
|
|
|
has_api_key_credentials=false
|
|
if [ -n "${APPLE_API_KEY_CONTENT}" ] || [ -n "${APPLE_API_KEY_ID}" ] || [ -n "${APPLE_API_ISSUER}" ]; then
|
|
if [ -z "${APPLE_API_KEY_CONTENT}" ] || [ -z "${APPLE_API_KEY_ID}" ] || [ -z "${APPLE_API_ISSUER}" ]; then
|
|
echo "::error::APPLE_API_KEY, APPLE_API_KEY_ID, and APPLE_API_ISSUER must all be set for App Store Connect API key notarization."
|
|
exit 1
|
|
fi
|
|
has_api_key_credentials=true
|
|
fi
|
|
|
|
has_apple_id_credentials=false
|
|
if [ -n "${APPLE_ID}" ] || [ -n "${APPLE_APP_SPECIFIC_PASSWORD}" ] || [ -n "${APPLE_TEAM_ID}" ]; then
|
|
if [ -z "${APPLE_ID}" ] || [ -z "${APPLE_APP_SPECIFIC_PASSWORD}" ] || [ -z "${APPLE_TEAM_ID}" ]; then
|
|
echo "::error::APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, and APPLE_TEAM_ID must all be set for Apple ID notarization."
|
|
exit 1
|
|
fi
|
|
has_apple_id_credentials=true
|
|
fi
|
|
|
|
if [ "${has_api_key_credentials}" = false ] && [ "${has_apple_id_credentials}" = false ]; then
|
|
echo "::error::Missing notarization credentials. Configure either APPLE_API_KEY + APPLE_API_KEY_ID + APPLE_API_ISSUER, or APPLE_ID + APPLE_APP_SPECIFIC_PASSWORD + APPLE_TEAM_ID."
|
|
exit 1
|
|
fi
|
|
|
|
- name: Prepare macOS notarization credentials
|
|
if: matrix.os == 'macos' && github.event_name != 'pull_request'
|
|
shell: bash
|
|
env:
|
|
APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY }}
|
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
|
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
run: |
|
|
if [ -n "${APPLE_API_KEY_CONTENT}" ]; then
|
|
APPLE_API_KEY_PATH="${RUNNER_TEMP}/AuthKey_${APPLE_API_KEY_ID}.p8"
|
|
printf '%s' "${APPLE_API_KEY_CONTENT}" > "${APPLE_API_KEY_PATH}"
|
|
chmod 600 "${APPLE_API_KEY_PATH}"
|
|
echo "APPLE_API_KEY=${APPLE_API_KEY_PATH}" >> "${GITHUB_ENV}"
|
|
echo "APPLE_API_KEY_ID=${APPLE_API_KEY_ID}" >> "${GITHUB_ENV}"
|
|
echo "APPLE_API_ISSUER=${APPLE_API_ISSUER}" >> "${GITHUB_ENV}"
|
|
echo "APPLE_ID=" >> "${GITHUB_ENV}"
|
|
echo "APPLE_APP_SPECIFIC_PASSWORD=" >> "${GITHUB_ENV}"
|
|
echo "APPLE_TEAM_ID=" >> "${GITHUB_ENV}"
|
|
exit 0
|
|
fi
|
|
|
|
if [ -n "${APPLE_ID}" ]; then
|
|
echo "APPLE_API_KEY=" >> "${GITHUB_ENV}"
|
|
echo "APPLE_API_KEY_ID=" >> "${GITHUB_ENV}"
|
|
echo "APPLE_API_ISSUER=" >> "${GITHUB_ENV}"
|
|
echo "APPLE_ID=${APPLE_ID}" >> "${GITHUB_ENV}"
|
|
echo "APPLE_APP_SPECIFIC_PASSWORD=${APPLE_APP_SPECIFIC_PASSWORD}" >> "${GITHUB_ENV}"
|
|
echo "APPLE_TEAM_ID=${APPLE_TEAM_ID}" >> "${GITHUB_ENV}"
|
|
fi
|
|
|
|
- name: Make Electron app (macOS)
|
|
if: matrix.os == 'macos' && github.event_name != 'pull_request'
|
|
env:
|
|
CSC_NAME: ${{ vars.CSC_NAME }}
|
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
|
DEBUG: electron-builder,electron-notarize*
|
|
IPTVNATOR_EMBEDDED_MPV_PLATFORM: ${{ matrix.embedded_mpv_platform }}
|
|
IPTVNATOR_EMBEDDED_MPV_ARCH: ${{ matrix.embedded_mpv_arch }}
|
|
# TEMPORARY MASTER ARTIFACT TEST: remove `|| github.ref == 'refs/heads/master'`
|
|
# after the macOS Embedded MPV artifacts are built and manually tested.
|
|
IPTVNATOR_REQUIRE_EMBEDDED_MPV: ${{ (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master') && '1' || '0' }}
|
|
run: pnpm run make:app -- --publishPolicy=never
|
|
|
|
- name: Verify signed macOS app
|
|
if: matrix.os == 'macos' && github.event_name != 'pull_request'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
case "${{ matrix.arch }}" in
|
|
x64)
|
|
APP_PATH="dist/executables/mac/IPTVnator.app"
|
|
;;
|
|
arm64)
|
|
APP_PATH="dist/executables/mac-arm64/IPTVnator.app"
|
|
;;
|
|
*)
|
|
echo "::error::Unsupported macOS arch: ${{ matrix.arch }}"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
print_debug_attrs() {
|
|
echo "::group::Extended attributes"
|
|
xattr -lr "${APP_PATH}" | sed -n '1,120p' || true
|
|
echo "::endgroup::"
|
|
}
|
|
|
|
trap print_debug_attrs ERR
|
|
|
|
if [ ! -d "${APP_PATH}" ]; then
|
|
echo "::error::Expected app bundle not found at ${APP_PATH}"
|
|
exit 1
|
|
fi
|
|
|
|
SIGNATURE_INFO="$(codesign -dv --verbose=4 "${APP_PATH}" 2>&1)"
|
|
printf '%s\n' "${SIGNATURE_INFO}"
|
|
|
|
if printf '%s\n' "${SIGNATURE_INFO}" | grep -q 'Signature=adhoc'; then
|
|
echo "::error::macOS app is still ad-hoc signed."
|
|
exit 1
|
|
fi
|
|
|
|
if printf '%s\n' "${SIGNATURE_INFO}" | grep -q 'TeamIdentifier=not set'; then
|
|
echo "::error::macOS app is missing a TeamIdentifier."
|
|
exit 1
|
|
fi
|
|
|
|
codesign --verify --deep --strict --verbose=4 "${APP_PATH}"
|
|
spctl -a -vvv --type execute "${APP_PATH}"
|
|
xcrun stapler validate "${APP_PATH}"
|
|
|
|
- name: Make Electron app
|
|
if: matrix.os != 'macos' || github.event_name == 'pull_request'
|
|
env:
|
|
IPTVNATOR_EMBEDDED_MPV_PLATFORM: ${{ matrix.embedded_mpv_platform || '' }}
|
|
IPTVNATOR_EMBEDDED_MPV_ARCH: ${{ matrix.embedded_mpv_arch || matrix.arch || '' }}
|
|
IPTVNATOR_LINUX_FRAME_COPY_PROFILE: ${{ matrix.linux_profile || '' }}
|
|
# TEMPORARY PR TEST: change this back to '0' after manually
|
|
# testing the macOS PR artifact with Embedded MPV included.
|
|
IPTVNATOR_REQUIRE_EMBEDDED_MPV: ${{ (matrix.os == 'linux' || matrix.os == 'windows' || (matrix.os == 'macos' && github.event_name == 'pull_request')) && '1' || '0' }}
|
|
run: pnpm run make:app -- --publishPolicy=never
|
|
|
|
- name: Verify packaged worker layout
|
|
shell: bash
|
|
env:
|
|
PACKAGE_OS: ${{ matrix.os }}
|
|
PACKAGE_ARCH: ${{ matrix.arch || matrix.embedded_mpv_arch || '' }}
|
|
IPTVNATOR_LINUX_FRAME_COPY_PROFILE: ${{ matrix.linux_profile || '' }}
|
|
# TEMPORARY ARTIFACT TEST: remove `|| github.event_name == 'pull_request' || github.ref == 'refs/heads/master'`
|
|
# after the macOS Embedded MPV artifacts are built and manually tested.
|
|
IPTVNATOR_REQUIRE_EMBEDDED_MPV: ${{ (matrix.os == 'linux' || matrix.os == 'windows' || (matrix.os == 'macos' && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.ref == 'refs/heads/master'))) && '1' || '0' }}
|
|
run: pnpm run verify:package-layout -- "$PACKAGE_OS" "$PACKAGE_ARCH"
|
|
|
|
- name: Make marker-only foreign-architecture DEB packages
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'system'
|
|
shell: bash
|
|
env:
|
|
IPTVNATOR_EMBEDDED_MPV_PLATFORM: linux
|
|
IPTVNATOR_EMBEDDED_MPV_ARCH: x64
|
|
IPTVNATOR_LINUX_FRAME_COPY_PROFILE: ''
|
|
IPTVNATOR_REQUIRE_EMBEDDED_MPV: '1'
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
for foreign_arch in armv7l arm64; do
|
|
rm -rf dist/executables-linux-foreign
|
|
cp "${RUNNER_TEMP}/electron-builder.base.json" electron-builder.json
|
|
node tools/packaging/configure-linux-frame-copy-build.mjs \
|
|
--foreign-deb \
|
|
--foreign-arch "${foreign_arch}"
|
|
pnpm nx run electron-backend:make \
|
|
--arch="${foreign_arch}" \
|
|
--outputPath=dist/executables-linux-foreign \
|
|
--publishPolicy=never
|
|
mapfile -t foreign_debs < <(
|
|
find dist/executables-linux-foreign -maxdepth 1 -type f -name '*.deb' -print
|
|
)
|
|
test "${#foreign_debs[@]}" -eq 1
|
|
case "${foreign_arch}" in
|
|
armv7l) expected_deb_arch=armhf ;;
|
|
arm64) expected_deb_arch=arm64 ;;
|
|
*)
|
|
echo "::error::Unexpected foreign DEB build architecture ${foreign_arch}"
|
|
exit 1
|
|
;;
|
|
esac
|
|
actual_deb_arch="$(dpkg-deb --field "${foreign_debs[0]}" Architecture)"
|
|
test "${actual_deb_arch}" = "${expected_deb_arch}"
|
|
mv "${foreign_debs[0]}" dist/executables/
|
|
done
|
|
|
|
- name: Verify DEB payloads and x64 system runtime
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'system'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
found=false
|
|
for artifact in dist/executables/*.deb; do
|
|
test -f "${artifact}" || continue
|
|
found=true
|
|
case "$(dpkg-deb --field "${artifact}" Architecture)" in
|
|
amd64)
|
|
docker run --rm \
|
|
--volume "${GITHUB_WORKSPACE}:/workspace:ro" \
|
|
--volume "$(realpath "${artifact}"):/artifact.deb:ro" \
|
|
--workdir /workspace \
|
|
ubuntu:24.04 \
|
|
bash -euo pipefail -c '
|
|
apt-get update
|
|
DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \
|
|
binutils libegl1 libgbm1 libgl1 libgl1-mesa-dri libmpv2 \
|
|
nodejs squashfs-tools xauth xvfb
|
|
xvfb-run -a env LIBGL_ALWAYS_SOFTWARE=1 \
|
|
node tools/packaging/verify-linux-frame-copy-runtime.mjs \
|
|
--artifact /artifact.deb --profile system
|
|
'
|
|
;;
|
|
arm64|armhf)
|
|
node tools/packaging/verify-linux-frame-copy-runtime.mjs \
|
|
--artifact "${artifact}" --profile system
|
|
;;
|
|
*)
|
|
echo "::error::Unexpected DEB architecture in ${artifact}"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
test "${found}" = true
|
|
|
|
- name: Verify RPM payload and x64 system runtime
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'system'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
artifact="$(find dist/executables -maxdepth 1 -type f -name '*.rpm' -print -quit)"
|
|
test -n "${artifact}"
|
|
docker run --rm \
|
|
--volume "${GITHUB_WORKSPACE}:/workspace:ro" \
|
|
--volume "$(realpath "${artifact}"):/artifact.rpm:ro" \
|
|
--workdir /workspace \
|
|
fedora:latest \
|
|
bash -euo pipefail -c '
|
|
dnf install -y \
|
|
binutils bsdtar libglvnd-egl libglvnd-glx mesa-dri-drivers \
|
|
mesa-libgbm mpv-libs nodejs rpm xorg-x11-server-Xvfb \
|
|
xorg-x11-xauth
|
|
xvfb-run -a env LIBGL_ALWAYS_SOFTWARE=1 \
|
|
node tools/packaging/verify-linux-frame-copy-runtime.mjs \
|
|
--artifact /artifact.rpm --profile system
|
|
'
|
|
|
|
- name: Verify Pacman payload and x64 system runtime
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'system'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
artifact="$(find dist/executables -maxdepth 1 -type f \( -name '*.pacman' -o -name '*.pkg.tar.*' \) -print -quit)"
|
|
test -n "${artifact}"
|
|
docker run --rm \
|
|
--volume "${GITHUB_WORKSPACE}:/workspace:ro" \
|
|
--volume "$(realpath "${artifact}"):/artifact.pacman:ro" \
|
|
--workdir /workspace \
|
|
archlinux:latest \
|
|
bash -euo pipefail -c '
|
|
pacman -Syu --noconfirm \
|
|
binutils libarchive libglvnd mesa mpv nodejs xorg-server-xvfb \
|
|
xorg-xauth
|
|
xvfb-run -a env LIBGL_ALWAYS_SOFTWARE=1 \
|
|
node tools/packaging/verify-linux-frame-copy-runtime.mjs \
|
|
--artifact /artifact.pacman --profile system
|
|
'
|
|
|
|
- name: Verify AppImage payloads and bundled runtime
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'portable'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
found=false
|
|
for artifact in dist/executables/*.AppImage; do
|
|
test -f "${artifact}" || continue
|
|
found=true
|
|
xvfb-run -a env LIBGL_ALWAYS_SOFTWARE=1 \
|
|
node tools/packaging/verify-linux-frame-copy-runtime.mjs \
|
|
--artifact "${artifact}" --profile portable
|
|
done
|
|
test "${found}" = true
|
|
|
|
- name: Verify Snap payloads and strict-confinement runtime
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'portable'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
found=false
|
|
installed_x64=false
|
|
for artifact in dist/executables/*.snap; do
|
|
test -f "${artifact}" || continue
|
|
found=true
|
|
verification="$(
|
|
xvfb-run -a env LIBGL_ALWAYS_SOFTWARE=1 \
|
|
node tools/packaging/verify-linux-frame-copy-runtime.mjs \
|
|
--artifact "${artifact}" --profile portable \
|
|
2>&1 | tee /dev/stderr
|
|
)"
|
|
if printf '%s\n' "${verification}" | grep -Fq 'Verified snap x64 Linux'; then
|
|
snap list mesa-core22 >/dev/null 2>&1 || sudo snap install mesa-core22
|
|
snap list gnome-3-28-1804 >/dev/null 2>&1 || sudo snap install gnome-3-28-1804
|
|
sudo snap install --dangerous "${artifact}"
|
|
installed_x64=true
|
|
fi
|
|
done
|
|
test "${found}" = true
|
|
test "${installed_x64}" = true
|
|
sudo snap connect iptvnator:graphics-core22 mesa-core22:graphics-core22
|
|
sudo snap connect iptvnator:gnome-3-28-1804 gnome-3-28-1804:gnome-3-28-1804
|
|
sudo snap disconnect iptvnator:graphics-core22 mesa-core22:graphics-core22
|
|
snap connections iptvnator | awk \
|
|
'$2 == "iptvnator:graphics-core22" && $3 == "-" { found=1 } END { exit !found }'
|
|
set +e
|
|
disconnected_probe="$(
|
|
xvfb-run -a env LIBGL_ALWAYS_SOFTWARE=1 \
|
|
snap run iptvnator --embedded-mpv-runtime-probe 2>&1
|
|
)"
|
|
disconnected_status=$?
|
|
set -e
|
|
printf '%s\n' "${disconnected_probe}"
|
|
test "${disconnected_status}" -eq 1
|
|
printf '%s\n' "${disconnected_probe}" | \
|
|
grep -Fx '{"usable":false,"reason":"snap-graphics-provider-unavailable"}'
|
|
sudo snap connect iptvnator:graphics-core22 mesa-core22:graphics-core22
|
|
snap connections iptvnator | awk \
|
|
'$2 == "iptvnator:graphics-core22" && $3 == "mesa-core22:graphics-core22" { found=1 } END { exit !found }'
|
|
snap connections iptvnator | awk \
|
|
'$2 == "iptvnator:gnome-3-28-1804" && $3 == "gnome-3-28-1804:gnome-3-28-1804" { found=1 } END { exit !found }'
|
|
snap connections iptvnator | awk \
|
|
'$1 == "shared-memory" && $2 == "iptvnator:shared-memory" && $3 == ":shared-memory" { found=1 } END { exit !found }'
|
|
xvfb-run -a env \
|
|
LIBGL_ALWAYS_SOFTWARE=1 \
|
|
IPTVNATOR_TRACE_PLAYER=1 \
|
|
EGL_LOG_LEVEL=debug \
|
|
LIBGL_DEBUG=verbose \
|
|
__EGL_VENDOR_LIBRARY_FILENAMES=/tmp/hostile-egl-vendor.json \
|
|
GBM_BACKEND=/tmp/hostile-gbm \
|
|
MESA_LOADER_DRIVER_OVERRIDE=/tmp/hostile-dri \
|
|
LIBVA_DRIVER_NAME=/tmp/hostile-va \
|
|
VDPAU_DRIVER_PATH=/tmp/hostile-vdpau \
|
|
VK_DRIVER_FILES=/tmp/hostile-vulkan-driver.json \
|
|
VK_ICD_FILENAMES=/tmp/hostile-vulkan-icd.json \
|
|
VK_ADD_DRIVER_FILES=/tmp/hostile-vulkan-add-driver.json \
|
|
VK_ADD_LAYER_PATH=/tmp/hostile-vulkan-layers \
|
|
VK_IMPLICIT_LAYER_PATH=/tmp/hostile-vulkan-implicit-layers \
|
|
VK_ADD_IMPLICIT_LAYER_PATH=/tmp/hostile-vulkan-add-implicit-layers \
|
|
XDG_CONFIG_HOME=/tmp/hostile-xdg-config-home \
|
|
XDG_CONFIG_DIRS=/tmp/hostile-xdg-config-dirs \
|
|
XDG_DATA_HOME=/tmp/hostile-xdg-data-home \
|
|
XDG_DATA_DIRS=/tmp/hostile-xdg-data-dirs \
|
|
snap run iptvnator --embedded-mpv-runtime-probe
|
|
|
|
- name: Run packaged x64 frame-copy and fallback smoke
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'portable'
|
|
env:
|
|
IPTVNATOR_E2E_REQUIRE_PACKAGED_FRAME_COPY: '1'
|
|
IPTVNATOR_E2E_PACKAGED_EXECUTABLE: ${{ github.workspace }}/dist/executables/linux-unpacked/iptvnator
|
|
LIBGL_ALWAYS_SOFTWARE: '1'
|
|
run: |
|
|
xvfb-run -a pnpm nx run \
|
|
electron-backend-e2e:packaged-frame-copy-smoke \
|
|
--skip-nx-cache
|
|
|
|
- name: Diagnose packaged x64 frame-copy hardware path
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'portable'
|
|
continue-on-error: true
|
|
env:
|
|
IPTVNATOR_E2E_REQUIRE_PACKAGED_FRAME_COPY: '1'
|
|
IPTVNATOR_E2E_PACKAGED_EXECUTABLE: ${{ github.workspace }}/dist/executables/linux-unpacked/iptvnator
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
if [ ! -e /dev/dri/renderD128 ]; then
|
|
echo "::notice::No /dev/dri/renderD128 is available; skipping the non-blocking hardware-path diagnostic."
|
|
exit 0
|
|
fi
|
|
ls -la /dev/dri
|
|
xvfb-run -a pnpm nx run \
|
|
electron-backend-e2e:packaged-frame-copy-smoke \
|
|
--skip-nx-cache
|
|
|
|
- name: Save embedded MPV runtime cache
|
|
# TEMPORARY ARTIFACT TEST: remove `|| github.ref == 'refs/heads/master'`
|
|
# after the macOS Embedded MPV artifacts are built and manually tested.
|
|
if: matrix.embedded_mpv_platform && matrix.os != 'linux' && !startsWith(github.ref, 'refs/tags/v') && github.repository == '4gray/iptvnator' && github.event_name != 'pull_request' && github.ref == 'refs/heads/master' && steps.embedded-mpv-runtime-cache.outputs.cache-hit != 'true'
|
|
uses: actions/cache/save@v4
|
|
with:
|
|
path: |
|
|
vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/include
|
|
vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/lib
|
|
vendor/embedded-mpv/${{ matrix.embedded_mpv_platform }}-${{ matrix.embedded_mpv_arch }}/runtime-manifest.json
|
|
key: ${{ steps.embedded-mpv-runtime-cache-key.outputs.key }}
|
|
|
|
- name: Verify Flatpak payload, launcher, and sandboxed runtime
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'flatpak'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
FLATPAK_BUNDLE="$(find dist/executables -maxdepth 1 -name '*.flatpak' | head -n 1)"
|
|
if [ -z "${FLATPAK_BUNDLE}" ]; then
|
|
echo "::error::Flatpak bundle not found in dist/executables"
|
|
exit 1
|
|
fi
|
|
|
|
xvfb-run -a env LIBGL_ALWAYS_SOFTWARE=1 \
|
|
node tools/packaging/verify-linux-frame-copy-runtime.mjs \
|
|
--artifact "${FLATPAK_BUNDLE}" --profile flatpak
|
|
flatpak install --user --noninteractive -y "${FLATPAK_BUNDLE}"
|
|
xvfb-run -a env LIBGL_ALWAYS_SOFTWARE=1 \
|
|
flatpak run --command=sh com.fourgray.iptvnator -c '
|
|
set -euo pipefail
|
|
|
|
test -f /app/share/metainfo/com.fourgray.iptvnator.metainfo.xml
|
|
test -L /app/bin/iptvnator
|
|
|
|
LAUNCHER_PATH="$(readlink -f /app/bin/iptvnator)"
|
|
test -f "${LAUNCHER_PATH}"
|
|
test -x "${LAUNCHER_PATH}"
|
|
if [ -e "${LAUNCHER_PATH}.bin" ] || [ -L "${LAUNCHER_PATH}.bin" ]; then
|
|
echo "::error::Flatpak must not contain ${LAUNCHER_PATH}.bin"
|
|
exit 1
|
|
fi
|
|
ELF_MAGIC="$(od -An -tx1 -N4 "${LAUNCHER_PATH}" | tr -d "[:space:]")"
|
|
test "${ELF_MAGIC}" = "7f454c46"
|
|
'
|
|
set +e
|
|
PROBE_OUTPUT="$(
|
|
xvfb-run -a dbus-run-session -- flatpak run \
|
|
--env=LIBGL_ALWAYS_SOFTWARE=1 \
|
|
com.fourgray.iptvnator \
|
|
--embedded-mpv-runtime-probe 2>&1
|
|
)"
|
|
PROBE_STATUS=$?
|
|
set -e
|
|
|
|
PROBE_OUTPUT_LIMIT=16384
|
|
printf '%s\n' "${PROBE_OUTPUT:0:PROBE_OUTPUT_LIMIT}"
|
|
if [ "${#PROBE_OUTPUT}" -gt "${PROBE_OUTPUT_LIMIT}" ]; then
|
|
echo "::warning::Flatpak runtime probe output was truncated to ${PROBE_OUTPUT_LIMIT} characters."
|
|
fi
|
|
if [[ "${PROBE_OUTPUT}" == *"not an ELF file"* ]] ||
|
|
[[ "${PROBE_OUTPUT}" == *"Zypak needs to be called directly"* ]]; then
|
|
echo "::error::Flatpak launched a wrapper instead of the Electron ELF."
|
|
exit 1
|
|
fi
|
|
if [ "${PROBE_STATUS}" -ne 0 ]; then
|
|
echo "::error::Flatpak application runtime probe failed with status ${PROBE_STATUS}."
|
|
exit "${PROBE_STATUS}"
|
|
fi
|
|
|
|
- name: Upload artifacts (macOS)
|
|
if: matrix.os == 'macos'
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: macos-${{ matrix.arch }}-artifacts
|
|
path: |
|
|
dist/executables/**/*.dmg
|
|
dist/executables/**/*.zip
|
|
dist/executables/**/latest-mac.yml
|
|
dist/executables/**/*.blockmap
|
|
retention-days: 7
|
|
|
|
- name: Upload system-runtime Linux artifacts
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'system'
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: linux-system-artifacts
|
|
path: |
|
|
dist/executables/*.deb
|
|
dist/executables/*.rpm
|
|
dist/executables/*.pacman
|
|
dist/executables/*.pkg.tar.*
|
|
retention-days: 7
|
|
|
|
- name: Upload portable-runtime Linux artifacts
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'portable'
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: linux-portable-artifacts
|
|
path: |
|
|
dist/executables/*.AppImage
|
|
dist/executables/*.snap
|
|
dist/executables/**/latest-linux*.yml
|
|
dist/executables/**/*.blockmap
|
|
retention-days: 7
|
|
|
|
- name: Upload Flatpak-runtime Linux artifacts
|
|
if: matrix.os == 'linux' && matrix.linux_profile == 'flatpak'
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: linux-flatpak-artifacts
|
|
path: |
|
|
dist/executables/**/*.flatpak
|
|
retention-days: 7
|
|
|
|
- name: Upload artifacts (Windows)
|
|
if: matrix.os == 'windows'
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: windows-artifacts
|
|
path: |
|
|
dist/executables/**/*.exe
|
|
dist/executables/**/*.msi
|
|
dist/executables/**/*.zip
|
|
dist/executables/**/latest.yml
|
|
dist/executables/**/*.blockmap
|
|
retention-days: 7
|
|
|
|
build-linux:
|
|
name: Build on ${{ matrix.os }} ${{ matrix.arch }} (${{ matrix.linux_profile }})
|
|
needs: linux-embedded-mpv-runtime
|
|
runs-on: ${{ matrix.runner }}
|
|
timeout-minutes: 120
|
|
concurrency:
|
|
group: ${{ github.workflow }}-build-${{ matrix.os }}-${{ matrix.linux_profile }}-${{ github.event.pull_request.number || github.ref }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- os: linux
|
|
runner: ubuntu-22.04
|
|
arch: x64
|
|
linux_profile: system
|
|
embedded_mpv_platform: linux
|
|
embedded_mpv_arch: x64
|
|
embedded_mpv_build_runtime: false
|
|
- os: linux
|
|
runner: ubuntu-22.04
|
|
arch: x64
|
|
linux_profile: portable
|
|
embedded_mpv_platform: linux
|
|
embedded_mpv_arch: x64
|
|
embedded_mpv_build_runtime: false
|
|
- os: linux
|
|
runner: ubuntu-24.04
|
|
arch: x64
|
|
linux_profile: flatpak
|
|
embedded_mpv_platform: linux
|
|
embedded_mpv_arch: x64
|
|
embedded_mpv_build_runtime: false
|
|
|
|
steps: *electron-build-steps
|
|
|
|
create-release:
|
|
name: Create Draft Release
|
|
needs:
|
|
- build-cross-platform
|
|
- build-linux
|
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
|
|
runs-on: ubuntu-latest
|
|
concurrency:
|
|
group: ${{ github.workflow }}-release-${{ github.event.pull_request.number || github.ref }}
|
|
cancel-in-progress: false
|
|
permissions:
|
|
contents: write
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Download all artifacts
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
path: artifacts
|
|
|
|
- name: Display structure of downloaded files
|
|
run: ls -R artifacts
|
|
|
|
- name: Merge macOS updater metadata
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
node <<'NODE'
|
|
const fs = require('fs');
|
|
|
|
const candidates = [
|
|
'artifacts/macos-x64-artifacts/latest-mac.yml',
|
|
'artifacts/macos-arm64-artifacts/latest-mac.yml',
|
|
].filter((filePath) => fs.existsSync(filePath));
|
|
|
|
if (candidates.length === 0) {
|
|
console.log('No macOS update metadata found; skipping merge.');
|
|
process.exit(0);
|
|
}
|
|
|
|
function extractFilesBlock(text, filePath) {
|
|
const lines = text.split(/\r?\n/);
|
|
const start = lines.findIndex((line) => line === 'files:');
|
|
|
|
if (start === -1) {
|
|
throw new Error(`Missing files block in ${filePath}`);
|
|
}
|
|
|
|
const block = [];
|
|
|
|
for (let index = start + 1; index < lines.length; index += 1) {
|
|
const line = lines[index];
|
|
|
|
if (/^\S/.test(line) && line.trim() !== '') {
|
|
break;
|
|
}
|
|
|
|
if (line.trim() !== '') {
|
|
block.push(line);
|
|
}
|
|
}
|
|
|
|
return block;
|
|
}
|
|
|
|
function splitFileEntries(block) {
|
|
const entries = [];
|
|
let current = [];
|
|
|
|
for (const line of block) {
|
|
if (/^\s*-\s+url:/.test(line) && current.length > 0) {
|
|
entries.push(current);
|
|
current = [];
|
|
}
|
|
|
|
current.push(line);
|
|
}
|
|
|
|
if (current.length > 0) {
|
|
entries.push(current);
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
function replaceFilesBlock(text, mergedBlock) {
|
|
const lines = text.split(/\r?\n/);
|
|
const start = lines.findIndex((line) => line === 'files:');
|
|
let end = lines.length;
|
|
|
|
for (let index = start + 1; index < lines.length; index += 1) {
|
|
const line = lines[index];
|
|
|
|
if (/^\S/.test(line) && line.trim() !== '') {
|
|
end = index;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return [
|
|
...lines.slice(0, start + 1),
|
|
...mergedBlock,
|
|
...lines.slice(end),
|
|
].join('\n').replace(/\n*$/, '\n');
|
|
}
|
|
|
|
const mergedEntries = [];
|
|
const seenUrls = new Set();
|
|
|
|
for (const filePath of candidates) {
|
|
const text = fs.readFileSync(filePath, 'utf8');
|
|
const entries = splitFileEntries(extractFilesBlock(text, filePath));
|
|
|
|
for (const entry of entries) {
|
|
const urlLine = entry.find((line) => /^\s*-\s+url:/.test(line));
|
|
const url = urlLine?.replace(/^\s*-\s+url:\s*/, '').trim();
|
|
|
|
if (!url || seenUrls.has(url)) {
|
|
continue;
|
|
}
|
|
|
|
seenUrls.add(url);
|
|
mergedEntries.push(...entry);
|
|
}
|
|
}
|
|
|
|
const merged = replaceFilesBlock(
|
|
fs.readFileSync(candidates[0], 'utf8'),
|
|
mergedEntries
|
|
);
|
|
|
|
fs.writeFileSync('artifacts/latest-mac.yml', merged);
|
|
console.log(`Merged ${candidates.length} macOS update metadata files.`);
|
|
NODE
|
|
|
|
- name: Get version from package.json
|
|
id: package-version
|
|
run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
|
|
|
|
# Test drafts (PR/master) get a self-describing title plus a context
|
|
# header linking the PR, real head commit, and workflow run. A stable
|
|
# tag per PR (test-pr-<n>) / branch (test-master) makes the action
|
|
# update one rolling draft in place instead of piling up a new draft
|
|
# for every push. PR builds must not use github.sha here: that is the
|
|
# ephemeral merge-commit SHA, which resolves to nothing in the repo.
|
|
- name: Compose release metadata
|
|
id: release-meta
|
|
shell: bash
|
|
env:
|
|
VERSION: ${{ steps.package-version.outputs.version }}
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
IS_TAG_BUILD: ${{ startsWith(github.ref, 'refs/tags/') }}
|
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
PR_URL: ${{ github.event.pull_request.html_url }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
SOURCE_BRANCH: ${{ github.head_ref || github.ref_name }}
|
|
REPO_URL: ${{ github.server_url }}/${{ github.repository }}
|
|
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
SHORT_SHA="${HEAD_SHA:0:7}"
|
|
SAFE_BRANCH="${SOURCE_BRANCH//\//-}"
|
|
|
|
if [ "${IS_TAG_BUILD}" = "true" ]; then
|
|
NAME="Release v${VERSION}"
|
|
TAG="${GITHUB_REF_NAME}"
|
|
# Authored release notes: the release flow writes this
|
|
# CHANGELOG section from .changes/*.md before tagging
|
|
# (see .changes/README.md), so at tag time the changelog
|
|
# is the authored source of truth. The extractor exits
|
|
# non-zero when the section is missing, failing the
|
|
# release rather than silently shipping PR-title-only
|
|
# notes. generate_release_notes stays on below, so the
|
|
# GitHub commit list still renders under this body.
|
|
BODY="$(node tools/release/extract-changelog-section.mjs "${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@v2
|
|
with:
|
|
draft: true
|
|
prerelease: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
|
name: ${{ steps.release-meta.outputs.name }}
|
|
tag_name: ${{ steps.release-meta.outputs.tag }}
|
|
target_commitish: ${{ steps.release-meta.outputs.commitish }}
|
|
body: ${{ steps.release-meta.outputs.body }}
|
|
generate_release_notes: true
|
|
files: |
|
|
artifacts/macos-x64-artifacts/*-x64.dmg
|
|
artifacts/macos-x64-artifacts/*-x64.zip
|
|
artifacts/macos-x64-artifacts/*.blockmap
|
|
artifacts/macos-arm64-artifacts/*-arm64.dmg
|
|
artifacts/macos-arm64-artifacts/*-arm64.zip
|
|
artifacts/macos-arm64-artifacts/*.blockmap
|
|
artifacts/latest-mac.yml
|
|
artifacts/linux-system-artifacts/*.deb
|
|
artifacts/linux-system-artifacts/*.rpm
|
|
artifacts/linux-system-artifacts/*.pacman
|
|
artifacts/linux-system-artifacts/*.pkg.tar.*
|
|
artifacts/linux-portable-artifacts/*.AppImage
|
|
artifacts/linux-portable-artifacts/*.snap
|
|
artifacts/linux-portable-artifacts/latest-linux*.yml
|
|
artifacts/linux-portable-artifacts/*.blockmap
|
|
artifacts/linux-flatpak-artifacts/*.flatpak
|
|
artifacts/linux-frame-copy-runtime-sources/linux-frame-copy-runtime-sources.tar.xz
|
|
artifacts/windows-artifacts/*-setup.exe
|
|
artifacts/windows-artifacts/*.msi
|
|
artifacts/windows-artifacts/*.zip
|
|
artifacts/windows-artifacts/latest.yml
|
|
artifacts/windows-artifacts/*.blockmap
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
# Rare action-gh-release path: when the release listing transiently
|
|
# misses the rolling draft, the action creates a duplicate, deletes
|
|
# it in favor of the canonical (oldest) draft, and uploads assets
|
|
# there WITHOUT refreshing that draft's metadata. Rebuild the full
|
|
# metadata (title, commitish, and body = context header + notes
|
|
# from the same generate-notes API the action uses) on the release
|
|
# id the action actually used, so the draft ends up correct no
|
|
# matter which internal path ran. If notes generation fails, the
|
|
# body is left as the action set it and only title/commitish are
|
|
# re-asserted.
|
|
- name: Ensure draft metadata is current
|
|
if: steps.draft-release.outputs.id != ''
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
|
|
RELEASE_TAG: ${{ steps.release-meta.outputs.tag }}
|
|
RELEASE_NAME: ${{ steps.release-meta.outputs.name }}
|
|
RELEASE_COMMITISH: ${{ steps.release-meta.outputs.commitish }}
|
|
RELEASE_BODY: ${{ steps.release-meta.outputs.body }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
GENERATED_NOTES="$(gh api -X POST "repos/${GITHUB_REPOSITORY}/releases/generate-notes" \
|
|
-f tag_name="${RELEASE_TAG}" \
|
|
-f target_commitish="${RELEASE_COMMITISH}" \
|
|
--jq '.body' || true)"
|
|
|
|
# tag_name MUST be included in every PATCH: updating a draft
|
|
# without it makes GitHub drop the pending tag (the draft
|
|
# becomes "untagged-<hash>"), which breaks the rolling-draft
|
|
# lookup on the next run.
|
|
if [ -z "${GENERATED_NOTES}" ]; then
|
|
jq -n \
|
|
--arg tag "${RELEASE_TAG}" \
|
|
--arg name "${RELEASE_NAME}" \
|
|
--arg commitish "${RELEASE_COMMITISH}" \
|
|
'{tag_name: $tag, name: $name, target_commitish: $commitish}' |
|
|
gh api -X PATCH "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" --input - > /dev/null
|
|
exit 0
|
|
fi
|
|
|
|
if [ -n "${RELEASE_BODY}" ]; then
|
|
FULL_BODY="$(printf '%s\n\n%s' "${RELEASE_BODY}" "${GENERATED_NOTES}")"
|
|
else
|
|
FULL_BODY="${GENERATED_NOTES}"
|
|
fi
|
|
|
|
jq -n \
|
|
--arg tag "${RELEASE_TAG}" \
|
|
--arg name "${RELEASE_NAME}" \
|
|
--arg commitish "${RELEASE_COMMITISH}" \
|
|
--arg body "${FULL_BODY}" \
|
|
'{tag_name: $tag, name: $name, target_commitish: $commitish, body: ($body | .[0:120000])}' |
|
|
gh api -X PATCH "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" --input - > /dev/null
|