Files
iptvnator/tools/packaging/release-snap-assets.test.mjs
T
4gray 728df1a68c fix(packaging): restore Snap desktop runtime (#1406)
* fix(packaging): restore Snap desktop runtime

* docs(packaging): publish Snap launch repair note

* fix(packaging): declare Node 22.12 floor

* docs(architecture): update SQLite pin rationale

* fix(tooling): align Node engine floor

* fix(tooling): constrain supported Node releases

* docs(architecture): correct node-abi consumer
2026-08-11 02:08:30 +02:00

1659 lines
57 KiB
JavaScript

import assert from 'node:assert/strict';
import childProcess from 'node:child_process';
import crypto from 'node:crypto';
import fs from 'node:fs';
import { createRequire } from 'node:module';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { parse } from 'yaml';
const require = createRequire(import.meta.url);
const { createPackage: createAsarPackage } = require('@electron/asar');
const workspaceRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'..',
'..'
);
const helperPath = path.join(
workspaceRoot,
'tools',
'packaging',
'release-snap-assets.cjs'
);
const sourceBindingHelperPath = path.join(
workspaceRoot,
'tools',
'packaging',
'release-snap-source-binding.cjs'
);
const sourceArchiveContractPath = path.join(
workspaceRoot,
'tools',
'embedded-mpv',
'linux-source-archive-contract.cjs'
);
const publishWorkflowPath = path.join(
workspaceRoot,
'.github',
'workflows',
'publish-snap.yaml'
);
async function loadHelper() {
return import(pathToFileURL(helperPath).href);
}
async function loadSourceBindingHelper() {
return import(pathToFileURL(sourceBindingHelperPath).href);
}
async function loadSourceArchiveContract() {
return import(pathToFileURL(sourceArchiveContractPath).href);
}
function syntheticSquashfsListing() {
return [
'drwxr-xr-x 0/0 0 2026-07-18 00:00 squashfs-root',
'-rw-r--r-- 0/0 1 2026-07-18 00:00 squashfs-root/payload',
'',
].join('\n');
}
function sha256(contents) {
return crypto.createHash('sha256').update(contents).digest('hex');
}
const SYNTHETIC_LIBPLACEBO_CONTENTS = Buffer.from(
'libplacebo source snapshot\n'
);
const SYNTHETIC_LIBPLACEBO_ENTRIES = Object.freeze([
Object.freeze({
path: 'README.md',
type: 'file',
size: SYNTHETIC_LIBPLACEBO_CONTENTS.length,
executable: false,
sha256: sha256(SYNTHETIC_LIBPLACEBO_CONTENTS),
}),
]);
const SYNTHETIC_LIBPLACEBO_SOURCE_SNAPSHOT = (() => {
const canonical = {
schemaVersion: 1,
entryCount: SYNTHETIC_LIBPLACEBO_ENTRIES.length,
totalBytes: SYNTHETIC_LIBPLACEBO_CONTENTS.length,
entries: SYNTHETIC_LIBPLACEBO_ENTRIES,
};
return Object.freeze({
schemaVersion: canonical.schemaVersion,
sha256: sha256(`${JSON.stringify(canonical)}\n`),
entryCount: canonical.entryCount,
totalBytes: canonical.totalBytes,
entries: SYNTHETIC_LIBPLACEBO_ENTRIES,
});
})();
const EXPECTED_LIBPLACEBO_SOURCE_SUBMODULES = Object.freeze([
'450bd2232225d6c7728a4108055ac2e37cef6475 3rdparty/Vulkan-Headers',
'97b54ca9e75f5303507699d27c6b4f4efe4641a1 3rdparty/fast_float',
'73db193f853e2ee079bf3ca8a64aa2eaf6459043 3rdparty/glad',
'15206881c006c79667fe5154fe80c01c65410679 3rdparty/jinja',
'297fc8e356e6836a62087949245d09a28e9f1b13 3rdparty/markupsafe',
'242f35efa067a46c595645eeda7b1771ea1f83b1 demos/3rdparty/nuklear',
]);
function sourcePackageIdentity(sourcePackage) {
return Object.fromEntries(
[
'version',
'sourceUrl',
'sourceTag',
'sourceSha256',
'sourceGitCommit',
'license',
]
.filter((field) => Object.hasOwn(sourcePackage, field))
.map((field) => [field, sourcePackage[field]])
);
}
function createComplianceInspection(sourceRuntime) {
const licenseInputFiles = [];
const noticeLicenseFiles = [];
const licenseInputPackages = [];
const noticePackages = [];
for (const [id, sourcePackage] of Object.entries(sourceRuntime.packages)) {
const contents = Buffer.from(`${id} license\n`);
const file = {
path: `licenses/${id}/LICENSE`,
size: contents.length,
sha256: sha256(contents),
};
licenseInputFiles.push(file);
noticeLicenseFiles.push(file);
licenseInputPackages.push({
id,
...sourcePackageIdentity(sourcePackage),
files: [
{
sourcePath: 'LICENSE',
...file,
},
],
});
noticePackages.push({
id,
...sourcePackageIdentity(sourcePackage),
files: [file],
});
}
const aggregateNoticeContents = Buffer.from('third-party notices\n');
const noticeFile = {
path: 'THIRD_PARTY_NOTICES.txt',
size: aggregateNoticeContents.length,
sha256: sha256(aggregateNoticeContents),
};
return {
aggregateNoticeContents,
libplaceboSourceSnapshot: SYNTHETIC_LIBPLACEBO_SOURCE_SNAPSHOT,
licenseInputFiles,
licenseInputs: {
schemaVersion: 1,
origin: 'pinned-linux-runtime-license-inputs',
platform: 'linux',
arch: 'x64',
packages: licenseInputPackages,
},
noticeFile,
noticeLicenseFiles,
notices: {
schemaVersion: 1,
origin: 'pinned-linux-runtime-upstream-licenses',
platform: 'linux',
arch: 'x64',
noticeFile,
packages: noticePackages,
totalBytes:
noticeFile.size +
noticeLicenseFiles.reduce(
(total, file) => total + file.size,
0
),
},
toolingValidated: true,
};
}
function createSourceBindingFixture() {
const repositoryRevision = 'a'.repeat(40);
const archiveSha256 = 'b'.repeat(64);
const sourceRuntime = {
generatedAt: '2026-07-18T00:00:00.000Z',
packages: {
ffmpeg: {
version: '1.0.0',
sourceUrl: 'https://example.test/ffmpeg.tar.xz',
sourceSha256: archiveSha256,
license: 'LGPL-2.1-or-later',
},
libplacebo: {
version: '2.0.0',
sourceUrl: 'https://example.test/libplacebo.git',
sourceTag: 'v2.0.0',
sourceGitCommit: 'c'.repeat(40),
sourceSubmodules: [...EXPECTED_LIBPLACEBO_SOURCE_SUBMODULES],
license: 'LGPL-2.1-or-later',
},
},
};
const compliance = createComplianceInspection(sourceRuntime);
const sourceCompliance = { ...compliance };
delete sourceCompliance.aggregateNoticeContents;
const sourceIndex = {
schemaVersion: 3,
repositoryRevision,
sourcePackages: sourceRuntime.packages,
archives: [
{
name: 'ffmpeg.tar.xz',
sha256: archiveSha256,
},
],
libplacebo: {
sourceGitCommit: sourceRuntime.packages.libplacebo.sourceGitCommit,
sourceSubmodules: [...EXPECTED_LIBPLACEBO_SOURCE_SUBMODULES],
sourceSnapshot: SYNTHETIC_LIBPLACEBO_SOURCE_SNAPSHOT,
},
legal: {
manifest: 'notices/embedded-mpv-notices.json',
noticeFile: compliance.notices.noticeFile,
packages: compliance.notices.packages,
},
};
const sourceInspection = {
archiveSha256: 'd'.repeat(64),
sourceRuntime,
sourceIndex,
repositoryRevision,
localChanges: Buffer.alloc(0),
archiveFiles: sourceIndex.archives,
compliance: sourceCompliance,
};
const snapPayloads = {
'IPTVnator-amd64.snap': {
assetName: 'IPTVnator-amd64.snap',
architecture: 'x64',
markerOnly: false,
manifest: {
platform: 'linux',
arch: 'x64',
profile: 'portable',
runtimeMode: 'bundled',
targets: ['appimage', 'snap'],
sourceArchive: {
schemaVersion: 1,
name: 'linux-frame-copy-runtime-sources.tar.xz',
sha256: 'd'.repeat(64),
repositoryRevision,
},
sourceRuntime,
},
},
'IPTVnator-arm64.snap': {
assetName: 'IPTVnator-arm64.snap',
architecture: 'arm64',
markerOnly: true,
manifest: null,
},
};
return {
expectedSourceSnapshotSha256:
SYNTHETIC_LIBPLACEBO_SOURCE_SNAPSHOT.sha256,
repositoryRevision,
sourceInspection,
snapPayloads,
};
}
test('binds source metadata and checksums to every selected Snap before publication', async (t) => {
const helper = await loadHelper();
const fixture = createSourceBindingFixture();
const temporaryRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-snap-source-binding-')
);
t.after(() => fs.rmSync(temporaryRoot, { recursive: true, force: true }));
const selection = {
snapAssets: [
{ id: 1, name: 'IPTVnator-amd64.snap' },
{ id: 2, name: 'IPTVnator-arm64.snap' },
],
sourceAsset: {
id: 3,
name: 'linux-frame-copy-runtime-sources.tar.xz',
},
};
for (const name of [
...selection.snapAssets.map(({ name }) => name),
selection.sourceAsset.name,
]) {
fs.writeFileSync(path.join(temporaryRoot, name), name);
}
const inspectedSnaps = [];
assert.deepEqual(
helper.verifySnapReleaseCorrespondence(selection, temporaryRoot, {
expectedRepositoryRevision: fixture.repositoryRevision,
expectedSourceSnapshotSha256: fixture.expectedSourceSnapshotSha256,
inspectSourceArchive: () => fixture.sourceInspection,
inspectSnapPayload: (_snapPath, asset) => {
inspectedSnaps.push(asset.name);
return fixture.snapPayloads[asset.name];
},
validateRuntimeManifest: () => [],
}),
selection
);
assert.deepEqual(
inspectedSnaps,
selection.snapAssets.map(({ name }) => name)
);
});
test('publishes only a stable verified asset snapshot with an exact receipt', async (t) => {
const helper = await loadHelper();
const fixture = createSourceBindingFixture();
const temporaryRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-snap-release-snapshot-')
);
t.after(() => fs.rmSync(temporaryRoot, { recursive: true, force: true }));
const downloadRoot = path.join(temporaryRoot, 'downloads');
const verifiedRoot = path.join(temporaryRoot, 'verified');
fs.mkdirSync(downloadRoot);
const selection = {
snapAssets: [
{ id: 1, name: 'IPTVnator-amd64.snap' },
{ id: 2, name: 'IPTVnator-arm64.snap' },
],
sourceAsset: {
id: 3,
name: 'linux-frame-copy-runtime-sources.tar.xz',
},
};
for (const name of [
...selection.snapAssets.map(({ name }) => name),
selection.sourceAsset.name,
]) {
fs.writeFileSync(path.join(downloadRoot, name), name);
}
const inspectedPaths = [];
assert.deepEqual(
helper.verifySnapReleaseCorrespondence(selection, downloadRoot, {
expectedRepositoryRevision: fixture.repositoryRevision,
expectedSourceSnapshotSha256: fixture.expectedSourceSnapshotSha256,
inspectSourceArchive: (sourcePath) => {
inspectedPaths.push(sourcePath);
return fixture.sourceInspection;
},
inspectSnapPayload: (snapPath, asset) => {
inspectedPaths.push(snapPath);
return fixture.snapPayloads[asset.name];
},
validateRuntimeManifest: () => [],
verifiedDirectory: verifiedRoot,
}),
selection
);
assert.ok(
inspectedPaths.every((filePath) =>
filePath.startsWith(`${verifiedRoot}${path.sep}`)
)
);
const receiptPath = path.join(
verifiedRoot,
helper.VERIFIED_RELEASE_RECEIPT_NAME
);
const receipt = helper.verifyVerifiedReleaseReceipt(
selection,
verifiedRoot,
receiptPath,
fixture.repositoryRevision
);
assert.equal(receipt.repositoryRevision, fixture.repositoryRevision);
assert.deepEqual(
receipt.assets.map(({ name }) => name),
[
...selection.snapAssets.map(({ name }) => name),
selection.sourceAsset.name,
]
);
const sealedInspectionPaths = [];
assert.deepEqual(
helper.verifySnapReleaseCorrespondence(selection, verifiedRoot, {
expectedRepositoryRevision: fixture.repositoryRevision,
expectedSourceSnapshotSha256: fixture.expectedSourceSnapshotSha256,
inspectSourceArchive: (sourcePath) => {
sealedInspectionPaths.push(sourcePath);
return fixture.sourceInspection;
},
inspectSnapPayload: (snapPath, asset) => {
sealedInspectionPaths.push(snapPath);
return fixture.snapPayloads[asset.name];
},
validateRuntimeManifest: () => [],
verifiedReceiptPath: receiptPath,
}),
selection
);
assert.ok(
sealedInspectionPaths.every((filePath) =>
filePath.startsWith(`${verifiedRoot}${path.sep}`)
)
);
assert.throws(
() =>
helper.verifyVerifiedReleaseReceipt(
selection,
verifiedRoot,
receiptPath,
'f'.repeat(40)
),
/invalid contract/i
);
const changedSnapPath = path.join(
verifiedRoot,
selection.snapAssets[0].name
);
fs.chmodSync(changedSnapPath, 0o644);
fs.appendFileSync(changedSnapPath, 'changed');
assert.throws(
() =>
helper.verifyVerifiedReleaseReceipt(
selection,
verifiedRoot,
receiptPath,
fixture.repositoryRevision
),
/no longer matches its receipt/i
);
});
test('binds sealed inspection to the initially verified receipt across a mutually consistent replacement', async (t) => {
const helper = await loadHelper();
const fixture = createSourceBindingFixture();
const temporaryRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-snap-release-replacement-')
);
t.after(() => fs.rmSync(temporaryRoot, { recursive: true, force: true }));
const downloadRoot = path.join(temporaryRoot, 'downloads');
const verifiedRoot = path.join(temporaryRoot, 'verified');
const replacementRoot = path.join(temporaryRoot, 'replacement');
const originalRoot = path.join(temporaryRoot, 'original');
fs.mkdirSync(downloadRoot);
fs.mkdirSync(replacementRoot);
const selection = {
snapAssets: [{ id: 1, name: 'IPTVnator-amd64.snap' }],
sourceAsset: {
id: 2,
name: 'linux-frame-copy-runtime-sources.tar.xz',
},
};
for (const name of [
selection.snapAssets[0].name,
selection.sourceAsset.name,
]) {
fs.writeFileSync(path.join(downloadRoot, name), name);
}
helper.verifySnapReleaseCorrespondence(selection, downloadRoot, {
expectedRepositoryRevision: fixture.repositoryRevision,
expectedSourceSnapshotSha256: fixture.expectedSourceSnapshotSha256,
inspectSourceArchive: () => fixture.sourceInspection,
inspectSnapPayload: (_snapPath, asset) =>
fixture.snapPayloads[asset.name],
validateRuntimeManifest: () => [],
verifiedDirectory: verifiedRoot,
});
const replacementSnapContents = Buffer.from('replacement Snap payload');
const sourceContents = fs.readFileSync(
path.join(verifiedRoot, selection.sourceAsset.name)
);
fs.writeFileSync(
path.join(replacementRoot, selection.snapAssets[0].name),
replacementSnapContents
);
fs.writeFileSync(
path.join(replacementRoot, selection.sourceAsset.name),
sourceContents
);
const replacementRecords = [
{
...selection.snapAssets[0],
sha256: sha256(replacementSnapContents),
size: replacementSnapContents.length,
},
{
...selection.sourceAsset,
sha256: sha256(sourceContents),
size: sourceContents.length,
},
];
const replacementReceiptPath = path.join(
replacementRoot,
helper.VERIFIED_RELEASE_RECEIPT_NAME
);
fs.writeFileSync(
replacementReceiptPath,
`${JSON.stringify(
{
schemaVersion: 1,
repositoryRevision: fixture.repositoryRevision,
assets: replacementRecords,
},
null,
2
)}\n`
);
assert.deepEqual(
helper.verifyVerifiedReleaseReceipt(
selection,
replacementRoot,
replacementReceiptPath,
fixture.repositoryRevision
).assets,
replacementRecords
);
const receiptPath = path.join(
verifiedRoot,
helper.VERIFIED_RELEASE_RECEIPT_NAME
);
let replaced = false;
assert.throws(
() =>
helper.verifySnapReleaseCorrespondence(selection, verifiedRoot, {
expectedRepositoryRevision: fixture.repositoryRevision,
expectedSourceSnapshotSha256:
fixture.expectedSourceSnapshotSha256,
inspectSourceArchive: () => {
fs.renameSync(verifiedRoot, originalRoot);
fs.renameSync(replacementRoot, verifiedRoot);
replaced = true;
return fixture.sourceInspection;
},
inspectSnapPayload: (_snapPath, asset) =>
fixture.snapPayloads[asset.name],
validateRuntimeManifest: () => [],
verifiedReceiptPath: receiptPath,
}),
/initially verified receipt/i
);
assert.equal(replaced, true);
});
test('removes a verified snapshot when an asset changes during inspection', async (t) => {
const helper = await loadHelper();
const fixture = createSourceBindingFixture();
const temporaryRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-snap-release-race-')
);
t.after(() => fs.rmSync(temporaryRoot, { recursive: true, force: true }));
const downloadRoot = path.join(temporaryRoot, 'downloads');
const verifiedRoot = path.join(temporaryRoot, 'verified');
fs.mkdirSync(downloadRoot);
const selection = {
snapAssets: [{ id: 1, name: 'IPTVnator-amd64.snap' }],
sourceAsset: {
id: 2,
name: 'linux-frame-copy-runtime-sources.tar.xz',
},
};
for (const name of [
selection.snapAssets[0].name,
selection.sourceAsset.name,
]) {
fs.writeFileSync(path.join(downloadRoot, name), name);
}
assert.throws(
() =>
helper.verifySnapReleaseCorrespondence(selection, downloadRoot, {
expectedRepositoryRevision: fixture.repositoryRevision,
expectedSourceSnapshotSha256:
fixture.expectedSourceSnapshotSha256,
inspectSourceArchive: (sourcePath) => {
fs.chmodSync(sourcePath, 0o644);
fs.appendFileSync(sourcePath, 'changed');
return fixture.sourceInspection;
},
inspectSnapPayload: (_snapPath, asset) =>
fixture.snapPayloads[asset.name],
validateRuntimeManifest: () => [],
verifiedDirectory: verifiedRoot,
}),
/changed during inspection/i
);
assert.equal(fs.existsSync(verifiedRoot), false);
});
test('fails closed for stale source identity, archive bytes, and x64 marker-only payloads', async () => {
const helper = await loadHelper();
const fixture = createSourceBindingFixture();
const validateRuntimeManifest = () => [];
const staleRevision = structuredClone(fixture.sourceInspection);
staleRevision.sourceIndex.repositoryRevision = 'e'.repeat(40);
assert.throws(
() =>
helper.verifySnapReleaseSourceBinding(
{
expectedRepositoryRevision: fixture.repositoryRevision,
sourceInspection: staleRevision,
snapPayloads: Object.values(fixture.snapPayloads),
},
{
expectedSourceSnapshotSha256:
fixture.expectedSourceSnapshotSha256,
validateRuntimeManifest,
}
),
/repository revision/i
);
const forgedSubmodules = structuredClone(fixture);
const forgedSubmoduleRecord = `${'f'.repeat(40)} 3rdparty/Vulkan-Headers`;
forgedSubmodules.sourceInspection.sourceRuntime.packages.libplacebo.sourceSubmodules[0] =
forgedSubmoduleRecord;
forgedSubmodules.sourceInspection.sourceIndex.libplacebo.sourceSubmodules[0] =
forgedSubmoduleRecord;
forgedSubmodules.sourceInspection.sourceIndex.sourcePackages =
forgedSubmodules.sourceInspection.sourceRuntime.packages;
forgedSubmodules.snapPayloads[
'IPTVnator-amd64.snap'
].manifest.sourceRuntime = forgedSubmodules.sourceInspection.sourceRuntime;
assert.throws(
() =>
helper.verifySnapReleaseSourceBinding(
{
expectedRepositoryRevision: fixture.repositoryRevision,
sourceInspection: forgedSubmodules.sourceInspection,
snapPayloads: Object.values(forgedSubmodules.snapPayloads),
},
{
expectedSourceSnapshotSha256:
fixture.expectedSourceSnapshotSha256,
validateRuntimeManifest,
}
),
/submodule/i
);
const tamperedArchive = structuredClone(fixture.sourceInspection);
tamperedArchive.archiveFiles[0].sha256 = 'f'.repeat(64);
assert.throws(
() =>
helper.verifySnapReleaseSourceBinding(
{
expectedRepositoryRevision: fixture.repositoryRevision,
sourceInspection: tamperedArchive,
snapPayloads: Object.values(fixture.snapPayloads),
},
{
expectedSourceSnapshotSha256:
fixture.expectedSourceSnapshotSha256,
validateRuntimeManifest,
}
),
/archive.*checksum/i
);
const staleSnapPayloads = structuredClone(fixture.snapPayloads);
staleSnapPayloads[
'IPTVnator-amd64.snap'
].manifest.sourceRuntime.generatedAt = '2025-01-01T00:00:00.000Z';
assert.throws(
() =>
helper.verifySnapReleaseSourceBinding(
{
expectedRepositoryRevision: fixture.repositoryRevision,
sourceInspection: fixture.sourceInspection,
snapPayloads: Object.values(staleSnapPayloads),
},
{
expectedSourceSnapshotSha256:
fixture.expectedSourceSnapshotSha256,
validateRuntimeManifest,
}
),
/Snap source runtime.*source archive/i
);
const markerOnlyX64 = structuredClone(fixture.snapPayloads);
markerOnlyX64['IPTVnator-amd64.snap'] = {
assetName: 'IPTVnator-amd64.snap',
architecture: 'x64',
markerOnly: true,
manifest: null,
};
assert.throws(
() =>
helper.verifySnapReleaseSourceBinding(
{
expectedRepositoryRevision: fixture.repositoryRevision,
sourceInspection: fixture.sourceInspection,
snapPayloads: Object.values(markerOnlyX64),
},
{
expectedSourceSnapshotSha256:
fixture.expectedSourceSnapshotSha256,
validateRuntimeManifest,
}
),
/x64 Snap.*frame-copy/i
);
const incompleteCompliance = structuredClone(fixture.sourceInspection);
incompleteCompliance.compliance.toolingValidated = false;
assert.throws(
() =>
helper.verifySnapReleaseSourceBinding(
{
expectedRepositoryRevision: fixture.repositoryRevision,
sourceInspection: incompleteCompliance,
snapPayloads: Object.values(fixture.snapPayloads),
},
{
expectedSourceSnapshotSha256:
fixture.expectedSourceSnapshotSha256,
validateRuntimeManifest,
}
),
/source archive compliance/i
);
assert.throws(
() =>
helper.verifySnapReleaseSourceBinding(
{
expectedRepositoryRevision: fixture.repositoryRevision,
sourceInspection: fixture.sourceInspection,
snapPayloads: [
fixture.snapPayloads['IPTVnator-arm64.snap'],
],
},
{
expectedSourceSnapshotSha256:
fixture.expectedSourceSnapshotSha256,
validateRuntimeManifest,
}
),
/exactly one x64 Snap/i
);
assert.throws(
() =>
helper.verifySnapReleaseSourceBinding(
{
expectedRepositoryRevision: fixture.repositoryRevision,
sourceInspection: fixture.sourceInspection,
snapPayloads: [
fixture.snapPayloads['IPTVnator-amd64.snap'],
{
...fixture.snapPayloads['IPTVnator-amd64.snap'],
assetName: 'IPTVnator-second-amd64.snap',
},
],
},
{
expectedSourceSnapshotSha256:
fixture.expectedSourceSnapshotSha256,
validateRuntimeManifest,
}
),
/exactly one x64 Snap/i
);
});
test('hashes the final source archive bytes and reads the exact packaged Snap binding', async (t) => {
const sourceBindingHelper = await loadSourceBindingHelper();
const sourceArchiveContract = await loadSourceArchiveContract();
const fixture = createSourceBindingFixture();
const temporaryRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-snap-source-inspection-')
);
t.after(() => fs.rmSync(temporaryRoot, { recursive: true, force: true }));
const sourceRoot = path.join(temporaryRoot, 'source');
const metadataRoot = path.join(sourceRoot, 'metadata');
const archivesRoot = path.join(sourceRoot, 'archives');
fs.mkdirSync(metadataRoot, { recursive: true });
fs.mkdirSync(archivesRoot, { recursive: true });
const pinnedSourceContents = Buffer.from('pinned ffmpeg source');
const pinnedSourceSha256 = crypto
.createHash('sha256')
.update(pinnedSourceContents)
.digest('hex');
const sourceRuntime = structuredClone(
fixture.sourceInspection.sourceRuntime
);
sourceRuntime.packages.ffmpeg.sourceSha256 = pinnedSourceSha256;
const sourceIndex = structuredClone(fixture.sourceInspection.sourceIndex);
sourceIndex.sourcePackages = sourceRuntime.packages;
sourceIndex.archives = [
{
name: 'ffmpeg.tar.xz',
sha256: pinnedSourceSha256,
},
];
sourceIndex.libplacebo = {
sourceGitCommit: sourceRuntime.packages.libplacebo.sourceGitCommit,
sourceSubmodules: [
...sourceRuntime.packages.libplacebo.sourceSubmodules,
],
sourceSnapshot: SYNTHETIC_LIBPLACEBO_SOURCE_SNAPSHOT,
};
const compliance = createComplianceInspection(sourceRuntime);
sourceIndex.legal = {
manifest: 'notices/embedded-mpv-notices.json',
noticeFile: compliance.notices.noticeFile,
packages: compliance.notices.packages,
};
fs.writeFileSync(
path.join(archivesRoot, 'ffmpeg.tar.xz'),
pinnedSourceContents
);
const licenseInputRoot = path.join(sourceRoot, 'license-inputs');
const noticesRoot = path.join(sourceRoot, 'notices');
for (const [root, files] of [
[licenseInputRoot, compliance.licenseInputFiles],
[noticesRoot, compliance.noticeLicenseFiles],
]) {
for (const file of files) {
const filePath = path.join(root, ...file.path.split('/'));
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, `${file.path.split('/')[1]} license\n`);
}
}
fs.writeFileSync(
path.join(licenseInputRoot, 'linux-runtime-license-inputs.json'),
`${JSON.stringify(compliance.licenseInputs)}\n`
);
fs.writeFileSync(
path.join(noticesRoot, 'embedded-mpv-notices.json'),
`${JSON.stringify(compliance.notices)}\n`
);
fs.writeFileSync(
path.join(noticesRoot, 'THIRD_PARTY_NOTICES.txt'),
compliance.aggregateNoticeContents
);
const libplaceboRoot = path.join(sourceRoot, 'git', 'libplacebo');
fs.mkdirSync(libplaceboRoot, { recursive: true });
fs.writeFileSync(
path.join(libplaceboRoot, 'README.md'),
'libplacebo source snapshot\n'
);
const toolingFiles = [
['embedded-mpv', 'build-linux-runtime.cjs'],
['embedded-mpv', 'build-linux-runtime.mjs'],
['embedded-mpv', 'generate-linux-runtime-notices.cjs'],
['embedded-mpv', 'linux-runtime-manifest.cjs'],
['embedded-mpv', 'linux-source-archive-contract.cjs'],
['embedded-mpv', 'stage-runtime.mjs'],
['packaging', 'prepare-linux-runtime-source-snapshot.cjs'],
];
const toolingRoot = path.join(sourceRoot, 'tooling');
fs.mkdirSync(toolingRoot, { recursive: true });
for (const [directoryName, fileName] of toolingFiles) {
fs.copyFileSync(
path.join(workspaceRoot, 'tools', directoryName, fileName),
path.join(toolingRoot, fileName)
);
}
fs.writeFileSync(
path.join(metadataRoot, 'runtime-manifest.json'),
`${JSON.stringify(sourceRuntime)}\n`
);
fs.writeFileSync(
path.join(metadataRoot, 'source-index.json'),
`${JSON.stringify(sourceIndex)}\n`
);
fs.writeFileSync(
path.join(metadataRoot, 'iptvnator-git-revision.txt'),
`${fixture.repositoryRevision}\n`
);
fs.writeFileSync(path.join(metadataRoot, 'local-changes.patch'), '');
const archiveChecksumsPath = path.join(metadataRoot, 'archive-sha256.txt');
fs.writeFileSync(
archiveChecksumsPath,
`${pinnedSourceSha256} ffmpeg.tar.xz\n`
);
const sourceArchivePath = path.join(
temporaryRoot,
'linux-frame-copy-runtime-sources.tar.xz'
);
const tarResult = childProcess.spawnSync(
'tar',
[
'--create',
'--xz',
'--file',
sourceArchivePath,
'--directory',
sourceRoot,
'.',
],
{ encoding: 'utf8' }
);
assert.equal(tarResult.error, undefined);
assert.equal(tarResult.status, 0, tarResult.stderr);
let observedTarFilesFromFile = false;
const runCommand = (command, args, options = {}) => {
if (command === 'tar' && args.includes('-T')) {
const filesFromPath = args[args.indexOf('-T') + 1];
assert.equal(path.isAbsolute(filesFromPath), true);
assert.equal(fs.lstatSync(filesFromPath).isFile(), true);
assert.equal(
fs.lstatSync(filesFromPath).mode & 0o077,
0,
'tar files-from input must not be group/world accessible'
);
const filesFromContents = fs.readFileSync(filesFromPath, 'utf8');
assert.match(
filesFromContents,
/(?:^|\n)\.\/git\/libplacebo\/README\.md(?:\n|$)/,
'tar files-from input must preserve the exact archive member names'
);
assert.ok(
args.includes('--no-recursion'),
'tar must not recursively consume descendants that are also listed explicitly'
);
observedTarFilesFromFile = true;
}
const result = childProcess.spawnSync(command, args, {
encoding:
options.encoding === undefined ? 'utf8' : options.encoding,
input: options.input,
killSignal: 'SIGKILL',
maxBuffer: options.maxBuffer,
stdio: 'pipe',
timeout: options.timeout,
windowsHide: true,
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(
`${command} exited with status ${String(result.status)}: ${
result.stderr ?? ''
}`
);
}
return result.stdout;
};
const sourceArchive = sourceArchiveContract.createLinuxSourceArchiveBinding(
{
archivePath: sourceArchivePath,
repositoryRevision: fixture.repositoryRevision,
}
);
assert.throws(
() =>
sourceBindingHelper.inspectSourceArchive(sourceArchivePath, {
runCommand,
}),
/snapshot digest mismatch/i
);
const sourceInspection = sourceBindingHelper.inspectSourceArchive(
sourceArchivePath,
{
expectedSourceSnapshotSha256: fixture.expectedSourceSnapshotSha256,
runCommand,
}
);
assert.equal(observedTarFilesFromFile, true);
assert.equal(sourceInspection.archiveSha256, sourceArchive.sha256);
assert.deepEqual(sourceInspection.archiveFiles, sourceIndex.archives);
const hiddenSourceRoot = path.join(temporaryRoot, 'hidden-source');
fs.mkdirSync(hiddenSourceRoot);
fs.writeFileSync(
path.join(hiddenSourceRoot, 'undeclared-hidden-source.txt'),
'not part of the canonical source bundle'
);
const hiddenSourceArchivePath = path.join(
temporaryRoot,
'hidden-source.tar.xz'
);
const hiddenSourceTarResult = childProcess.spawnSync(
'tar',
[
'--create',
'--xz',
'--file',
hiddenSourceArchivePath,
'--directory',
hiddenSourceRoot,
'.',
],
{ encoding: 'utf8' }
);
assert.equal(hiddenSourceTarResult.error, undefined);
assert.equal(hiddenSourceTarResult.status, 0, hiddenSourceTarResult.stderr);
const concatenatedSourceArchivePath = path.join(
temporaryRoot,
'concatenated-source.tar.xz'
);
fs.writeFileSync(
concatenatedSourceArchivePath,
Buffer.concat([
fs.readFileSync(sourceArchivePath),
fs.readFileSync(hiddenSourceArchivePath),
])
);
assert.throws(
() =>
sourceBindingHelper.inspectSourceArchive(
concatenatedSourceArchivePath,
{
expectedSourceSnapshotSha256:
fixture.expectedSourceSnapshotSha256,
}
),
/undeclared source archive member/i
);
fs.writeFileSync(
archiveChecksumsPath,
`${'0'.repeat(64)} ffmpeg.tar.xz\n`
);
const badChecksumArchivePath = path.join(
temporaryRoot,
'bad-checksum-source.tar.xz'
);
const badChecksumTarResult = childProcess.spawnSync(
'tar',
[
'--create',
'--xz',
'--file',
badChecksumArchivePath,
'--directory',
sourceRoot,
'.',
],
{ encoding: 'utf8' }
);
assert.equal(badChecksumTarResult.error, undefined);
assert.equal(badChecksumTarResult.status, 0, badChecksumTarResult.stderr);
assert.throws(
() =>
sourceBindingHelper.inspectSourceArchive(badChecksumArchivePath, {
expectedSourceSnapshotSha256:
fixture.expectedSourceSnapshotSha256,
}),
/archive checksum/i
);
fs.writeFileSync(
archiveChecksumsPath,
`${pinnedSourceSha256} ffmpeg.tar.xz\n`
);
const extraSourcePath = path.join(sourceRoot, 'undeclared-source.txt');
fs.writeFileSync(
extraSourcePath,
'not part of the canonical source bundle'
);
const extraMemberArchivePath = path.join(
temporaryRoot,
'extra-member-source.tar.xz'
);
const extraMemberTarResult = childProcess.spawnSync(
'tar',
[
'--create',
'--xz',
'--file',
extraMemberArchivePath,
'--directory',
sourceRoot,
'.',
],
{ encoding: 'utf8' }
);
assert.equal(extraMemberTarResult.error, undefined);
assert.equal(extraMemberTarResult.status, 0, extraMemberTarResult.stderr);
assert.throws(
() =>
sourceBindingHelper.inspectSourceArchive(extraMemberArchivePath, {
expectedSourceSnapshotSha256:
fixture.expectedSourceSnapshotSha256,
}),
/undeclared source archive member/i
);
fs.unlinkSync(extraSourcePath);
const oversizedSourceArchive = path.join(
temporaryRoot,
'oversized-source.tar.xz'
);
fs.writeFileSync(oversizedSourceArchive, '');
fs.truncateSync(oversizedSourceArchive, 1024 * 1024 * 1024 + 1);
assert.throws(
() => sourceBindingHelper.inspectSourceArchive(oversizedSourceArchive),
/bounded non-empty regular file/i
);
const localChangesPath = path.join(metadataRoot, 'local-changes.patch');
const emptyTargetPath = path.join(sourceRoot, 'empty-target');
fs.writeFileSync(emptyTargetPath, '');
fs.unlinkSync(localChangesPath);
fs.symlinkSync('../empty-target', localChangesPath);
const symlinkArchivePath = path.join(
temporaryRoot,
'symlinked-source.tar.xz'
);
const symlinkTarResult = childProcess.spawnSync(
'tar',
[
'--create',
'--xz',
'--file',
symlinkArchivePath,
'--directory',
sourceRoot,
'.',
],
{ encoding: 'utf8' }
);
assert.equal(symlinkTarResult.error, undefined);
assert.equal(symlinkTarResult.status, 0, symlinkTarResult.stderr);
assert.throws(
() => sourceBindingHelper.inspectSourceArchive(symlinkArchivePath),
/required member.*regular file/i
);
fs.unlinkSync(localChangesPath);
fs.writeFileSync(localChangesPath, '');
const oversizedMemberPath = path.join(
libplaceboRoot,
'oversized-source-member.bin'
);
fs.writeFileSync(oversizedMemberPath, '');
fs.truncateSync(oversizedMemberPath, 128 * 1024 * 1024 + 1);
const oversizedMemberArchivePath = path.join(
temporaryRoot,
'oversized-member-source.tar.xz'
);
const oversizedMemberTarResult = childProcess.spawnSync(
'tar',
[
'--create',
'--xz',
'--file',
oversizedMemberArchivePath,
'--directory',
sourceRoot,
'.',
],
{ encoding: 'utf8' }
);
assert.equal(oversizedMemberTarResult.error, undefined);
assert.equal(
oversizedMemberTarResult.status,
0,
oversizedMemberTarResult.stderr
);
assert.throws(
() =>
sourceBindingHelper.inspectSourceArchive(
oversizedMemberArchivePath,
{
expectedSourceSnapshotSha256:
fixture.expectedSourceSnapshotSha256,
}
),
/member exceeds.*size limit/i
);
fs.unlinkSync(oversizedMemberPath);
const snapSourceRoot = path.join(temporaryRoot, 'snap-source');
const appRoot = path.join(snapSourceRoot, 'usr', 'lib', 'iptvnator');
const nativeRoot = path.join(
appRoot,
'resources',
'app.asar.unpacked',
'electron-backend',
'native'
);
fs.mkdirSync(nativeRoot, { recursive: true });
const electronHeader = Buffer.alloc(20);
electronHeader.set([0x7f, 0x45, 0x4c, 0x46, 2, 1]);
electronHeader.writeUInt16LE(62, 18);
fs.writeFileSync(path.join(appRoot, 'iptvnator.bin'), electronHeader);
const packagedManifest = {
...fixture.snapPayloads['IPTVnator-amd64.snap'].manifest,
sourceArchive,
sourceRuntime,
};
fs.writeFileSync(
path.join(nativeRoot, 'embedded-mpv-runtime.json'),
`${JSON.stringify(packagedManifest)}\n`
);
const snapYamlPath = path.join(snapSourceRoot, 'meta', 'snap.yaml');
fs.mkdirSync(path.dirname(snapYamlPath), { recursive: true });
const validSnapYaml = [
'name: iptvnator',
'base: core22',
'confinement: strict',
'apps:',
' iptvnator:',
' command: iptvnator',
' environment:',
' SNAP_DESKTOP_RUNTIME: $SNAP/gnome-platform',
' plugs:',
' - desktop',
' - shared-memory',
' - graphics-core22',
'plugs:',
' shared-memory:',
' interface: shared-memory',
' private: true',
' graphics-core22:',
' interface: content',
' target: $SNAP/graphics',
' default-provider: mesa-core22',
'layout:',
' /usr/share/libdrm:',
' bind: $SNAP/graphics/libdrm',
' /usr/share/drirc.d:',
' symlink: $SNAP/graphics/drirc.d',
'',
].join('\n');
fs.writeFileSync(snapYamlPath, validSnapYaml);
const graphicsRoot = path.join(snapSourceRoot, 'graphics');
fs.mkdirSync(graphicsRoot, { mode: 0o755 });
fs.chmodSync(graphicsRoot, 0o755);
for (const script of [
'desktop-init.sh',
'desktop-common.sh',
'desktop-gnome-specific.sh',
]) {
const desktopScriptPath = path.join(snapSourceRoot, script);
fs.writeFileSync(desktopScriptPath, '#!/bin/sh\n');
fs.chmodSync(desktopScriptPath, 0o755);
}
const asarSourceRoot = path.join(temporaryRoot, 'asar-source');
const appAsarPath = path.join(appRoot, 'resources', 'app.asar');
fs.mkdirSync(asarSourceRoot);
fs.writeFileSync(
path.join(asarSourceRoot, 'main.js'),
'module.exports = {}'
);
await createAsarPackage(asarSourceRoot, appAsarPath);
const cleanReleaseCheckoutRoot = path.join(
temporaryRoot,
'clean-release-checkout'
);
const cleanReleaseToolsRoot = path.join(cleanReleaseCheckoutRoot, 'tools');
fs.mkdirSync(cleanReleaseToolsRoot, { recursive: true });
for (const toolDirectory of ['packaging', 'embedded-mpv']) {
fs.cpSync(
path.join(workspaceRoot, 'tools', toolDirectory),
path.join(cleanReleaseToolsRoot, toolDirectory),
{ recursive: true }
);
}
const cleanReleaseBoundaryHelperPath = path.join(
cleanReleaseToolsRoot,
'packaging',
'validate-snap-release-boundary.mjs'
);
const dependencyFreeBoundaryResult = childProcess.spawnSync(
process.execPath,
[cleanReleaseBoundaryHelperPath, snapSourceRoot],
{
encoding: 'utf8',
env: {
...process.env,
NODE_OPTIONS: '',
NODE_PATH: '',
},
}
);
assert.equal(
dependencyFreeBoundaryResult.status,
0,
dependencyFreeBoundaryResult.stderr
);
assert.deepEqual(JSON.parse(dependencyFreeBoundaryResult.stdout), {
schemaVersion: 1,
errors: [],
});
const snapPath = path.join(temporaryRoot, 'IPTVnator-amd64.snap');
fs.writeFileSync(snapPath, 'synthetic Snap bytes');
const staticValidationCalls = [];
const snapPayload = 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;
assert.ok(destinationIndex > 0);
fs.cpSync(snapSourceRoot, args[destinationIndex], {
recursive: true,
});
return '';
},
validatePackagedEmbeddedMpv: (resourceDirectory, options) => {
staticValidationCalls.push({ options, resourceDirectory });
return [];
},
}
);
assert.deepEqual(snapPayload, {
architecture: 'x64',
assetName: 'IPTVnator-amd64.snap',
manifest: packagedManifest,
markerOnly: false,
});
assert.equal(staticValidationCalls.length, 1);
assert.ok(
staticValidationCalls[0].resourceDirectory.endsWith(
['payload', 'usr', 'lib', 'iptvnator', 'resources'].join(path.sep)
)
);
assert.deepEqual(staticValidationCalls[0].options, {
artifactFormat: 'snap',
executableName: 'iptvnator',
foreignArch: false,
hostPlatform: 'linux',
platform: 'linux',
profile: 'portable',
required: true,
targetArch: 'x64',
targetNames: ['appimage', 'snap'],
});
fs.writeFileSync(
snapYamlPath,
validSnapYaml.replace(' - shared-memory\n', '')
);
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(snapSourceRoot, args[destinationIndex], {
recursive: true,
});
return '';
},
validatePackagedEmbeddedMpv: () => [],
}
),
/shared-memory plug/i
);
fs.writeFileSync(snapYamlPath, validSnapYaml);
fs.rmSync(asarSourceRoot, { recursive: true });
const staleAsarNativeRoot = path.join(
asarSourceRoot,
'electron-backend',
'native'
);
fs.mkdirSync(staleAsarNativeRoot, { recursive: true });
fs.writeFileSync(
path.join(staleAsarNativeRoot, 'embedded-mpv-runtime.json'),
'{}\n'
);
fs.rmSync(appAsarPath);
await createAsarPackage(asarSourceRoot, appAsarPath);
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(snapSourceRoot, args[destinationIndex], {
recursive: true,
});
return '';
},
validatePackagedEmbeddedMpv: () => [],
}
),
/app\.asar.*embedded MPV native payload/i
);
fs.rmSync(asarSourceRoot, { recursive: true });
fs.mkdirSync(asarSourceRoot);
fs.writeFileSync(
path.join(asarSourceRoot, 'main.js'),
'module.exports = {}'
);
fs.rmSync(appAsarPath);
await createAsarPackage(asarSourceRoot, appAsarPath);
assert.equal(
sourceBindingHelper.verifySnapReleaseSourceBinding(
{
expectedRepositoryRevision: fixture.repositoryRevision,
sourceInspection,
snapPayloads: [snapPayload],
},
{
expectedSourceSnapshotSha256:
fixture.expectedSourceSnapshotSha256,
validateRuntimeManifest: () => [],
}
),
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(snapSourceRoot, args[destinationIndex], {
recursive: true,
});
return '';
},
validatePackagedEmbeddedMpv: () => [
'Hidden inherited frame-copy artifact.',
],
}
),
/Hidden inherited frame-copy artifact/
);
const decoySourceRoot = path.join(temporaryRoot, 'decoy-snap-source');
const decoyAppRoot = path.join(decoySourceRoot, 'decoy');
fs.cpSync(appRoot, decoyAppRoot, { 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(decoySourceRoot, args[destinationIndex], {
recursive: true,
});
return '';
},
validatePackagedEmbeddedMpv: () => {
throw new Error(
'Static validation must not inspect a decoy root.'
);
},
}
),
/canonical frame-copy manifest/i
);
const manifestPath = path.join(nativeRoot, 'embedded-mpv-runtime.json');
fs.truncateSync(manifestPath, 16 * 1024 * 1024 + 1);
let oversizedStaticValidationCalls = 0;
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(snapSourceRoot, args[destinationIndex], {
recursive: true,
});
return '';
},
validatePackagedEmbeddedMpv: () => {
oversizedStaticValidationCalls += 1;
return [];
},
}
),
/invalid frame-copy manifest/i
);
assert.equal(oversizedStaticValidationCalls, 0);
});
test('publish workflow installs the source verifier and binds the release tag revision', () => {
const workflow = fs.readFileSync(publishWorkflowPath, 'utf8');
const parsedWorkflow = parse(workflow);
const verifyJob = parsedWorkflow.jobs['verify-snap'];
const publishJob = parsedWorkflow.jobs['publish-snap'];
const uploadStep = publishJob.steps.find(
(step) => step.name === 'Publish all public-release snaps to edge'
);
const checkoutStep = verifyJob.steps.find(
(step) => step.name === 'Checkout released tooling'
);
const snapcraftStep = publishJob.steps.find(
(step) => step.name === 'Install Snapcraft'
);
const selectStep = verifyJob.steps.find(
(step) => step.name === 'Select exact public release assets'
);
const downloadStep = verifyJob.steps.find(
(step) => step.name === 'Download exact public release assets'
);
const verifyStep = verifyJob.steps.find(
(step) => step.name === 'Verify downloaded public release assets'
);
const sealedVerifyStep = verifyJob.steps.find(
(step) => step.name === 'Reverify sealed public release assets'
);
const transferStep = verifyJob.steps.find(
(step) => step.name === 'Transfer verified release assets'
);
const artifactDownloadStep = publishJob.steps.find(
(step) => step.name === 'Download verified release assets'
);
const transferredSealStep = publishJob.steps.find(
(step) => step.name === 'Seal transferred public release assets'
);
assert.equal(verifyJob['timeout-minutes'], 45);
assert.equal(publishJob['timeout-minutes'], 20);
assert.equal(publishJob.needs, 'verify-snap');
assert.match(
workflow,
/apt-get install[\s\S]*binutils[\s\S]*squashfs-tools[\s\S]*xz-utils/
);
assert.match(
workflow,
/release-snap-assets\.cjs verify[\s\S]*--repository-revision/
);
assert.ok(
workflow.indexOf('--repository-revision') <
workflow.indexOf('snapcraft upload --release=edge')
);
assert.equal(JSON.stringify(verifyJob).includes('snapcraft_token'), false);
assert.equal(
checkoutStep.with?.['persist-credentials'],
false,
'release checkout must not persist github.token for later steps'
);
assert.equal(
checkoutStep.uses,
'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
);
assert.equal(snapcraftStep.uses, undefined);
assert.match(
snapcraftStep.run,
/sudo snap install snapcraft --classic --channel=stable/
);
assert.match(verifyStep.run, /--verified-directory/);
assert.match(verifyStep.run, /chown -R root:root/);
assert.match(verifyStep.run, /chmod 0555/);
assert.match(verifyStep.run, /chmod 0444/);
assert.match(
verifyStep.run,
/SEALED_ASSET_PARENT="\/var\/lib\/iptvnator-snap-release"/
);
assert.match(
verifyStep.run,
/sudo mv "\$\{VERIFIED_ASSET_STAGING\}" "\$\{SEALED_ASSET_DIRECTORY\}"/
);
assert.ok(sealedVerifyStep);
assert.equal(sealedVerifyStep.env, undefined);
assert.match(
sealedVerifyStep.run,
/release-snap-assets\.cjs verify-sealed/
);
assert.ok(
verifyJob.steps.indexOf(sealedVerifyStep) <
verifyJob.steps.indexOf(transferStep)
);
assert.equal(
transferStep.uses,
'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a'
);
assert.equal(
artifactDownloadStep.uses,
'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c'
);
assert.ok(
publishJob.steps.indexOf(artifactDownloadStep) <
publishJob.steps.indexOf(transferredSealStep)
);
assert.ok(
publishJob.steps.indexOf(transferredSealStep) <
publishJob.steps.indexOf(snapcraftStep)
);
assert.ok(
publishJob.steps.indexOf(snapcraftStep) <
publishJob.steps.indexOf(uploadStep)
);
assert.match(
uploadStep.run,
/VERIFIED_ASSET_DIRECTORY="\/var\/lib\/iptvnator-snap-release\/assets"/
);
assert.doesNotMatch(uploadStep.run, /\bnode\b/);
assert.doesNotMatch(uploadStep.run, /release-snap-assets\.cjs/);
assert.doesNotMatch(uploadStep.run, /\bfind\b|\bsort\b/);
assert.match(
uploadStep.run,
/SNAP_FILES=\("\$\{VERIFIED_ASSET_DIRECTORY\}"\/\*\.snap\)/
);
assert.match(
uploadStep.run,
/SNAPCRAFT_STORE_CREDENTIALS="\$\{STORE_CREDENTIALS\}" \/snap\/bin\/snapcraft upload/
);
assert.doesNotMatch(
uploadStep.run,
/snap-release-downloads\/\$\{SNAP_NAME\}/
);
assert.equal(
Object.hasOwn(publishJob.env ?? {}, 'SNAPCRAFT_STORE_CREDENTIALS'),
false
);
assert.equal(Object.hasOwn(publishJob.env ?? {}, 'GH_TOKEN'), false);
assert.deepEqual(selectStep.env, {
GH_TOKEN: '${{ github.token }}',
});
assert.deepEqual(downloadStep.env, {
GH_TOKEN: '${{ github.token }}',
});
assert.equal(verifyStep.env, undefined);
assert.deepEqual(uploadStep.env, {
SNAPCRAFT_STORE_CREDENTIALS: '${{ secrets.snapcraft_token }}',
});
});