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 = { origin: 'vendored-lgpl', sourceBuildOrigin: 'vendored-lgpl-source-build', stagedAt: '2026-07-18T01:00:00.000Z', 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, }; // 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', 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: snapSourceRuntime, }, }, '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) ); 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) => { 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', 'download-pinned-source.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 = snapSourceRoot; 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 packagedSourceRuntime = { ...sourceRuntime, origin: 'vendored-lgpl-source-build', }; delete packagedSourceRuntime.sourceBuildOrigin; delete packagedSourceRuntime.stagedAt; const packagedManifest = { ...fixture.snapPayloads['IPTVnator-amd64.snap'].manifest, sourceArchive, sourceRuntime: packagedSourceRuntime, }; 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', '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 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; 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 }}', RELEASE_ID: '${{ steps.resolve-release.outputs.release-id }}', }); assert.deepEqual(downloadStep.env, { GH_TOKEN: '${{ github.token }}', }); assert.equal(verifyStep.env, undefined); assert.deepEqual(uploadStep.env, { SNAPCRAFT_STORE_CREDENTIALS: '${{ secrets.snapcraft_token }}', }); });