CI / verify (push) Failing after 21m12s
Preserve uncertain remote commits and destructive source-cleanup requirements across cancellation, persistence failures, retries, and restart recovery. Revalidate account availability after admission waits and prevent late upload results from bypassing cancellation state. Strengthen release argument, CI tag, source allowlist, and secret scanning gates. Run the Electron smoke suite in a fully hidden offscreen harness that cannot reveal, focus, or elevate native windows.
112 lines
4.6 KiB
JavaScript
112 lines
4.6 KiB
JavaScript
const PRODUCT_NAME = 'Multi Hoster Uploader';
|
||
const ARTIFACT_NAME = 'Multi-Hoster-Upload';
|
||
const NON_ENGLISH_RELEASE_PATTERN = /\b(?:das|dies(?:e|er|es)|fehler|für|hinzugefügt|jetzt|korrigiert|laufen|mit|nach|probleme|programm|sicherheit|stabil|und|verbessert|verbesserungen|werden|wieder|wurde|wurden|zuverlässig\w*|amélior\w*|après|avec|corrig\w*|et|les|mises|pour|reprennent|réseau|sécurité|téléversements)\b/iu;
|
||
const ENGLISH_RELEASE_PATTERN = /\b(?:added|after|and|are|bridge|cancel|changes|credential|files?|faster|fixed|hardened|improved|installation|more|new|notes?|now|recovery|release|reliable|resume|security|safer|smoother|source|the|updates?|uploads?|with|without)\b/i;
|
||
|
||
function requireEnglishReleaseNotes(value) {
|
||
const notes = typeof value === 'string' ? value.trim() : '';
|
||
const words = notes.match(/[A-Za-z]+(?:['’-][A-Za-z]+)*/g) || [];
|
||
if (words.length < 2 || /(?![A-Za-z])\p{L}/u.test(notes) || NON_ENGLISH_RELEASE_PATTERN.test(notes) || !ENGLISH_RELEASE_PATTERN.test(notes)) {
|
||
throw new Error('English release notes are required');
|
||
}
|
||
return notes;
|
||
}
|
||
|
||
export function parseReleaseArgs(args) {
|
||
const version = Array.isArray(args) ? args[0] : '';
|
||
if (!/^\d+\.\d+\.\d+$/.test(version || '')) {
|
||
throw new Error('Usage: <version> --transport-tag <vX.Y.Z> --notes <English release notes> [--dry-run]');
|
||
}
|
||
|
||
let transportTag = '';
|
||
let dryRun = false;
|
||
let notes = '';
|
||
const legacyNoteParts = [];
|
||
for (let index = 1; index < args.length; index++) {
|
||
const arg = args[index];
|
||
if (arg === '--transport-tag') {
|
||
if (transportTag) {
|
||
throw new Error('Duplicate option: --transport-tag');
|
||
}
|
||
transportTag = args[++index] || '';
|
||
if (!/^v\d+\.\d+\.\d+$/.test(transportTag)) {
|
||
throw new Error('--transport-tag must match vX.Y.Z');
|
||
}
|
||
continue;
|
||
}
|
||
if (arg === '--notes') {
|
||
if (notes) {
|
||
throw new Error('Duplicate option: --notes');
|
||
}
|
||
const value = args[++index];
|
||
if (typeof value !== 'string' || !value.trim() || value.startsWith('-')) {
|
||
throw new Error('--notes requires English release notes');
|
||
}
|
||
notes = value;
|
||
continue;
|
||
}
|
||
if (arg === '--dry-run') {
|
||
if (dryRun) {
|
||
throw new Error('Duplicate option: --dry-run');
|
||
}
|
||
dryRun = true;
|
||
continue;
|
||
}
|
||
if (typeof arg !== 'string' || arg.startsWith('-')) {
|
||
throw new Error(`Unknown option: ${String(arg)}`);
|
||
}
|
||
if (notes) {
|
||
throw new Error(`Unexpected release note argument: ${arg}`);
|
||
}
|
||
legacyNoteParts.push(arg);
|
||
}
|
||
|
||
if (!/^v\d+\.\d+\.\d+$/.test(transportTag)) {
|
||
throw new Error('--transport-tag must match vX.Y.Z');
|
||
}
|
||
|
||
const releaseNotes = requireEnglishReleaseNotes(notes || legacyNoteParts.join(' '));
|
||
return { version, transportTag, notes: releaseNotes, dryRun };
|
||
}
|
||
|
||
export function createReleasePlan(options) {
|
||
const releaseBody = requireEnglishReleaseNotes(options.notes);
|
||
const releaseTitle = `${PRODUCT_NAME} v${options.version}`;
|
||
const setupName = `${ARTIFACT_NAME} Setup ${options.version}.exe`;
|
||
const portableName = `${ARTIFACT_NAME} ${options.version}.exe`;
|
||
const blockmapName = `${setupName}.blockmap`;
|
||
const githubSetupName = setupName.replaceAll(' ', '.');
|
||
const githubPortableName = portableName.replaceAll(' ', '.');
|
||
const githubBlockmapName = blockmapName.replaceAll(' ', '.');
|
||
return {
|
||
...options,
|
||
tag: options.transportTag,
|
||
releaseTitle,
|
||
releaseBody,
|
||
setupName,
|
||
portableName,
|
||
blockmapName,
|
||
githubSetupName,
|
||
githubPortableName,
|
||
githubBlockmapName,
|
||
expectedArtifacts: [setupName, portableName, blockmapName, 'latest.yml'],
|
||
githubExpectedArtifacts: [githubSetupName, githubPortableName, githubBlockmapName, 'latest.yml']
|
||
};
|
||
}
|
||
|
||
export function resolveExistingReleaseId(plan, release) {
|
||
const existingTitle = typeof release?.name === 'string' ? release.name : '';
|
||
const existingTag = typeof release?.tag_name === 'string' ? release.tag_name : '';
|
||
if (existingTag !== plan.tag) {
|
||
throw new Error(`Refusing recovery for ${plan.tag}: existing release tag "${existingTag}" does not match "${plan.tag}"`);
|
||
}
|
||
if (existingTitle !== plan.releaseTitle) {
|
||
throw new Error(`Refusing recovery for ${plan.tag}: existing release title "${existingTitle}" does not match "${plan.releaseTitle}"`);
|
||
}
|
||
return release.id;
|
||
}
|
||
|
||
export function renderLatestYml(plan, sha, size, releaseDate = new Date().toISOString(), setupName = plan.setupName) {
|
||
return `version: ${plan.version}\nfiles:\n - url: ${setupName}\n sha512: ${sha}\n size: ${size}\npath: ${setupName}\nsha512: ${sha}\nreleaseDate: '${releaseDate}'\n`;
|
||
}
|