mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
ci(embedded-mpv): keep Windows runtime pin available (#1495)
This commit is contained in:
1 parent
29ca94aa43
commit
f04f67728e
14 files changed
+972
-75
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 <archive-url-or-path> <sha256>',
|
||||
'',
|
||||
'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.',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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 ?? '<unknown>'} 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);
|
||||
});
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
@@ -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/);
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in new issue
Block a user