From f04f67728eef27094cd030f8eb9e981a5704c758 Mon Sep 17 00:00:00 2001 From: 4gray <4gray@users.noreply.github.com> Date: Sat, 29 Aug 2026 21:24:06 +0200 Subject: [PATCH] ci(embedded-mpv): keep Windows runtime pin available (#1495) --- .github/workflows/build-and-make.yaml | 40 ++-- .../refresh-windows-embedded-mpv-runtime.yaml | 108 +++++++++ AGENTS.md | 16 ++ CLAUDE.md | 16 ++ docs/architecture/embedded-mpv-native.md | 51 +++- package.json | 2 + tools/embedded-mpv/README.md | 41 +++- .../stage-windows-runtime-archive.mjs | 38 ++- .../update-windows-runtime-pin.mjs | 217 +++++++++++++++++ tools/embedded-mpv/windows-runtime-pin.json | 17 ++ tools/embedded-mpv/windows-runtime-pin.mjs | 171 ++++++++++++++ .../embedded-mpv/windows-runtime-pin.test.mjs | 223 ++++++++++++++++++ .../electron-package-identity.test.mjs | 89 +++++-- tools/packaging/project.json | 18 +- 14 files changed, 972 insertions(+), 75 deletions(-) create mode 100644 .github/workflows/refresh-windows-embedded-mpv-runtime.yaml create mode 100644 tools/embedded-mpv/update-windows-runtime-pin.mjs create mode 100644 tools/embedded-mpv/windows-runtime-pin.json create mode 100644 tools/embedded-mpv/windows-runtime-pin.mjs create mode 100644 tools/embedded-mpv/windows-runtime-pin.test.mjs diff --git a/.github/workflows/build-and-make.yaml b/.github/workflows/build-and-make.yaml index f04ca8330..fc7e8bb27 100644 --- a/.github/workflows/build-and-make.yaml +++ b/.github/workflows/build-and-make.yaml @@ -448,6 +448,12 @@ jobs: - 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. @@ -455,8 +461,7 @@ jobs: 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 + IPTVNATOR_PINNED_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256: ${{ steps.windows-embedded-mpv-runtime-pin.outputs.sha256 || '' }} run: | set -euo pipefail @@ -500,11 +505,13 @@ jobs: `xcode${hash(xcodeVersion)}` ); } else if (targetPlatform === 'win32') { - sourceHashFiles.push('tools/embedded-mpv/stage-windows-runtime-archive.mjs'); + 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_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 || - process.env.IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 || - 'missing'; + process.env.IPTVNATOR_PINNED_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 || 'missing'; cacheKeyParts.push( `runtime${hash(windowsRuntimeSha256)}` ); @@ -566,28 +573,13 @@ jobs: 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 + 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 - 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." + echo "::error::The checked-in Windows Embedded MPV runtime pin did not resolve a URL and SHA-256 digest." exit 1 fi diff --git a/.github/workflows/refresh-windows-embedded-mpv-runtime.yaml b/.github/workflows/refresh-windows-embedded-mpv-runtime.yaml new file mode 100644 index 000000000..fafdab266 --- /dev/null +++ b/.github/workflows/refresh-windows-embedded-mpv-runtime.yaml @@ -0,0 +1,108 @@ +name: Refresh Windows Embedded MPV Runtime Pin + +on: + schedule: + - cron: '17 6 * * 1' + workflow_dispatch: + inputs: + force: + description: Refresh even when the current pin is still young + required: false + default: false + type: boolean + +permissions: + contents: read + +concurrency: + group: refresh-windows-embedded-mpv-runtime-pin + cancel-in-progress: false + +jobs: + refresh: + name: Refresh checked-in runtime pin + if: github.repository == '4gray/iptvnator' + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: '22' + + - name: Refresh pin when it approaches upstream retention + id: refresh + env: + FORCE_REFRESH: ${{ inputs.force && 'true' || 'false' }} + GITHUB_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + + if [ "${FORCE_REFRESH}" = "true" ]; then + node tools/embedded-mpv/update-windows-runtime-pin.mjs --force + else + node tools/embedded-mpv/update-windows-runtime-pin.mjs + fi + + - name: Validate updated pin + if: steps.refresh.outputs.changed == 'true' + run: >- + node --test + tools/embedded-mpv/windows-runtime-pin.test.mjs + + - name: Create or update refresh pull request + if: steps.refresh.outputs.changed == 'true' + env: + GH_TOKEN: ${{ secrets.PAT }} + BRANCH_NAME: automation/windows-embedded-mpv-runtime-pin + RELEASE_TAG: ${{ steps.refresh.outputs.release-tag }} + shell: bash + run: | + set -euo pipefail + + if [ -z "${GH_TOKEN}" ]; then + echo "::error::The PAT secret is required so the bot-created PR triggers normal CI." + exit 1 + fi + + gh auth setup-git + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git switch -C "${BRANCH_NAME}" + git add -- tools/embedded-mpv/windows-runtime-pin.json + git commit -m "ci(embedded-mpv): refresh Windows runtime pin" + git fetch origin "${BRANCH_NAME}:refs/remotes/origin/${BRANCH_NAME}" || true + git push --force-with-lease origin "HEAD:refs/heads/${BRANCH_NAME}" + + PR_BODY="$(printf '%s\n\n%s\n%s\n%s' \ + 'Automated refresh of the checksum-pinned Windows Embedded MPV CI input.' \ + "- Pins upstream release \`${RELEASE_TAG}\` in repository history." \ + '- Keeps the binary on its upstream host; IPTVnator does not mirror it.' \ + '- Preserves the explicit checksum/layout-only license-verification statement.')" + PR_NUMBER="$(gh pr list \ + --base master \ + --head "${BRANCH_NAME}" \ + --state open \ + --json number \ + --jq '.[0].number // empty')" + + if [ -n "${PR_NUMBER}" ]; then + gh pr edit "${PR_NUMBER}" \ + --title "ci(embedded-mpv): refresh Windows runtime pin" \ + --body "${PR_BODY}" + gh pr view "${PR_NUMBER}" --json url --jq '.url' + else + gh pr create \ + --base master \ + --head "${BRANCH_NAME}" \ + --title "ci(embedded-mpv): refresh Windows runtime pin" \ + --body "${PR_BODY}" + fi diff --git a/AGENTS.md b/AGENTS.md index 380832d3c..91c7418a8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -496,6 +496,22 @@ Key files: holds its own blocker in `EmbeddedMpvNativeService`; external MPV/VLC inhibit the screensaver themselves. +## Windows Embedded MPV Pin Maintenance + +- PR, master, and tag builds resolve the Windows runtime only from + `tools/embedded-mpv/windows-runtime-pin.json`; repository variables are not + build inputs. +- Validate the checked-in schema and provenance with + `pnpm embedded-mpv:windows-runtime-pin:check`. +- Prepare a manual rotation with + `pnpm embedded-mpv:windows-runtime-pin:refresh -- --force`. The weekly + `refresh-windows-embedded-mpv-runtime.yaml` workflow runs the same updater + and opens a reviewable PR before upstream retention expires. +- The PAT-backed refresh job must keep every third-party action pinned to a + full commit. Do not mirror the upstream binary without complete + corresponding source, build records, license notices, and a validated + transitive license closure. + ## Linux Embedded MPV Packaging - Official Linux frame-copy artifacts are x64-only. AppImage, DEB, RPM, diff --git a/CLAUDE.md b/CLAUDE.md index e4701a13a..f894f1cce 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -148,6 +148,22 @@ pnpm run make:app nx run electron-backend:make ``` +### Windows Embedded MPV Pin Maintenance + +- PR, master, and tag builds resolve the Windows runtime only from + `tools/embedded-mpv/windows-runtime-pin.json`; repository variables are not + build inputs. +- Validate the checked-in schema and provenance with + `pnpm embedded-mpv:windows-runtime-pin:check`. +- Prepare a manual rotation with + `pnpm embedded-mpv:windows-runtime-pin:refresh -- --force`. The weekly + `refresh-windows-embedded-mpv-runtime.yaml` workflow runs the same updater + and opens a reviewable PR before upstream retention expires. +- The PAT-backed refresh job must keep every third-party action pinned to a + full commit. Do not mirror the upstream binary without complete + corresponding source, build records, license notices, and a validated + transitive license closure. + ### Electron CDP Debugging - Start Electron in dev mode with: `nx serve electron-backend` diff --git a/docs/architecture/embedded-mpv-native.md b/docs/architecture/embedded-mpv-native.md index 1abd08468..43d0a4e28 100644 --- a/docs/architecture/embedded-mpv-native.md +++ b/docs/architecture/embedded-mpv-native.md @@ -886,17 +886,35 @@ builder; both matrices reuse one YAML-anchored step list to prevent packaging logic drift. Draft release assembly still requires both matrices, so a public release cannot silently omit a promised platform. -Windows CI uses a checksum-pinned `win32-x64` runtime archive configured -through `IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_URL` and -`IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256`. Non-tag artifact builds have -a pinned `zhongfly/mpv-winbuild` `mpv-dev-lgpl-x86_64` fallback; tagged -releases require explicit repository configuration. Upstream retains only its -latest 30 daily builds, so the fallback and any repository-variable copy must -be refreshed as one URL/checksum pair before expiry. A long-lived mirror must -publish the matching source/build records and license notices with the binary. -The archive helper accepts normal `lib/` + `bin/` prefixes and common flat -archives, preserves the DLL basename encoded by the import library, and -generates minimal build metadata only when the archive lacks it. +Windows CI reads its single checksum-pinned `win32-x64` input from +`tools/embedded-mpv/windows-runtime-pin.json`. The validated pin owns the exact +upstream release, non-v3 `mpv-dev-lgpl-x86_64` asset URL, GitHub-published +SHA-256 digest, mpv commit, build-run evidence, retention policy, and the +limited license-verification statement. PR, master, and tag builds all consume +that checked-in record; mutable repository variables cannot silently change a +build or drift from the cache key. + +`refresh-windows-embedded-mpv-runtime.yaml` checks the pin weekly. If its asset +is unavailable or 14 days old, the dependency-free updater selects the newest +matching public release with a GitHub digest and upstream build evidence, +verifies that it is downloadable, updates only the JSON pin, and opens or +refreshes a reviewable bot PR. The `PAT` repository secret is used deliberately +for that PR so its create/synchronize events trigger the normal CI workflows; +it needs repository contents and pull-request access. Run +`pnpm embedded-mpv:windows-runtime-pin:check` for a local schema check or +`pnpm embedded-mpv:windows-runtime-pin:refresh -- --force` to prepare the same +update manually. + +The zhongfly asset contains only libmpv headers, its import library, and the +DLL. Upstream labels it LGPLv2.1+ with statically linked LGPLv3 FFmpeg, but also +states that its transitive LGPL compatibility is not guaranteed. IPTVnator +therefore verifies availability, checksum, and archive layout only, records +that limitation in the pin and generated runtime manifest, and does not host a +long-lived mirror of the binary. A future mirror or first-party Windows build +must first publish complete corresponding source, exact build scripts and +patches, license notices, and a validated transitive license closure beside the +binary. The archive helper accepts normal `lib/` + `bin/` prefixes and common +flat archives, and preserves the DLL basename encoded by the import library. The Linux builder pins FFmpeg `8.1`, mpv `0.41.0`, libplacebo `7.360.1`, libass `0.17.3`, FreeType `2.13.3`, FriBidi `1.0.16`, HarfBuzz `8.5.0`, @@ -966,7 +984,16 @@ For tagged macOS builds, CI must: - set `IPTVNATOR_EMBEDDED_MPV_ARCH=${arch}` for backend build and packaging - set `IPTVNATOR_REQUIRE_EMBEDDED_MPV=1` for packaging and package-layout verification -For Windows builds, CI must restore the `win32-x64` staged runtime cache or stage the checksum-pinned runtime archive before `pnpm run build:backend`. The Windows job must set `IPTVNATOR_EMBEDDED_MPV_PLATFORM=win32`, `IPTVNATOR_EMBEDDED_MPV_ARCH=x64`, and `IPTVNATOR_REQUIRE_EMBEDDED_MPV=1` for backend build, package make, and package-layout verification. CI narrows `electron-builder.json` to x64 Windows targets while only a `win32-x64` runtime is available. The Windows job is pinned to `windows-2022` until the Electron `node-gyp` toolchain can identify Visual Studio 18 from `windows-latest`. +For Windows builds, CI must resolve the validated checked-in pin, restore its +exact-keyed `win32-x64` staged runtime cache or stage that checksum-pinned +archive, and only then run `pnpm run build:backend`. The Windows job must set +`IPTVNATOR_EMBEDDED_MPV_PLATFORM=win32`, +`IPTVNATOR_EMBEDDED_MPV_ARCH=x64`, and +`IPTVNATOR_REQUIRE_EMBEDDED_MPV=1` for backend build, package make, and +package-layout verification. CI narrows `electron-builder.json` to x64 Windows +targets while only a `win32-x64` runtime is available. The Windows job is +pinned to `windows-2022` until the Electron `node-gyp` toolchain can identify +Visual Studio 18 from `windows-latest`. For Linux builds, CI first builds or restores the pinned x64 source runtime and stages it under `vendor/embedded-mpv/linux-x64`. It then runs three isolated diff --git a/package.json b/package.json index 2e00e0bde..69a60cf2d 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,8 @@ "embedded-mpv:stage-runtime": "node tools/embedded-mpv/stage-runtime.mjs", "embedded-mpv:stage-runtime:macos": "node tools/embedded-mpv/stage-macos-runtime.mjs", "embedded-mpv:stage-runtime:windows-archive": "node tools/embedded-mpv/stage-windows-runtime-archive.mjs", + "embedded-mpv:windows-runtime-pin:check": "node tools/embedded-mpv/windows-runtime-pin.mjs", + "embedded-mpv:windows-runtime-pin:refresh": "node tools/embedded-mpv/update-windows-runtime-pin.mjs", "serve:backend:embedded-mpv": "pnpm embedded-mpv:build-native:homebrew && IPTVNATOR_EMBEDDED_MPV_ALLOW_HOMEBREW=1 IPTVNATOR_ENABLE_EMBEDDED_MPV_EXPERIMENT=1 pnpm serve:backend", "package:app": "nx run electron-backend:make --prepackageOnly", "make:app": "nx run electron-backend:make", diff --git a/tools/embedded-mpv/README.md b/tools/embedded-mpv/README.md index ffaafbca1..558485eb3 100644 --- a/tools/embedded-mpv/README.md +++ b/tools/embedded-mpv/README.md @@ -82,6 +82,31 @@ pnpm embedded-mpv:build-runtime:linux -- /tmp/linux-prefix pnpm embedded-mpv:stage-runtime -- linux x64 /tmp/linux-prefix ``` +### Windows CI pin lifecycle + +Windows package builds consume the one validated record in +`windows-runtime-pin.json`; URL and checksum repository variables are not build +inputs. Check it locally with: + +```bash +pnpm embedded-mpv:windows-runtime-pin:check +``` + +The weekly `refresh-windows-embedded-mpv-runtime.yaml` workflow opens a bot PR +when the upstream asset is unavailable or 14 days old, well before zhongfly's +30-day retention boundary. A manual refresh uses the same dependency-free +updater: + +```bash +pnpm embedded-mpv:windows-runtime-pin:refresh -- --force +``` + +The upstream archive is checksum- and layout-verified, not independently +certified as a complete LGPL closure. It contains no corresponding source or +license notices, so IPTVnator does not mirror it. Any future stable mirror must +ship complete corresponding source, exact build scripts and patches, notices, +and a validated transitive license record beside the binary. + The macOS builder verifies every downloaded archive against its pinned SHA-256 digest before extraction. FreeType uses its official SourceForge distribution as the primary source and the official Savannah distribution as @@ -398,14 +423,14 @@ scoped to asset selection/download. Candidate/stable promotion is manual after installed-Snap frame-copy and missing-runtime fallback smoke; GitHub Actions never promotes automatically. -Windows CI stages a checksum-pinned x64 LGPL archive. The DLL basename encoded -in its import library is preserved and must be present beside -`iptvnator_mpv_helper.exe`. Tagged releases require explicit repository -configuration; the public fallback is for non-tag artifacts only. The upstream -keeps only its latest 30 daily builds, so the fallback URL and checksum plus any -matching repository variables must be refreshed as one pair before they age -out. A permanent mirror must publish the corresponding source/build records and -license notices with the binary. +Windows CI stages the x64 LGPL archive selected by the checked-in validated pin +described above. PR, master, and tag builds consume that same record; repository +variables and fallback URLs are not build inputs. The DLL basename encoded in +the archive's import library is preserved and must be present beside +`iptvnator_mpv_helper.exe`. The weekly workflow rotates the reviewed pin before +the upstream's latest-30-build retention removes it. A permanent mirror still +requires the complete corresponding source/build records, license notices, and +validated transitive license closure beside the binary. ## Local Development diff --git a/tools/embedded-mpv/stage-windows-runtime-archive.mjs b/tools/embedded-mpv/stage-windows-runtime-archive.mjs index abd13e140..adeefdf8d 100644 --- a/tools/embedded-mpv/stage-windows-runtime-archive.mjs +++ b/tools/embedded-mpv/stage-windows-runtime-archive.mjs @@ -6,6 +6,7 @@ import path from 'path'; import { spawnSync } from 'child_process'; import { Readable } from 'stream'; import { pipeline } from 'stream/promises'; +import { readWindowsRuntimePin } from './windows-runtime-pin.mjs'; const rawArgs = process.argv.slice(2); const args = rawArgs[0] === '--' ? rawArgs.slice(1) : rawArgs; @@ -18,7 +19,7 @@ if (!archiveSource || !rawExpectedSha256) { [ 'Usage: node tools/embedded-mpv/stage-windows-runtime-archive.mjs ', '', - 'Downloads or reads a checksum-pinned LGPL-compatible Windows libmpv runtime archive,', + 'Downloads or reads a checksum-pinned, upstream-labelled LGPL Windows libmpv runtime archive,', 'extracts it, and stages it as vendor/embedded-mpv/win32-x64.', '', 'The archive must contain a prefix with:', @@ -189,23 +190,50 @@ function copyFile(sourcePath, destinationPath) { } function writeGeneratedManifest(destinationPath, archiveSha256) { + let checkedInPin = null; + try { + const candidate = readWindowsRuntimePin(); + if ( + candidate.asset.url === archiveSource && + candidate.asset.sha256 === archiveSha256 + ) { + checkedInPin = candidate; + } + } catch { + // Arbitrary local archives remain supported without repository pin metadata. + } + const upstreamEvidence = checkedInPin?.upstream; const manifest = { sourceDistribution: archiveSource, archive: { urlOrPath: archiveSource, sha256: archiveSha256, }, + verification: { + level: 'checksum-and-layout-only', + licenseClosure: 'not-independently-verified', + }, + ...(upstreamEvidence + ? { + upstream: { + releaseTag: checkedInPin.releaseTag, + mpvCommit: upstreamEvidence.mpvCommit, + buildRunUrl: upstreamEvidence.buildRunUrl, + licenseClaim: upstreamEvidence.licenseClaim, + }, + } + : {}), ffmpeg: { licensePolicy: - 'LGPL-compatible Windows runtime archive supplied to CI.', + 'The upstream archive describes statically linked FFmpeg as LGPLv3; IPTVnator does not independently certify its complete static dependency closure.', configureFlags: - 'Record exact FFmpeg configure flags in the upstream runtime manifest when available.', + 'Not embedded in the upstream binary archive; consult the pinned upstream build run.', }, mpv: { licensePolicy: - 'LGPL-compatible libmpv Windows runtime archive supplied to CI.', + 'The upstream archive describes libmpv as LGPLv2.1+ with GPL-only features disabled; IPTVnator verifies only the checksum and required files.', mesonFlags: - 'Record exact mpv Meson flags in the upstream runtime manifest when available.', + 'Not embedded in the upstream binary archive; consult the pinned upstream build run.', }, }; diff --git a/tools/embedded-mpv/update-windows-runtime-pin.mjs b/tools/embedded-mpv/update-windows-runtime-pin.mjs new file mode 100644 index 000000000..2a937d468 --- /dev/null +++ b/tools/embedded-mpv/update-windows-runtime-pin.mjs @@ -0,0 +1,217 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; +import { + readWindowsRuntimePin, + serializeWindowsRuntimePin, + validateWindowsRuntimePin, + WINDOWS_RUNTIME_LICENSE_CLAIM, + WINDOWS_RUNTIME_PIN_PATH, + WINDOWS_RUNTIME_REPOSITORY, +} from './windows-runtime-pin.mjs'; + +const modulePath = fileURLToPath(import.meta.url); +const RELEASES_API = `https://api.github.com/repos/${WINDOWS_RUNTIME_REPOSITORY}/releases?per_page=50`; +const ASSET_NAME_PATTERN = /^mpv-dev-lgpl-x86_64-\d{8}-git-[a-f0-9]{10}\.7z$/; + +export const WINDOWS_RUNTIME_REFRESH_AFTER_DAYS = 14; + +function githubApiHeaders() { + return { + Accept: 'application/vnd.github+json', + 'User-Agent': 'iptvnator-windows-runtime-pin-updater', + ...(process.env.GITHUB_TOKEN + ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } + : {}), + }; +} + +async function fetchJson(url, fetchImpl) { + const response = await fetchImpl(url, { headers: githubApiHeaders() }); + if (!response.ok) { + throw new Error( + `Unable to query Windows runtime releases: ${response.status} ${response.statusText}` + ); + } + return response.json(); +} + +function extractUpstreamEvidence(body) { + const mpvCommit = body?.match( + /github\.com\/mpv-player\/mpv\/commit\/([a-f0-9]{40})/i + )?.[1]; + const buildRunId = body?.match( + /github\.com\/zhongfly\/mpv-winbuild\/actions\/runs\/(\d+)/i + )?.[1]; + if (!mpvCommit || !buildRunId) { + throw new Error( + 'Latest Windows runtime release lacks the expected mpv commit or build-run evidence.' + ); + } + return { + mpvCommit: mpvCommit.toLowerCase(), + buildRunUrl: `https://github.com/zhongfly/mpv-winbuild/actions/runs/${buildRunId}`, + }; +} + +export function pinFromUpstreamRelease(release) { + if (release?.draft || release?.prerelease) { + throw new Error( + 'Windows runtime pin cannot use a draft or prerelease.' + ); + } + const matchingAssets = (release?.assets ?? []).filter((asset) => + ASSET_NAME_PATTERN.test(asset?.name ?? '') + ); + if (matchingAssets.length !== 1) { + throw new Error( + `Release ${release?.tag_name ?? ''} must contain exactly one non-v3 x86_64 LGPL dev archive.` + ); + } + const asset = matchingAssets[0]; + const sha256 = asset.digest?.match(/^sha256:([a-f0-9]{64})$/)?.[1]; + if (!sha256) { + throw new Error( + `Release asset ${asset.name} must expose a GitHub SHA-256 digest.` + ); + } + const evidence = extractUpstreamEvidence(release.body); + return validateWindowsRuntimePin({ + schemaVersion: 1, + repository: WINDOWS_RUNTIME_REPOSITORY, + releaseTag: release.tag_name, + publishedAt: release.published_at, + retentionDays: 30, + asset: { + name: asset.name, + url: asset.browser_download_url, + sha256, + }, + upstream: { + ...evidence, + licenseClaim: WINDOWS_RUNTIME_LICENSE_CLAIM, + }, + }); +} + +export function selectNewestWindowsRuntimePin(releases) { + if (!Array.isArray(releases)) { + throw new Error('Windows runtime releases response must be an array.'); + } + const candidates = releases + .filter((release) => !release?.draft && !release?.prerelease) + .filter((release) => + (release?.assets ?? []).some((asset) => + ASSET_NAME_PATTERN.test(asset?.name ?? '') + ) + ) + .sort( + (left, right) => + Date.parse(right.published_at) - Date.parse(left.published_at) + ); + if (candidates.length === 0) { + throw new Error('No suitable Windows LGPL runtime release was found.'); + } + return pinFromUpstreamRelease(candidates[0]); +} + +export function runtimePinAgeDays(pin, now = new Date()) { + return (now.getTime() - Date.parse(pin.publishedAt)) / 86_400_000; +} + +export async function isRuntimeAssetAvailable(url, fetchImpl = fetch) { + try { + const response = await fetchImpl(url, { + method: 'HEAD', + redirect: 'follow', + headers: { + 'User-Agent': 'iptvnator-windows-runtime-pin-updater', + }, + }); + return response.ok; + } catch { + return false; + } +} + +export async function refreshWindowsRuntimePin({ + pinPath = WINDOWS_RUNTIME_PIN_PATH, + now = new Date(), + force = false, + dryRun = false, + fetchImpl = fetch, +} = {}) { + const current = readWindowsRuntimePin(pinPath); + const currentAvailable = await isRuntimeAssetAvailable( + current.asset.url, + fetchImpl + ); + const ageDays = runtimePinAgeDays(current, now); + const refreshReason = force + ? 'forced' + : !currentAvailable + ? 'unavailable' + : ageDays >= WINDOWS_RUNTIME_REFRESH_AFTER_DAYS + ? 'age-threshold' + : null; + + if (!refreshReason) { + return { + changed: false, + reason: 'current', + current, + next: current, + ageDays, + }; + } + + const releases = await fetchJson(RELEASES_API, fetchImpl); + const next = selectNewestWindowsRuntimePin(releases); + if (!(await isRuntimeAssetAvailable(next.asset.url, fetchImpl))) { + throw new Error( + `Selected Windows runtime asset is not downloadable: ${next.asset.url}` + ); + } + const changed = + serializeWindowsRuntimePin(next) !== + serializeWindowsRuntimePin(current); + if (changed && !dryRun) { + fs.writeFileSync(pinPath, serializeWindowsRuntimePin(next)); + } + return { changed, reason: refreshReason, current, next, ageDays }; +} + +function appendGitHubOutputs(result) { + if (!process.env.GITHUB_OUTPUT) { + return; + } + fs.appendFileSync( + process.env.GITHUB_OUTPUT, + [ + `changed=${result.changed}`, + `reason=${result.reason}`, + `release-tag=${result.next.releaseTag}`, + '', + ].join('\n') + ); +} + +async function main() { + const result = await refreshWindowsRuntimePin({ + force: process.argv.includes('--force'), + dryRun: process.argv.includes('--dry-run'), + }); + appendGitHubOutputs(result); + const action = result.changed ? 'updated' : 'kept'; + console.log( + `Windows Embedded MPV runtime pin ${action}: ${result.next.releaseTag} (${result.reason}).` + ); +} + +if (process.argv[1] === modulePath) { + main().catch((error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + }); +} diff --git a/tools/embedded-mpv/windows-runtime-pin.json b/tools/embedded-mpv/windows-runtime-pin.json new file mode 100644 index 000000000..8305b6373 --- /dev/null +++ b/tools/embedded-mpv/windows-runtime-pin.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": 1, + "repository": "zhongfly/mpv-winbuild", + "releaseTag": "2026-08-21-49418246f3", + "publishedAt": "2026-08-21T12:20:41Z", + "retentionDays": 30, + "asset": { + "name": "mpv-dev-lgpl-x86_64-20260821-git-49418246f3.7z", + "url": "https://github.com/zhongfly/mpv-winbuild/releases/download/2026-08-21-49418246f3/mpv-dev-lgpl-x86_64-20260821-git-49418246f3.7z", + "sha256": "317dfd9ee814be76e5f6e20b45efcc07440389a62b55dd85201829b4880510e0" + }, + "upstream": { + "mpvCommit": "49418246f30a9c24af31ac184aa24f39755db89a", + "buildRunUrl": "https://github.com/zhongfly/mpv-winbuild/actions/runs/32479875364", + "licenseClaim": "Upstream labels this libmpv build LGPLv2.1+ with statically linked LGPLv3 FFmpeg; IPTVnator verifies the checksum and archive layout, not the complete transitive license closure." + } +} diff --git a/tools/embedded-mpv/windows-runtime-pin.mjs b/tools/embedded-mpv/windows-runtime-pin.mjs new file mode 100644 index 000000000..7e10f8d57 --- /dev/null +++ b/tools/embedded-mpv/windows-runtime-pin.mjs @@ -0,0 +1,171 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; + +const modulePath = fileURLToPath(import.meta.url); +const moduleDir = path.dirname(modulePath); + +export const WINDOWS_RUNTIME_PIN_PATH = path.join( + moduleDir, + 'windows-runtime-pin.json' +); +export const WINDOWS_RUNTIME_REPOSITORY = 'zhongfly/mpv-winbuild'; +export const WINDOWS_RUNTIME_LICENSE_CLAIM = + 'Upstream labels this libmpv build LGPLv2.1+ with statically linked LGPLv3 FFmpeg; IPTVnator verifies the checksum and archive layout, not the complete transitive license closure.'; + +const RELEASE_TAG_PATTERN = /^(\d{4})-(\d{2})-(\d{2})-([a-f0-9]{10})$/; +const ASSET_NAME_PATTERN = + /^mpv-dev-lgpl-x86_64-(\d{8})-git-([a-f0-9]{10})\.7z$/; +const SHA256_PATTERN = /^[a-f0-9]{64}$/; +const COMMIT_PATTERN = /^[a-f0-9]{40}$/; +const BUILD_RUN_PATTERN = + /^https:\/\/github\.com\/zhongfly\/mpv-winbuild\/actions\/runs\/(\d+)$/; + +function hasExactFields(value, fields) { + return ( + value !== null && + typeof value === 'object' && + !Array.isArray(value) && + Object.keys(value).sort().join('\0') === [...fields].sort().join('\0') + ); +} + +function assert(condition, message) { + if (!condition) { + throw new Error(`Invalid Windows Embedded MPV runtime pin: ${message}`); + } +} + +function expectedAssetUrl(releaseTag, assetName) { + return `https://github.com/${WINDOWS_RUNTIME_REPOSITORY}/releases/download/${releaseTag}/${assetName}`; +} + +export function validateWindowsRuntimePin(pin) { + assert( + hasExactFields(pin, [ + 'schemaVersion', + 'repository', + 'releaseTag', + 'publishedAt', + 'retentionDays', + 'asset', + 'upstream', + ]), + 'root fields do not match schema version 1' + ); + assert(pin.schemaVersion === 1, 'schemaVersion must be 1'); + assert( + pin.repository === WINDOWS_RUNTIME_REPOSITORY, + `repository must be ${WINDOWS_RUNTIME_REPOSITORY}` + ); + assert(pin.retentionDays === 30, 'retentionDays must be 30'); + + const releaseMatch = pin.releaseTag?.match(RELEASE_TAG_PATTERN); + assert(releaseMatch, 'releaseTag has an unexpected format'); + const publishedAt = Date.parse(pin.publishedAt); + assert( + Number.isFinite(publishedAt), + 'publishedAt must be an ISO timestamp' + ); + + assert( + hasExactFields(pin.asset, ['name', 'url', 'sha256']), + 'asset fields do not match schema version 1' + ); + const assetMatch = pin.asset.name?.match(ASSET_NAME_PATTERN); + assert(assetMatch, 'asset.name must be the non-v3 x86_64 LGPL dev archive'); + assert( + assetMatch[1] === releaseMatch.slice(1, 4).join(''), + 'asset date must match the release tag date' + ); + assert( + assetMatch[2] === releaseMatch[4], + 'asset commit suffix must match the release tag' + ); + assert( + pin.asset.url === expectedAssetUrl(pin.releaseTag, pin.asset.name), + 'asset.url must be derived from the pinned release and asset name' + ); + assert( + SHA256_PATTERN.test(pin.asset.sha256), + 'asset.sha256 must be a lowercase SHA-256 digest' + ); + + assert( + hasExactFields(pin.upstream, [ + 'mpvCommit', + 'buildRunUrl', + 'licenseClaim', + ]), + 'upstream fields do not match schema version 1' + ); + assert( + COMMIT_PATTERN.test(pin.upstream.mpvCommit), + 'upstream.mpvCommit must be a full commit hash' + ); + assert( + pin.upstream.mpvCommit.startsWith(releaseMatch[4]), + 'upstream.mpvCommit must match the release tag suffix' + ); + assert( + BUILD_RUN_PATTERN.test(pin.upstream.buildRunUrl), + 'upstream.buildRunUrl must identify the zhongfly build run' + ); + assert( + pin.upstream.licenseClaim === WINDOWS_RUNTIME_LICENSE_CLAIM, + 'upstream.licenseClaim must preserve the limited verification statement' + ); + + return pin; +} + +export function readWindowsRuntimePin(pinPath = WINDOWS_RUNTIME_PIN_PATH) { + return validateWindowsRuntimePin( + JSON.parse(fs.readFileSync(pinPath, 'utf8')) + ); +} + +export function serializeWindowsRuntimePin(pin) { + validateWindowsRuntimePin(pin); + return `${JSON.stringify(pin, null, 4)}\n`; +} + +export function appendWindowsRuntimeGitHubOutputs( + pin, + outputPath = process.env.GITHUB_OUTPUT +) { + validateWindowsRuntimePin(pin); + assert(outputPath, 'GITHUB_OUTPUT is required for --github-output'); + fs.appendFileSync( + outputPath, + [ + `url=${pin.asset.url}`, + `sha256=${pin.asset.sha256}`, + `asset-name=${pin.asset.name}`, + `release-tag=${pin.releaseTag}`, + `published-at=${pin.publishedAt}`, + '', + ].join('\n') + ); +} + +function main() { + const pin = readWindowsRuntimePin(); + if (process.argv.includes('--github-output')) { + appendWindowsRuntimeGitHubOutputs(pin); + } + console.log( + `Windows Embedded MPV runtime pin: ${pin.releaseTag} (${pin.asset.sha256})` + ); +} + +if (process.argv[1] === modulePath) { + try { + main(); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + } +} diff --git a/tools/embedded-mpv/windows-runtime-pin.test.mjs b/tools/embedded-mpv/windows-runtime-pin.test.mjs new file mode 100644 index 000000000..bbf95961a --- /dev/null +++ b/tools/embedded-mpv/windows-runtime-pin.test.mjs @@ -0,0 +1,223 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; +import { + appendWindowsRuntimeGitHubOutputs, + readWindowsRuntimePin, + serializeWindowsRuntimePin, + validateWindowsRuntimePin, + WINDOWS_RUNTIME_LICENSE_CLAIM, + WINDOWS_RUNTIME_PIN_PATH, +} from './windows-runtime-pin.mjs'; +import { + pinFromUpstreamRelease, + refreshWindowsRuntimePin, + runtimePinAgeDays, + selectNewestWindowsRuntimePin, + WINDOWS_RUNTIME_REFRESH_AFTER_DAYS, +} from './update-windows-runtime-pin.mjs'; + +const CURRENT_PIN = readWindowsRuntimePin(); + +function releaseFixture({ + date = '2026-08-28', + commit = 'e8673660ab123456789012345678901234567890', + digest = '470437b5dc9f8c74092fdfab668e89bedf7b1a6385a53ffadf241a6a7a4c6ffb', + runId = '33215046953', + publishedAt = `${date}T22:32:29Z`, +} = {}) { + const compactDate = date.replaceAll('-', ''); + const shortCommit = commit.slice(0, 10); + const tag = `${date}-${shortCommit}`; + const name = `mpv-dev-lgpl-x86_64-${compactDate}-git-${shortCommit}.7z`; + return { + draft: false, + prerelease: false, + tag_name: tag, + published_at: publishedAt, + body: [ + `MPV Git commit: https://github.com/mpv-player/mpv/commit/${commit}`, + `Build Details: https://github.com/zhongfly/mpv-winbuild/actions/runs/${runId}`, + ].join('\n'), + assets: [ + { + name, + digest: `sha256:${digest}`, + browser_download_url: `https://github.com/zhongfly/mpv-winbuild/releases/download/${tag}/${name}`, + }, + { + name: `mpv-dev-lgpl-x86_64-v3-${compactDate}-git-${shortCommit}.7z`, + digest: `sha256:${'a'.repeat(64)}`, + browser_download_url: 'https://example.invalid/v3.7z', + }, + ], + }; +} + +function response({ ok = true, status = 200, json } = {}) { + return { + ok, + status, + statusText: ok ? 'OK' : 'Not Found', + json: async () => json, + }; +} + +function createPinFixture() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'impv-win-pin-')); + const pinPath = path.join(root, 'windows-runtime-pin.json'); + fs.writeFileSync(pinPath, serializeWindowsRuntimePin(CURRENT_PIN)); + return { root, pinPath }; +} + +test('checked-in Windows runtime pin is internally consistent', () => { + assert.equal(WINDOWS_RUNTIME_PIN_PATH.endsWith('.json'), true); + assert.match(CURRENT_PIN.asset.sha256, /^[a-f0-9]{64}$/); + assert.match(CURRENT_PIN.asset.name, /^mpv-dev-lgpl-x86_64-/); + assert.doesNotMatch(CURRENT_PIN.asset.name, /-v3-/); + assert.equal( + CURRENT_PIN.upstream.licenseClaim, + WINDOWS_RUNTIME_LICENSE_CLAIM + ); +}); + +test('pin validation rejects a softened license-verification statement', () => { + const invalid = structuredClone(CURRENT_PIN); + invalid.upstream.licenseClaim = 'Verified LGPL runtime.'; + assert.throws( + () => validateWindowsRuntimePin(invalid), + /limited verification statement/ + ); +}); + +test('upstream release selection excludes the v3 archive and keeps evidence', () => { + const older = releaseFixture({ + date: '2026-08-27', + commit: '182fa6ca49123456789012345678901234567890', + publishedAt: '2026-08-27T12:41:23Z', + }); + const latest = releaseFixture(); + const pin = selectNewestWindowsRuntimePin([older, latest]); + + assert.equal(pin.releaseTag, latest.tag_name); + assert.equal(pin.asset.name, latest.assets[0].name); + assert.equal(pin.asset.sha256, latest.assets[0].digest.slice(7)); + assert.equal(pin.upstream.mpvCommit, latest.body.match(/[a-f0-9]{40}/)[0]); + assert.doesNotMatch(pin.asset.name, /-v3-/); +}); + +test('upstream release must provide GitHub digest and build evidence', () => { + const missingDigest = releaseFixture(); + delete missingDigest.assets[0].digest; + assert.throws( + () => pinFromUpstreamRelease(missingDigest), + /must expose a GitHub SHA-256 digest/ + ); + + const missingEvidence = releaseFixture(); + missingEvidence.body = ''; + assert.throws( + () => pinFromUpstreamRelease(missingEvidence), + /lacks the expected mpv commit or build-run evidence/ + ); +}); + +test('young available pin does not query releases or rewrite the file', async () => { + const fixture = createPinFixture(); + let requestCount = 0; + try { + const result = await refreshWindowsRuntimePin({ + pinPath: fixture.pinPath, + now: new Date('2026-08-29T00:00:00Z'), + fetchImpl: async () => { + requestCount += 1; + return response(); + }, + }); + assert.equal(result.changed, false); + assert.equal(result.reason, 'current'); + assert.equal(requestCount, 1); + assert.equal( + fs.readFileSync(fixture.pinPath, 'utf8'), + serializeWindowsRuntimePin(CURRENT_PIN) + ); + } finally { + fs.rmSync(fixture.root, { recursive: true, force: true }); + } +}); + +test('unavailable pin rotates to the newest downloadable release', async () => { + const fixture = createPinFixture(); + const latest = releaseFixture(); + const requests = []; + try { + const result = await refreshWindowsRuntimePin({ + pinPath: fixture.pinPath, + now: new Date('2026-08-29T00:00:00Z'), + fetchImpl: async (url, options = {}) => { + requests.push([url, options.method ?? 'GET']); + if (url === CURRENT_PIN.asset.url) { + return response({ ok: false, status: 404 }); + } + if (url.startsWith('https://api.github.com/')) { + return response({ json: [latest] }); + } + return response(); + }, + }); + assert.equal(result.changed, true); + assert.equal(result.reason, 'unavailable'); + assert.equal( + readWindowsRuntimePin(fixture.pinPath).releaseTag, + latest.tag_name + ); + assert.deepEqual( + requests.map((request) => request[1]), + ['HEAD', 'GET', 'HEAD'] + ); + } finally { + fs.rmSync(fixture.root, { recursive: true, force: true }); + } +}); + +test('age threshold rotates an available pin before upstream retention', async () => { + const fixture = createPinFixture(); + const latest = releaseFixture(); + try { + const result = await refreshWindowsRuntimePin({ + pinPath: fixture.pinPath, + now: new Date('2026-09-05T13:00:00Z'), + fetchImpl: async (url) => + url.startsWith('https://api.github.com/') + ? response({ json: [latest] }) + : response(), + }); + assert.equal( + runtimePinAgeDays(CURRENT_PIN, new Date('2026-09-05T13:00:00Z')) >= + WINDOWS_RUNTIME_REFRESH_AFTER_DAYS, + true + ); + assert.equal(result.changed, true); + assert.equal(result.reason, 'age-threshold'); + } finally { + fs.rmSync(fixture.root, { recursive: true, force: true }); + } +}); + +test('GitHub output exposes only the validated checked-in pin', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'impv-win-output-')); + const outputPath = path.join(root, 'output'); + try { + appendWindowsRuntimeGitHubOutputs(CURRENT_PIN, outputPath); + const output = fs.readFileSync(outputPath, 'utf8'); + assert.match(output, new RegExp(`sha256=${CURRENT_PIN.asset.sha256}`)); + assert.match( + output, + new RegExp(`release-tag=${CURRENT_PIN.releaseTag}`) + ); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/tools/packaging/electron-package-identity.test.mjs b/tools/packaging/electron-package-identity.test.mjs index 1982965b5..c28344434 100644 --- a/tools/packaging/electron-package-identity.test.mjs +++ b/tools/packaging/electron-package-identity.test.mjs @@ -5,6 +5,7 @@ import os from 'node:os'; import { dirname, join } from 'node:path'; import test from 'node:test'; import { fileURLToPath } from 'node:url'; +import { readWindowsRuntimePin } from '../embedded-mpv/windows-runtime-pin.mjs'; const currentDir = dirname(fileURLToPath(import.meta.url)); const require = createRequire(import.meta.url); @@ -15,6 +16,18 @@ const buildAndMakeWorkflow = fs.readFileSync( join(currentDir, '..', '..', '.github', 'workflows', 'build-and-make.yaml'), 'utf8' ); +const windowsRuntimeRefreshWorkflow = fs.readFileSync( + join( + currentDir, + '..', + '..', + '.github', + 'workflows', + 'refresh-windows-embedded-mpv-runtime.yaml' + ), + 'utf8' +); +const windowsRuntimePin = readWindowsRuntimePin(); const electronBuilderConfig = JSON.parse( fs.readFileSync( join(currentDir, '..', '..', 'electron-builder.json'), @@ -874,16 +887,6 @@ test('Windows CI packages embedded MPV from a staged x64 runtime', () => { const requireEmbeddedMpvLines = buildAndMakeWorkflow .split(/\r?\n/) .filter((line) => line.includes('IPTVNATOR_REQUIRE_EMBEDDED_MPV:')); - const defaultRuntimeUrls = [ - ...buildAndMakeWorkflow.matchAll( - /IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_URL:\s+(\S+)/g - ), - ].map((match) => match[1]); - const defaultRuntimeSha256s = [ - ...buildAndMakeWorkflow.matchAll( - /IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256:\s+([a-f0-9]{64})/g - ), - ].map((match) => match[1]); assert.equal( packageMetadata.scripts?.['embedded-mpv:stage-runtime:windows-archive'], @@ -896,27 +899,61 @@ test('Windows CI packages embedded MPV from a staged x64 runtime', () => { assert.match(buildAndMakeWorkflow, /runner:\s+windows-2022/); assert.match( buildAndMakeWorkflow, - /IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_URL/ + /name:\s+Resolve pinned Windows Embedded MPV runtime/ ); assert.match( buildAndMakeWorkflow, - /IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256/ + /node tools\/embedded-mpv\/windows-runtime-pin\.mjs --github-output/ ); assert.match( buildAndMakeWorkflow, - /IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_URL: https:\/\/github\.com\/zhongfly\/mpv-winbuild\/releases\/download\// + /WINDOWS_RUNTIME_URL:\s+\$\{\{ steps\.windows-embedded-mpv-runtime-pin\.outputs\.url \}\}/ ); - assert.deepEqual( - [...new Set(defaultRuntimeUrls)], - [ - 'https://github.com/zhongfly/mpv-winbuild/releases/download/2026-07-17-94335ab87a/mpv-dev-lgpl-x86_64-20260717-git-94335ab87a.7z', - ] + assert.match( + buildAndMakeWorkflow, + /WINDOWS_RUNTIME_SHA256:\s+\$\{\{ steps\.windows-embedded-mpv-runtime-pin\.outputs\.sha256 \}\}/ ); - assert.deepEqual( - [...new Set(defaultRuntimeSha256s)], - ['6014aa0e6d8e98cdba90f5288295a7105d7d14ab0ca906f51465eeb478d5fea0'] + assert.doesNotMatch( + buildAndMakeWorkflow, + /(?:vars|secrets)\.IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME/ + ); + assert.doesNotMatch(buildAndMakeWorkflow, /zhongfly\/mpv-winbuild/); + assert.equal(windowsRuntimePin.repository, 'zhongfly/mpv-winbuild'); + assert.match(windowsRuntimePin.asset.name, /^mpv-dev-lgpl-x86_64-/); + assert.match(windowsRuntimePin.asset.sha256, /^[a-f0-9]{64}$/); + assert.match( + windowsRuntimePin.upstream.licenseClaim, + /checksum and archive layout, not the complete transitive license closure/ + ); + assert.match( + windowsRuntimeRefreshWorkflow, + /node tools\/embedded-mpv\/update-windows-runtime-pin\.mjs/ + ); + assert.match( + windowsRuntimeRefreshWorkflow, + /node --test\s+tools\/embedded-mpv\/windows-runtime-pin\.test\.mjs/ + ); + assert.doesNotMatch( + windowsRuntimeRefreshWorkflow, + /electron-package-identity\.test\.mjs/ + ); + assert.match( + windowsRuntimeRefreshWorkflow, + /automation\/windows-embedded-mpv-runtime-pin/ + ); + assert.match(windowsRuntimeRefreshWorkflow, /secrets\.PAT/); + assert.match( + windowsRuntimeRefreshWorkflow, + /actions\/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1/ + ); + assert.match( + windowsRuntimeRefreshWorkflow, + /actions\/setup-node@820762786026740c76f36085b0efc47a31fe5020/ + ); + assert.doesNotMatch( + windowsRuntimeRefreshWorkflow, + /actions\/(?:checkout|setup-node)@v\d+/ ); - assert.match(buildAndMakeWorkflow, /refs\/tags\/v\*/); assert.match( buildAndMakeWorkflow, /name:\s+Override Windows arch in electron-builder\.json/ @@ -927,6 +964,14 @@ test('Windows CI packages embedded MPV from a staged x64 runtime', () => { ); assert.match(embeddedMpvStageRuntimeSource, /\.dll\.a/); assert.match(embeddedMpvBuildSource, /\.dll\.a/); + assert.match( + embeddedMpvWindowsArchiveStageSource, + /checksum-and-layout-only/ + ); + assert.match( + embeddedMpvWindowsArchiveStageSource, + /not-independently-verified/ + ); assert.ok(requireEmbeddedMpvLines.length > 0); for (const line of requireEmbeddedMpvLines) { assert.doesNotMatch(line, /cache-hit/); diff --git a/tools/packaging/project.json b/tools/packaging/project.json index 0b84f2ac3..5d2e7afd2 100644 --- a/tools/packaging/project.json +++ b/tools/packaging/project.json @@ -13,6 +13,7 @@ "{workspaceRoot}/electron-builder.json", "{workspaceRoot}/.github/workflows/build-and-make.yaml", "{workspaceRoot}/.github/workflows/publish-snap.yaml", + "{workspaceRoot}/.github/workflows/refresh-windows-embedded-mpv-runtime.yaml", "{workspaceRoot}/apps/electron-backend/build-embedded-mpv.js", "{workspaceRoot}/apps/electron-backend/project.json", "{workspaceRoot}/apps/electron-backend/native/src/embedded_mpv_win32.cc", @@ -52,10 +53,14 @@ "{workspaceRoot}/tools/embedded-mpv/runtime-probe-contract.cjs", "{workspaceRoot}/tools/embedded-mpv/runtime-probe-contract.d.cts", "{workspaceRoot}/tools/embedded-mpv/stage-runtime.mjs", - "{workspaceRoot}/tools/embedded-mpv/stage-windows-runtime-archive.mjs" + "{workspaceRoot}/tools/embedded-mpv/stage-windows-runtime-archive.mjs", + "{workspaceRoot}/tools/embedded-mpv/update-windows-runtime-pin.mjs", + "{workspaceRoot}/tools/embedded-mpv/windows-runtime-pin.json", + "{workspaceRoot}/tools/embedded-mpv/windows-runtime-pin.mjs", + "{workspaceRoot}/tools/embedded-mpv/windows-runtime-pin.test.mjs" ], "options": { - "command": "node --test tools/packaging/electron-package-identity.test.mjs tools/packaging/asar-dependency-closure.test.mjs tools/packaging/embedded-mpv-arch.test.mjs tools/packaging/flatpak-launcher-validation.test.mjs tools/packaging/configure-linux-frame-copy-build.test.mjs tools/packaging/linux-after-pack.test.mjs tools/packaging/linux-frame-copy-profile.test.mjs tools/packaging/prepare-linux-runtime-source-snapshot.test.mjs tools/packaging/publish-snap-workflow.test.mjs tools/packaging/release-snap-assets.test.mjs tools/packaging/verify-linux-frame-copy-runtime.test.mjs tools/embedded-mpv/build-linux-runtime.test.mjs tools/embedded-mpv/download-pinned-source.test.mjs tools/embedded-mpv/generate-linux-runtime-notices.test.mjs tools/embedded-mpv/linux-runtime-manifest.test.mjs", + "command": "node --test tools/packaging/electron-package-identity.test.mjs tools/packaging/asar-dependency-closure.test.mjs tools/packaging/embedded-mpv-arch.test.mjs tools/packaging/flatpak-launcher-validation.test.mjs tools/packaging/configure-linux-frame-copy-build.test.mjs tools/packaging/linux-after-pack.test.mjs tools/packaging/linux-frame-copy-profile.test.mjs tools/packaging/prepare-linux-runtime-source-snapshot.test.mjs tools/packaging/publish-snap-workflow.test.mjs tools/packaging/release-snap-assets.test.mjs tools/packaging/verify-linux-frame-copy-runtime.test.mjs tools/embedded-mpv/build-linux-runtime.test.mjs tools/embedded-mpv/download-pinned-source.test.mjs tools/embedded-mpv/generate-linux-runtime-notices.test.mjs tools/embedded-mpv/linux-runtime-manifest.test.mjs tools/embedded-mpv/windows-runtime-pin.test.mjs", "cwd": "{workspaceRoot}" } }, @@ -67,6 +72,7 @@ "{workspaceRoot}/tools/eslint/**/*", "{workspaceRoot}/.github/workflows/build-and-make.yaml", "{workspaceRoot}/.github/workflows/publish-snap.yaml", + "{workspaceRoot}/.github/workflows/refresh-windows-embedded-mpv-runtime.yaml", "{workspaceRoot}/tools/packaging/prepare-linux-runtime-source-snapshot.cjs", "{workspaceRoot}/tools/packaging/prepare-linux-runtime-source-snapshot.test.mjs", "{workspaceRoot}/tools/packaging/publish-snap-workflow.test.mjs", @@ -81,9 +87,13 @@ "{workspaceRoot}/tools/embedded-mpv/linux-source-archive-contract.cjs", "{workspaceRoot}/tools/embedded-mpv/linux-source-archive-contract.d.cts", "{workspaceRoot}/tools/embedded-mpv/runtime-probe-contract.cjs", - "{workspaceRoot}/tools/embedded-mpv/runtime-probe-contract.d.cts" + "{workspaceRoot}/tools/embedded-mpv/runtime-probe-contract.d.cts", + "{workspaceRoot}/tools/embedded-mpv/stage-windows-runtime-archive.mjs", + "{workspaceRoot}/tools/embedded-mpv/update-windows-runtime-pin.mjs", + "{workspaceRoot}/tools/embedded-mpv/windows-runtime-pin.mjs", + "{workspaceRoot}/tools/embedded-mpv/windows-runtime-pin.test.mjs" ], - "command": "eslint \"tools/packaging/**/*.{js,cjs,mjs,ts}\" \"tools/embedded-mpv/build-macos-runtime.mjs\" \"tools/embedded-mpv/build-linux-runtime.{mjs,test.mjs}\" \"tools/embedded-mpv/download-pinned-source.{mjs,test.mjs}\" \"tools/embedded-mpv/generate-linux-runtime-notices.{cjs,test.mjs}\" \"tools/embedded-mpv/linux-source-archive-contract.cjs\" \"tools/embedded-mpv/runtime-probe-contract.cjs\"" + "command": "eslint \"tools/packaging/**/*.{js,cjs,mjs,ts}\" \"tools/embedded-mpv/build-macos-runtime.mjs\" \"tools/embedded-mpv/build-linux-runtime.{mjs,test.mjs}\" \"tools/embedded-mpv/download-pinned-source.{mjs,test.mjs}\" \"tools/embedded-mpv/generate-linux-runtime-notices.{cjs,test.mjs}\" \"tools/embedded-mpv/linux-source-archive-contract.cjs\" \"tools/embedded-mpv/runtime-probe-contract.cjs\" \"tools/embedded-mpv/stage-windows-runtime-archive.mjs\" \"tools/embedded-mpv/update-windows-runtime-pin.mjs\" \"tools/embedded-mpv/windows-runtime-pin.{mjs,test.mjs}\"" } }, "tags": ["scope:tools", "domain:packaging", "type:tool"]