Files
Twitch-VOD-Manager/scripts/smoke-test-managed-tools-live.js
Sucukdeluxe 5f88897c75
Windows CI / verify (push) Failing after 4m30s
fix(ci): accept the lowercase RUNNER_OS reported by Gitea act runners
The live managed-tool smoke and the real installer smoke gated on
RUNNER_OS being exactly Windows. The Gitea act runner reports the value
as lowercase windows, so both gates rejected the approved self-hosted
git.24-music.de runner and the corresponding verify steps could never
run there. The gates now compare RUNNER_OS case-insensitively while
keeping every other identity requirement unchanged, with contract
coverage for the lowercase Gitea identity.
2026-08-14 15:21:14 +02:00

170 lines
8.2 KiB
JavaScript

const fs = require('node:fs');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const root = path.resolve(__dirname, '..');
function assertActionsWindowsCi(environment = process.env, platform = process.platform) {
const serverUrl = String(environment.GITHUB_SERVER_URL || '').replace(/\/+$/, '').toLowerCase();
const isGitHubActions = environment.GITHUB_ACTIONS === 'true' && environment.GITEA_ACTIONS !== 'true' && environment.RUNNER_ENVIRONMENT === 'github-hosted' && serverUrl === 'https://github.com';
const isGiteaActions = environment.GITEA_ACTIONS === 'true' && serverUrl === 'https://git.24-music.de';
const runnerOs = String(environment.RUNNER_OS || '').toLowerCase();
if (platform !== 'win32' || environment.CI !== 'true' || runnerOs !== 'windows' || !environment.RUNNER_TEMP || !environment.GITHUB_RUN_ID || (!isGitHubActions && !isGiteaActions)) {
throw new Error('Live managed-tool smoke is restricted to an approved Windows Actions runner');
}
}
function assertPathInside(targetPath, parentPath) {
const relative = path.win32.relative(path.win32.resolve(parentPath), path.win32.resolve(targetPath));
if (!relative || relative === '..' || relative.startsWith('..\\') || path.win32.isAbsolute(relative)) {
throw new Error(`Refusing cleanup outside the owned runner directory: ${targetPath}`);
}
}
function findFileRecursive(directory, fileName) {
if (!fs.existsSync(directory)) return '';
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
const entryPath = path.join(directory, entry.name);
if (entry.isFile() && entry.name.toLowerCase() === fileName.toLowerCase()) return entryPath;
if (entry.isDirectory()) {
const nested = findFileRecursive(entryPath, fileName);
if (nested) return nested;
}
}
return '';
}
function corruptInstalledTools(streamlinkDirectory, ffmpegDirectory) {
const streamlinkPath = findFileRecursive(streamlinkDirectory, 'streamlink.exe');
const ffmpegPath = findFileRecursive(ffmpegDirectory, 'ffmpeg.exe');
if (!streamlinkPath || !ffmpegPath) throw new Error('Managed executables are missing before corruption check');
fs.rmSync(streamlinkPath);
fs.appendFileSync(ffmpegPath, 'corrupt');
return { ffmpegPath, streamlinkPath };
}
function runVersionCheck(executablePath, args, label) {
const result = spawnSync(executablePath, args, {
encoding: 'utf8',
timeout: 60000,
windowsHide: true,
maxBuffer: 4 * 1024 * 1024
});
if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(`${label} version check failed: ${JSON.stringify({ status: result.status, stderr: result.stderr })}`);
}
const output = `${result.stdout || ''}\n${result.stderr || ''}`.trim();
if (!output) throw new Error(`${label} version check produced no output`);
return output.split(/\r?\n/, 1)[0];
}
function assertPinnedVersion(output, version, label) {
if (!output.toLowerCase().includes(version.toLowerCase())) {
throw new Error(`${label} version output does not match the pinned ${version}: ${output}`);
}
}
function assertVerified(statuses, manifest, phase) {
for (const id of ['streamlink', 'ffmpeg']) {
if (!statuses[id]?.verified || statuses[id]?.state !== 'verified' || statuses[id]?.version !== manifest[id].version) {
throw new Error(`${id} is not verified after ${phase}: ${JSON.stringify(statuses[id])}`);
}
}
}
async function main() {
assertActionsWindowsCi();
const runnerTemp = process.env.RUNNER_TEMP;
if (!runnerTemp || !path.win32.isAbsolute(runnerTemp) || !fs.statSync(runnerTemp).isDirectory()) {
throw new Error(`Actions runner temp directory is invalid: ${runnerTemp || ''}`);
}
const smokeRoot = fs.mkdtempSync(path.join(runnerTemp, 'tvm-managed-tools-'));
assertPathInside(smokeRoot, runnerTemp);
try {
const streamlinkDirectory = path.join(smokeRoot, 'tools', 'streamlink');
const ffmpegDirectory = path.join(smokeRoot, 'tools', 'ffmpeg');
const temporaryDirectory = path.join(smokeRoot, 'temporary');
fs.mkdirSync(temporaryDirectory, { recursive: true });
const toolsPath = path.join(root, 'dist', 'tools.js');
const manifestPath = path.join(root, 'dist', 'main', 'domain', 'tool-manifest.js');
if (!fs.existsSync(toolsPath)) throw new Error('Build output is missing; run npm run build first');
if (!fs.existsSync(manifestPath)) throw new Error('Built tool manifest is missing; run npm run build first');
const tools = require(toolsPath);
const { APPLICATION_TOOL_MANIFEST: manifest } = require(manifestPath);
tools.initToolDirs(streamlinkDirectory, ffmpegDirectory, () => temporaryDirectory);
const initial = await tools.getManagedToolStatuses();
if (initial.streamlink.state !== 'missing' || initial.ffmpeg.state !== 'missing') {
throw new Error(`Clean managed-tool surface is not empty: ${JSON.stringify(initial)}`);
}
const firstRepair = await tools.repairManagedTools();
if (!firstRepair.success) throw new Error(`Initial managed-tool provisioning failed: ${JSON.stringify(firstRepair.statuses)}`);
assertVerified(firstRepair.statuses, manifest, 'initial provisioning');
const initialPaths = {
streamlink: fs.realpathSync.native(tools.getStreamlinkPath()),
ffmpeg: fs.realpathSync.native(tools.getFFmpegPath()),
ffprobe: fs.realpathSync.native(tools.getFFprobePath())
};
assertPathInside(initialPaths.streamlink, streamlinkDirectory);
assertPathInside(initialPaths.ffmpeg, ffmpegDirectory);
assertPathInside(initialPaths.ffprobe, ffmpegDirectory);
const initialVersions = {
streamlink: runVersionCheck(initialPaths.streamlink, ['--version'], 'Streamlink'),
ffmpeg: runVersionCheck(initialPaths.ffmpeg, ['-version'], 'FFmpeg'),
ffprobe: runVersionCheck(initialPaths.ffprobe, ['-version'], 'FFprobe')
};
assertPinnedVersion(initialVersions.streamlink, manifest.streamlink.version, 'Streamlink');
assertPinnedVersion(initialVersions.ffmpeg, manifest.ffmpeg.version, 'FFmpeg');
assertPinnedVersion(initialVersions.ffprobe, manifest.ffmpeg.version, 'FFprobe');
corruptInstalledTools(streamlinkDirectory, ffmpegDirectory);
tools.invalidateVerifiedToolCaches();
const damaged = await tools.getManagedToolStatuses();
if (damaged.streamlink.state !== 'corrupt' || damaged.ffmpeg.state !== 'corrupt') {
throw new Error(`Damaged managed tools were not detected: ${JSON.stringify(damaged)}`);
}
const secondRepair = await tools.repairManagedTools();
if (!secondRepair.success) throw new Error(`Managed-tool repair failed: ${JSON.stringify(secondRepair.statuses)}`);
assertVerified(secondRepair.statuses, manifest, 'corruption repair');
const repairedPaths = {
streamlink: fs.realpathSync.native(tools.getStreamlinkPath()),
ffmpeg: fs.realpathSync.native(tools.getFFmpegPath()),
ffprobe: fs.realpathSync.native(tools.getFFprobePath())
};
assertPathInside(repairedPaths.streamlink, streamlinkDirectory);
assertPathInside(repairedPaths.ffmpeg, ffmpegDirectory);
assertPathInside(repairedPaths.ffprobe, ffmpegDirectory);
const repairedVersions = {
streamlink: runVersionCheck(repairedPaths.streamlink, ['--version'], 'Repaired Streamlink'),
ffmpeg: runVersionCheck(repairedPaths.ffmpeg, ['-version'], 'Repaired FFmpeg'),
ffprobe: runVersionCheck(repairedPaths.ffprobe, ['-version'], 'Repaired FFprobe')
};
assertPinnedVersion(repairedVersions.streamlink, manifest.streamlink.version, 'Repaired Streamlink');
assertPinnedVersion(repairedVersions.ffmpeg, manifest.ffmpeg.version, 'Repaired FFmpeg');
assertPinnedVersion(repairedVersions.ffprobe, manifest.ffmpeg.version, 'Repaired FFprobe');
console.log(JSON.stringify({ failures: [], initialVersions, repairedVersions }, null, 2));
} finally {
assertPathInside(smokeRoot, runnerTemp);
await fs.promises.rm(smokeRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 250 });
}
}
if (require.main === module) {
main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});
}
module.exports = {
assertActionsWindowsCi,
assertPathInside,
corruptInstalledTools,
findFileRecursive
};