Windows CI / verify (push) Failing after 4m30s
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.
170 lines
8.2 KiB
JavaScript
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
|
|
};
|