fix(embedded-mpv): unbreak the Windows runtime pin and the refresh that abandoned it (#1656)

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 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5 authored and GitHub committed 2026-09-21 19:19:05 +02:00
1 parent faad8fd8fd
commit 11358caa7a
4 files changed
+98 -29

No files matched your search

+4 -5
View File
@@ -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.
+12
View File
@@ -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
+7 -7
View File
@@ -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."
}
}
+75 -17
View File
@@ -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 });
}