Refactor and improve external URL checks

- Move external URL checks to its own module under `tests/`. This
  separates them from integration test, addressing long runs and
  frequent failures that led to ignoring test results.
- Move `check-desktop-runtime-errors` to `tests/checks` to keep all
  test-related checks into one directory.
- Replace `ts-node` with `vite` for running
  `check-desktop-runtime-errors` to maintain a consistent execution
  environment across checks.
- Implement a timeout for each fetch call.
- Be nice to external sources, wait 5 seconds before sending another
  request to an URL under same domain. This solves rate-limiting issues.
- Instead of running test on every push/pull request, run them only
  weekly.
- Do not run tests on each commit/PR but only scheduled (weekly) to
  minimize noise.
- Fix URLs are not captured correctly inside backticks or parenthesis.
This commit is contained in:
undergroundwires
2023-09-01 00:18:47 +02:00
parent f4d86fccfd
commit 19e42c9c52
43 changed files with 400 additions and 449 deletions

View File

@@ -0,0 +1,46 @@
import { join } from 'path';
import { readdir } from 'fs/promises';
import { die } from '../../../utils/log';
import { exists } from '../../../utils/io';
import { getAppName } from '../../../utils/npm';
export async function findByFilePattern(
pattern: string,
directory: string,
projectRootDir: string,
): Promise<ArtifactLocation> {
if (!directory) { throw new Error('Missing directory'); }
if (!pattern) { throw new Error('Missing file pattern'); }
if (!await exists(directory)) {
return die(`Directory does not exist: ${directory}`);
}
const directoryContents = await readdir(directory);
const appName = await getAppName(projectRootDir);
const regexPattern = pattern
/* eslint-disable no-template-curly-in-string */
.replaceAll('${name}', escapeRegExp(appName))
.replaceAll('${version}', '\\d+\\.\\d+\\.\\d+')
.replaceAll('${ext}', '.*');
/* eslint-enable no-template-curly-in-string */
const regex = new RegExp(`^${regexPattern}$`);
const foundFileNames = directoryContents.filter((file) => regex.test(file));
if (!foundFileNames.length) {
return die(`No files found matching pattern "${pattern}" in ${directory} directory.`);
}
if (foundFileNames.length > 1) {
return die(`Found multiple files matching pattern "${pattern}": ${foundFileNames.join(', ')}`);
}
return {
absolutePath: join(directory, foundFileNames[0]),
};
}
function escapeRegExp(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
interface ArtifactLocation {
readonly absolutePath?: string;
}

View File

@@ -0,0 +1,4 @@
export interface ExtractionResult {
readonly appExecutablePath: string;
readonly cleanup?: () => Promise<void>;
}

View File

@@ -0,0 +1,40 @@
import { access, chmod } from 'fs/promises';
import { constants } from 'fs';
import { log } from '../../utils/log';
import { ExtractionResult } from './common/extraction-result';
import { findByFilePattern } from './common/app-artifact-locator';
export async function prepareLinuxApp(
desktopDistPath: string,
projectRootDir: string,
): Promise<ExtractionResult> {
const { absolutePath: appFile } = await findByFilePattern(
// eslint-disable-next-line no-template-curly-in-string
'${name}-${version}.AppImage',
desktopDistPath,
projectRootDir,
);
await makeExecutable(appFile);
return {
appExecutablePath: appFile,
};
}
async function makeExecutable(appFile: string): Promise<void> {
if (!appFile) { throw new Error('missing file'); }
if (await isExecutable(appFile)) {
log('AppImage is already executable.');
return;
}
log('Making it executable...');
await chmod(appFile, 0o755);
}
async function isExecutable(file: string): Promise<boolean> {
try {
await access(file, constants.X_OK);
return true;
} catch {
return false;
}
}

View File

@@ -0,0 +1,80 @@
import { runCommand } from '../../utils/run-command';
import { exists } from '../../utils/io';
import { log, die, LogLevel } from '../../utils/log';
import { sleep } from '../../utils/sleep';
import { ExtractionResult } from './common/extraction-result';
import { findByFilePattern } from './common/app-artifact-locator';
export async function prepareMacOsApp(
desktopDistPath: string,
projectRootDir: string,
): Promise<ExtractionResult> {
const { absolutePath: dmgPath } = await findByFilePattern(
// eslint-disable-next-line no-template-curly-in-string
'${name}-${version}.dmg',
desktopDistPath,
projectRootDir,
);
const { mountPath } = await mountDmg(dmgPath);
const appPath = await findMacAppExecutablePath(mountPath);
return {
appExecutablePath: appPath,
cleanup: async () => {
log('Cleaning up resources...');
await detachMount(mountPath);
},
};
}
async function mountDmg(
dmgFile: string,
) {
const { stdout: hdiutilOutput, error } = await runCommand(
`hdiutil attach '${dmgFile}'`,
);
if (error) {
die(`Failed to mount DMG file at ${dmgFile}.\n${error}`);
}
const mountPathMatch = hdiutilOutput.match(/\/Volumes\/[^\n]+/);
const mountPath = mountPathMatch ? mountPathMatch[0] : null;
return {
mountPath,
};
}
async function findMacAppExecutablePath(
mountPath: string,
): Promise<string> {
const { stdout: findOutput, error } = await runCommand(
`find '${mountPath}' -maxdepth 1 -type d -name "*.app"`,
);
if (error) {
return die(`Failed to find executable path at mount path ${mountPath}\n${error}`);
}
const appFolder = findOutput.trim();
const appName = appFolder.split('/').pop().replace('.app', '');
const appPath = `${appFolder}/Contents/MacOS/${appName}`;
if (await exists(appPath)) {
log(`Application is located at ${appPath}`);
} else {
return die(`Application does not exist at ${appPath}`);
}
return appPath;
}
async function detachMount(
mountPath: string,
retries = 5,
) {
const { error } = await runCommand(`hdiutil detach '${mountPath}'`);
if (error) {
if (retries <= 0) {
log(`Failed to detach mount after multiple attempts: ${mountPath}\n${error}`, LogLevel.Warn);
return;
}
await sleep(500);
await detachMount(mountPath, retries - 1);
return;
}
log(`Successfully detached from ${mountPath}`);
}

View File

@@ -0,0 +1,58 @@
import { mkdtemp, rm } from 'fs/promises';
import { join } from 'path';
import { tmpdir } from 'os';
import { exists } from '../../utils/io';
import { log, die, LogLevel } from '../../utils/log';
import { runCommand } from '../../utils/run-command';
import { ExtractionResult } from './common/extraction-result';
import { findByFilePattern } from './common/app-artifact-locator';
export async function prepareWindowsApp(
desktopDistPath: string,
projectRootDir: string,
): Promise<ExtractionResult> {
const workdir = await mkdtemp(join(tmpdir(), 'win-nsis-installation-'));
if (await exists(workdir)) {
log(`Temporary directory ${workdir} already exists, cleaning up...`);
await rm(workdir, { recursive: true });
}
const appExecutablePath = await installNsis(workdir, desktopDistPath, projectRootDir);
return {
appExecutablePath,
cleanup: async () => {
log(`Cleaning up working directory ${workdir}...`);
try {
await rm(workdir, { recursive: true, force: true });
} catch (error) {
log(`Could not cleanup the working directory: ${error.message}`, LogLevel.Error);
}
},
};
}
async function installNsis(
installationPath: string,
desktopDistPath: string,
projectRootDir: string,
): Promise<string> {
const { absolutePath: installerPath } = await findByFilePattern(
// eslint-disable-next-line no-template-curly-in-string
'${name}-Setup-${version}.exe',
desktopDistPath,
projectRootDir,
);
log(`Silently installing contents of ${installerPath} to ${installationPath}...`);
const { error } = await runCommand(`"${installerPath}" /S /D=${installationPath}`);
if (error) {
return die(`Failed to install.\n${error}`);
}
const { absolutePath: appExecutablePath } = await findByFilePattern(
// eslint-disable-next-line no-template-curly-in-string
'${name}.exe',
installationPath,
projectRootDir,
);
return appExecutablePath;
}