fix(release): align public Snap verifier with the shipped snap layout

The publish-snap verifier had never run against a real release and
encoded three stale expectations that the tag build's own validators do
not share:

- it required the app under usr/lib/iptvnator inside the snap, while
  Electron Builder's snap target ships the app at the snap root
  (/iptvnator.bin, /resources/**) — the layout the packaged smoke tests
  exercise;
- it validated the source archive's runtime manifest with the raw
  source-build validator, but the archive carries the STAGED manifest
  (origin "vendored-lgpl" + sourceBuildOrigin) written by
  stage-runtime.mjs; the staged envelope is now checked explicitly and
  the remaining fields still go through the shared validator via an
  origin projection;
- it deep-equaled the snap's bundled sourceRuntime against the archive
  manifest, but the snap bundles the builder view (no staging
  envelope); the binding now projects the envelope away first.

Verified end-to-end in a Linux container against the real v0.23.0
release assets: release-snap-assets.cjs verify now passes and emits the
sealed snapshot receipt. Regression tests cover the legacy usr/lib
layout and staged-envelope mismatches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-08-30 11:07:02 +02:00
1 parent f52337d05e
commit 0adf562177
5 files changed
+138 -35

No files matched your search

+3 -2
View File
@@ -1058,8 +1058,9 @@ hashes, the exact VCS-free libplacebo tree inventory/digest, and byte-identical
tooling from the released tag. Checkout and both artifact-transfer actions use
full pinned commits, and checkout sets `persist-credentials: false`.
The bounded SquashFS preflight and extraction then require the canonical
`/usr/lib/iptvnator` layout and reuse the static package validator for every
selected Snap. The public-release verifier separately reapplies the exact
snap-root layout (Electron app at `/`, so `/iptvnator.bin` and
`/resources/**` — the layout Electron Builder's Snap target produces) and
reuse the static package validator for every selected Snap. The public-release verifier separately reapplies the exact
strict `meta/snap.yaml` graphics/shared-memory/layout contract and enumerates
the extracted `resources/app.asar`; any archived
`electron-backend/native/**` entry fails before Store publication. The bounded
+3 -2
View File
@@ -395,8 +395,9 @@ inventory/digest, released tooling, and runtime manifest. Checkout and both
artifact-transfer actions use full pinned commits, and checkout does not
persist its repository credential. The verifier bounds
source members, the archive, SquashFS listing, extracted size, entry count,
command time, and job time; every Snap must use the canonical
`/usr/lib/iptvnator` layout and pass the existing static package validator.
command time, and job time; every Snap must use the canonical snap-root
layout (Electron app at `/`, so `/iptvnator.bin` and `/resources/**`) and
pass the existing static package validator.
The public-release boundary also reapplies the exact strict
`meta/snap.yaml` graphics/shared-memory/layout contract and enumerates the
extracted `resources/app.asar`, rejecting any archived
+87 -4
View File
@@ -187,6 +187,9 @@ function createSourceBindingFixture() {
const repositoryRevision = 'a'.repeat(40);
const archiveSha256 = 'b'.repeat(64);
const sourceRuntime = {
origin: 'vendored-lgpl',
sourceBuildOrigin: 'vendored-lgpl-source-build',
stagedAt: '2026-07-18T01:00:00.000Z',
generatedAt: '2026-07-18T00:00:00.000Z',
packages: {
ffmpeg: {
@@ -238,6 +241,14 @@ function createSourceBindingFixture() {
archiveFiles: sourceIndex.archives,
compliance: sourceCompliance,
};
// The snap bundles the from-source builder view of the staged manifest:
// origin restored, staging envelope (sourceBuildOrigin, stagedAt) absent.
const snapSourceRuntime = {
...sourceRuntime,
origin: 'vendored-lgpl-source-build',
};
delete snapSourceRuntime.sourceBuildOrigin;
delete snapSourceRuntime.stagedAt;
const snapPayloads = {
'IPTVnator-amd64.snap': {
assetName: 'IPTVnator-amd64.snap',
@@ -255,7 +266,7 @@ function createSourceBindingFixture() {
sha256: 'd'.repeat(64),
repositoryRevision,
},
sourceRuntime,
sourceRuntime: snapSourceRuntime,
},
},
'IPTVnator-arm64.snap': {
@@ -316,6 +327,37 @@ test('binds source metadata and checksums to every selected Snap before publicat
inspectedSnaps,
selection.snapAssets.map(({ name }) => name)
);
for (const [field, value] of [
['origin', 'vendored-lgpl-source-build'],
['sourceBuildOrigin', undefined],
]) {
const stagedMismatch = structuredClone(fixture.sourceInspection);
if (value === undefined) {
delete stagedMismatch.sourceRuntime[field];
} else {
stagedMismatch.sourceRuntime[field] = value;
}
assert.throws(
() =>
helper.verifySnapReleaseCorrespondence(
selection,
temporaryRoot,
{
expectedRepositoryRevision: fixture.repositoryRevision,
expectedSourceSnapshotSha256:
fixture.expectedSourceSnapshotSha256,
inspectSourceArchive: () => stagedMismatch,
inspectSnapPayload: (_snapPath, asset) =>
fixture.snapPayloads[asset.name],
validateRuntimeManifest: () => [],
}
),
field === 'origin'
? /origin must be "vendored-lgpl"/
: /sourceBuildOrigin must be "vendored-lgpl-source-build"/
);
}
});
test('publishes only a stable verified asset snapshot with an exact receipt', async (t) => {
@@ -1180,7 +1222,7 @@ test('hashes the final source archive bytes and reads the exact packaged Snap bi
fs.unlinkSync(oversizedMemberPath);
const snapSourceRoot = path.join(temporaryRoot, 'snap-source');
const appRoot = path.join(snapSourceRoot, 'usr', 'lib', 'iptvnator');
const appRoot = snapSourceRoot;
const nativeRoot = path.join(
appRoot,
'resources',
@@ -1193,10 +1235,16 @@ test('hashes the final source archive bytes and reads the exact packaged Snap bi
electronHeader.set([0x7f, 0x45, 0x4c, 0x46, 2, 1]);
electronHeader.writeUInt16LE(62, 18);
fs.writeFileSync(path.join(appRoot, 'iptvnator.bin'), electronHeader);
const packagedSourceRuntime = {
...sourceRuntime,
origin: 'vendored-lgpl-source-build',
};
delete packagedSourceRuntime.sourceBuildOrigin;
delete packagedSourceRuntime.stagedAt;
const packagedManifest = {
...fixture.snapPayloads['IPTVnator-amd64.snap'].manifest,
sourceArchive,
sourceRuntime,
sourceRuntime: packagedSourceRuntime,
};
fs.writeFileSync(
path.join(nativeRoot, 'embedded-mpv-runtime.json'),
@@ -1326,7 +1374,7 @@ test('hashes the final source archive bytes and reads the exact packaged Snap bi
assert.equal(staticValidationCalls.length, 1);
assert.ok(
staticValidationCalls[0].resourceDirectory.endsWith(
['payload', 'usr', 'lib', 'iptvnator', 'resources'].join(path.sep)
['payload', 'resources'].join(path.sep)
)
);
assert.deepEqual(staticValidationCalls[0].options, {
@@ -1484,6 +1532,41 @@ test('hashes the final source archive bytes and reads the exact packaged Snap bi
/canonical frame-copy manifest/i
);
const legacySourceRoot = path.join(temporaryRoot, 'legacy-snap-source');
const legacyAppRoot = path.join(
legacySourceRoot,
'usr',
'lib',
'iptvnator'
);
fs.cpSync(appRoot, legacyAppRoot, { recursive: true });
assert.throws(
() =>
sourceBindingHelper.inspectSnapPayload(
snapPath,
{ id: 1, name: 'IPTVnator-amd64.snap' },
{
runCommand: (command, args) => {
assert.equal(command, 'unsquashfs');
if (args[0] === '-lln') {
return syntheticSquashfsListing();
}
const destinationIndex = args.indexOf('-dest') + 1;
fs.cpSync(legacySourceRoot, args[destinationIndex], {
recursive: true,
});
return '';
},
validatePackagedEmbeddedMpv: () => {
throw new Error(
'Static validation must not inspect a legacy usr/lib layout.'
);
},
}
),
/canonical frame-copy manifest/i
);
const manifestPath = path.join(nativeRoot, 'embedded-mpv-runtime.json');
fs.truncateSync(manifestPath, 16 * 1024 * 1024 + 1);
let oversizedStaticValidationCalls = 0;
+44 -19
View File
@@ -41,6 +41,12 @@ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
const SAFE_BASENAME_PATTERN = /^[A-Za-z0-9_+.-]+$/;
const FRAME_COPY_MANIFEST_NAME = 'embedded-mpv-runtime.json';
const FRAME_COPY_UNAVAILABLE_MARKER_NAME = 'embedded-mpv-unavailable.txt';
// The archive carries the STAGED runtime manifest (`stage-runtime.mjs`):
// origin "vendored-lgpl", with the from-source builder's origin preserved in
// sourceBuildOrigin. The shared source-build validator still checks every
// other field through the projected origin below.
const STAGED_RUNTIME_ORIGIN = 'vendored-lgpl';
const SOURCE_BUILD_RUNTIME_ORIGIN = 'vendored-lgpl-source-build';
const SNAP_RELEASE_BOUNDARY_SCHEMA_VERSION = 1;
const SNAP_RELEASE_BOUNDARY_HELPER_PATH = path.join(
__dirname,
@@ -1116,9 +1122,6 @@ function inspectSnapPayload(
const payloads = collectSnapNativePayloads(extractionRoot);
const canonicalNativeDirectory = path.join(
extractionRoot,
'usr',
'lib',
'iptvnator',
'resources',
'app.asar.unpacked',
'electron-backend',
@@ -1148,20 +1151,8 @@ function inspectSnapPayload(
);
}
const payloadPath = canonicalPayloads[0];
const resourcesDirectory = path.join(
extractionRoot,
'usr',
'lib',
'iptvnator',
'resources'
);
const electronPath = path.join(
extractionRoot,
'usr',
'lib',
'iptvnator',
'iptvnator.bin'
);
const resourcesDirectory = path.join(extractionRoot, 'resources');
const electronPath = path.join(extractionRoot, 'iptvnator.bin');
const electronStat = fs.lstatSync(electronPath);
if (
!electronStat.isFile() ||
@@ -1562,7 +1553,26 @@ function verifySnapReleaseSourceBinding(
sourceIndex,
sourceRuntime,
} = sourceInspection;
const runtimeErrors = validateRuntimeManifest(sourceRuntime);
const stagedRuntimeErrors = [];
if (sourceRuntime?.origin !== STAGED_RUNTIME_ORIGIN) {
stagedRuntimeErrors.push(
`Linux runtime manifest origin must be "${STAGED_RUNTIME_ORIGIN}".`
);
}
if (sourceRuntime?.sourceBuildOrigin !== SOURCE_BUILD_RUNTIME_ORIGIN) {
stagedRuntimeErrors.push(
`Linux runtime manifest sourceBuildOrigin must be "${SOURCE_BUILD_RUNTIME_ORIGIN}".`
);
}
const validatorErrors = isObject(sourceRuntime)
? validateRuntimeManifest({
...sourceRuntime,
origin: SOURCE_BUILD_RUNTIME_ORIGIN,
})
: validateRuntimeManifest(sourceRuntime);
const runtimeErrors = Array.isArray(validatorErrors)
? [...stagedRuntimeErrors, ...validatorErrors]
: validatorErrors;
if (!Array.isArray(runtimeErrors) || runtimeErrors.length > 0) {
throw new Error(
`Source archive runtime manifest is invalid${
@@ -1742,7 +1752,22 @@ function verifySnapReleaseSourceBinding(
`x64 Snap ${payload.assetName} has an invalid frame-copy source archive binding.`
);
}
if (!isDeepStrictEqual(manifest.sourceRuntime, sourceRuntime)) {
// The snap bundles the from-source builder view of the runtime
// manifest, while the archive carries its staged envelope
// (origin renamed to "vendored-lgpl", sourceBuildOrigin and
// stagedAt added). Project the envelope away before binding.
const expectedSnapSourceRuntime = {
...sourceRuntime,
origin: SOURCE_BUILD_RUNTIME_ORIGIN,
};
delete expectedSnapSourceRuntime.sourceBuildOrigin;
delete expectedSnapSourceRuntime.stagedAt;
if (
!isDeepStrictEqual(
manifest.sourceRuntime,
expectedSnapSourceRuntime
)
) {
throw new Error(
`Snap source runtime does not match source archive: ${payload.assetName}`
);
@@ -18,14 +18,7 @@ export function validateExtractedSnapReleaseBoundary(
{ asarListPackage = listAsarPackageEntries } = {}
) {
const errors = [...validateExtractedSnapMetadata(extractionRoot)];
const asarPath = path.join(
extractionRoot,
'usr',
'lib',
'iptvnator',
'resources',
'app.asar'
);
const asarPath = path.join(extractionRoot, 'resources', 'app.asar');
let asarStat;
try {
asarStat = fs.lstatSync(asarPath);