From 11358caa7a8008f72c4159b927b951125fed3094 Mon Sep 17 00:00:00 2001 From: 4gray <4gray@users.noreply.github.com> Date: Mon, 21 Sep 2026 19:19:05 +0200 Subject: [PATCH] fix(embedded-mpv): unbreak the Windows runtime pin and the refresh that abandoned it (#1656) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The checked-in Windows Embedded MPV runtime pin pointed at an upstream release whose 30-day retention had expired, so `Build on windows x64` failed repository-wide the moment a runner's cache went cold: Unable to download Windows embedded MPV runtime archive: 404 Not Found The weekly refresh exists to rotate the pin well before that boundary, and it had been red since 2026-09-07 — because its own validation step could only pass while the pin was stale. `createPinFixture()` copied the CHECKED-IN pin into the temp dir, and the age-threshold test then asserted, against a clock frozen at 2026-09-05, that this pin was at least 14 days old. So every successful rotation invalidated the assertion that guarded it: the step went red, the bot pull request was never opened, and the pin was left to expire. Build the refresh fixtures from the synthetic release fixture at an age the test chooses (`createPinFixture({ ageDays })`) instead. The three refresh tests are about the rule, so the rule is now what they exercise — one day under the threshold is left alone, exactly at the threshold rotates, and the unavailable case is deliberately young so only the 404 can explain it — and the outcome no longer depends on production data that this job exists to replace. `CURRENT_PIN` stays with the schema, naming and licence-statement checks, which hold for any pin. A new case pins the invariant the job actually needs: a pin written moments ago must read as current on the next run. Rotate the pin to the newest upstream LGPL x86_64 release. Verified end-to-end: the archive downloads (27 MB) and its SHA-256 matches the pinned digest. The binary stays on its upstream host; IPTVnator does not mirror it and the limited checksum/layout-only licence statement is preserved verbatim. Also drops three catalog titles from a pending TMDB release note: notes are published verbatim into CHANGELOG.md, the GitHub release body and the announcement drafts, so the example now names the letter rather than the shows. No release note: CI plumbing under `tools/`, outside the gate's `apps/`+`libs/` trigger, with no user-visible behaviour change. Co-Authored-By: Claude Opus 5 --- .changes/tmdb-cyrillic-search-spelling.md | 9 +- tools/embedded-mpv/README.md | 12 +++ tools/embedded-mpv/windows-runtime-pin.json | 14 +-- .../embedded-mpv/windows-runtime-pin.test.mjs | 92 +++++++++++++++---- 4 files changed, 98 insertions(+), 29 deletions(-) diff --git a/.changes/tmdb-cyrillic-search-spelling.md b/.changes/tmdb-cyrillic-search-spelling.md index bd7fb6d05..7ea93f6d2 100644 --- a/.changes/tmdb-cyrillic-search-spelling.md +++ b/.changes/tmdb-cyrillic-search-spelling.md @@ -3,8 +3,7 @@ type: fix area: tmdb --- -Russian titles containing "й" or "ё" ("Фейк", "Волшебный участок", "Молодой -Шерлок") and Arabic titles with hamza letters ("أطرق بابي") now match on TMDB. -The search used to send a folded spelling ("феик") that TMDB never recognised -and cached the miss for a week; those cached misses are cleared on the next -start. +Russian titles containing "й" or "ё" and Arabic titles with hamza letters now +match on TMDB. The search used to send a folded spelling — dropping the breve +from "й", for instance — that TMDB never recognised, and cached the miss for a +week; those cached misses are cleared on the next start. diff --git a/tools/embedded-mpv/README.md b/tools/embedded-mpv/README.md index fc4c423ae..fca1985c6 100644 --- a/tools/embedded-mpv/README.md +++ b/tools/embedded-mpv/README.md @@ -103,6 +103,18 @@ updater: pnpm embedded-mpv:windows-runtime-pin:refresh -- --force ``` +That workflow validates its own result with +`windows-runtime-pin.test.mjs`, so nothing in those refresh tests may read the +CHECKED-IN pin's `publishedAt`. Building a fixture from it makes the outcome a +function of the very data the job replaces, and the assertion then fails +exactly when the job succeeds: a rotation asserted that the pin it had just +written was already past the 14-day threshold, the validation step went red, +the bot PR was never opened, and the pin sat until upstream retention deleted +its asset — turning every Windows build red on a cold cache. Refresh tests +build their own pin at a chosen age (`createPinFixture({ ageDays })`); +`CURRENT_PIN` is for the schema, naming and licence-statement checks, which +hold for any pin. + 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 diff --git a/tools/embedded-mpv/windows-runtime-pin.json b/tools/embedded-mpv/windows-runtime-pin.json index 8305b6373..8b088058e 100644 --- a/tools/embedded-mpv/windows-runtime-pin.json +++ b/tools/embedded-mpv/windows-runtime-pin.json @@ -1,17 +1,17 @@ { "schemaVersion": 1, "repository": "zhongfly/mpv-winbuild", - "releaseTag": "2026-08-21-49418246f3", - "publishedAt": "2026-08-21T12:20:41Z", + "releaseTag": "2026-09-21-e76a35ec95", + "publishedAt": "2026-09-21T12:23:42Z", "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" + "name": "mpv-dev-lgpl-x86_64-20260921-git-e76a35ec95.7z", + "url": "https://github.com/zhongfly/mpv-winbuild/releases/download/2026-09-21-e76a35ec95/mpv-dev-lgpl-x86_64-20260921-git-e76a35ec95.7z", + "sha256": "e0b8202aa8269795e41076898fd5ec0a9038ae8267a42d6fbbe94c727d586f33" }, "upstream": { - "mpvCommit": "49418246f30a9c24af31ac184aa24f39755db89a", - "buildRunUrl": "https://github.com/zhongfly/mpv-winbuild/actions/runs/32479875364", + "mpvCommit": "e76a35ec95b27f5cf2d27b043b5e2e0d90e468ae", + "buildRunUrl": "https://github.com/zhongfly/mpv-winbuild/actions/runs/35597141455", "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.test.mjs b/tools/embedded-mpv/windows-runtime-pin.test.mjs index bbf95961a..e518f77eb 100644 --- a/tools/embedded-mpv/windows-runtime-pin.test.mjs +++ b/tools/embedded-mpv/windows-runtime-pin.test.mjs @@ -65,11 +65,44 @@ function response({ ok = true, status = 200, json } = {}) { }; } -function createPinFixture() { +/** The clock every refresh-rule test reasons against. */ +const REFRESH_CLOCK = new Date('2026-08-29T00:00:00Z'); +const DAY_MS = 86_400_000; + +/** + * A checked-out pin whose AGE the test chooses, built from the synthetic + * release fixture rather than from `CURRENT_PIN`. + * + * The refresh tests are about the rule, so they must not read the checked-in + * pin's publish date. Reading it makes the outcome a function of production + * data that this very job exists to replace, and the assertion then fails + * precisely when the job succeeds: the weekly refresh has been red since + * 2026-09-07 because its validation step asserted that the pin it had just + * rotated was already over two weeks old, so the rotation never reached a + * pull request and the checked-in pin was left to expire out of upstream + * retention. `CURRENT_PIN` stays where it belongs — the schema, naming and + * licence-statement checks, which exist to validate the checked-in file and + * hold for any pin. + * + * The fixture's commit differs from `releaseFixture()`'s default so a + * rotation is observable as a change. + */ +function createPinFixture({ + ageDays = 0, + commit = 'a1b2c3d4e5123456789012345678901234567890', +} = {}) { + const publishedAt = new Date(REFRESH_CLOCK.getTime() - ageDays * DAY_MS); + const pin = pinFromUpstreamRelease( + releaseFixture({ + date: publishedAt.toISOString().slice(0, 10), + commit, + publishedAt: publishedAt.toISOString(), + }) + ); 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 }; + fs.writeFileSync(pinPath, serializeWindowsRuntimePin(pin)); + return { root, pinPath, pin }; } test('checked-in Windows runtime pin is internally consistent', () => { @@ -124,13 +157,15 @@ test('upstream release must provide GitHub digest and build evidence', () => { ); }); -test('young available pin does not query releases or rewrite the file', async () => { - const fixture = createPinFixture(); +test('available pin one day under the threshold is left alone', async () => { + const fixture = createPinFixture({ + ageDays: WINDOWS_RUNTIME_REFRESH_AFTER_DAYS - 1, + }); let requestCount = 0; try { const result = await refreshWindowsRuntimePin({ pinPath: fixture.pinPath, - now: new Date('2026-08-29T00:00:00Z'), + now: REFRESH_CLOCK, fetchImpl: async () => { requestCount += 1; return response(); @@ -141,24 +176,42 @@ test('young available pin does not query releases or rewrite the file', async () assert.equal(requestCount, 1); assert.equal( fs.readFileSync(fixture.pinPath, 'utf8'), - serializeWindowsRuntimePin(CURRENT_PIN) + serializeWindowsRuntimePin(fixture.pin) ); } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); } }); +test('a pin rotated moments ago does not immediately ask to rotate again', async () => { + // The invariant the refresh job depends on: its own freshly written pin + // must read as current on the next run, whatever date that run falls on. + const fixture = createPinFixture({ ageDays: 0 }); + try { + const result = await refreshWindowsRuntimePin({ + pinPath: fixture.pinPath, + now: REFRESH_CLOCK, + fetchImpl: async () => response(), + }); + assert.equal(result.reason, 'current'); + assert.equal(result.changed, false); + } finally { + fs.rmSync(fixture.root, { recursive: true, force: true }); + } +}); + test('unavailable pin rotates to the newest downloadable release', async () => { - const fixture = createPinFixture(); + // Deliberately young, so only the 404 can explain the rotation. + const fixture = createPinFixture({ ageDays: 1 }); const latest = releaseFixture(); const requests = []; try { const result = await refreshWindowsRuntimePin({ pinPath: fixture.pinPath, - now: new Date('2026-08-29T00:00:00Z'), + now: REFRESH_CLOCK, fetchImpl: async (url, options = {}) => { requests.push([url, options.method ?? 'GET']); - if (url === CURRENT_PIN.asset.url) { + if (url === fixture.pin.asset.url) { return response({ ok: false, status: 404 }); } if (url.startsWith('https://api.github.com/')) { @@ -183,24 +236,29 @@ test('unavailable pin rotates to the newest downloadable release', async () => { }); test('age threshold rotates an available pin before upstream retention', async () => { - const fixture = createPinFixture(); + const fixture = createPinFixture({ + ageDays: WINDOWS_RUNTIME_REFRESH_AFTER_DAYS, + }); const latest = releaseFixture(); try { + assert.equal( + runtimePinAgeDays(fixture.pin, REFRESH_CLOCK), + WINDOWS_RUNTIME_REFRESH_AFTER_DAYS + ); const result = await refreshWindowsRuntimePin({ pinPath: fixture.pinPath, - now: new Date('2026-09-05T13:00:00Z'), + now: REFRESH_CLOCK, 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'); + assert.equal( + readWindowsRuntimePin(fixture.pinPath).releaseTag, + latest.tag_name + ); } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }