Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/scripts/app.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const vm = require('node:vm');

test('application startup handles an unexpected initialization rejection', async () => {
let onReady;
const errors = [];
const sandbox = vm.createContext({
document: {
title: 'Packages',
addEventListener(event, handler) {
assert.equal(event, 'DOMContentLoaded');
onReady = handler;
}
},
console: { error: (...args) => errors.push(args) }
});
const appPath = path.resolve(__dirname, '../../gh-pages-template/assets/js/app.js');
vm.runInContext(fs.readFileSync(appPath, 'utf8'), sandbox);
vm.runInContext(`
UIManager = class {};
LizardByteAssetsApp.prototype.init = () => Promise.reject(new Error('startup failed'));
`, sandbox);

onReady();
await new Promise(resolve => setImmediate(resolve));

assert.equal(errors.length, 1);
assert.equal(errors[0][0], 'Failed to start application:');
assert.equal(errors[0][1].message, 'startup failed');
});
40 changes: 23 additions & 17 deletions .github/scripts/cleanup-releases.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
const fs = require('fs');
const path = require('path');
const fs = require('node:fs');
const path = require('node:path');

/**
* Remove non-v-prefixed release directories from a single repository.
* @param {string} repoPath - Path to the repository directory.
*/
function cleanupRepositoryReleases(repoPath) {
try {
const repoContents = fs.readdirSync(repoPath, { withFileTypes: true });

for (const releaseDir of repoContents) {
if (releaseDir.isDirectory() && !releaseDir.name.startsWith('v')) {
const releasePath = path.join(repoPath, releaseDir.name);
console.log(`Removing non-v-prefixed release directory: ${releasePath}`);
fs.rmSync(releasePath, { recursive: true, force: true });
}
}
} catch (repoError) {
console.log(`Error processing repository ${path.basename(repoPath)}:`, repoError.message);
}
}

/**
* Clean up non-v-prefixed release directories from the dist directory
Expand All @@ -17,21 +37,7 @@ function cleanupNonVPrefixedReleases(distPath = '.') {
dirent.name !== 'packages.json' &&
!dirent.name.startsWith('.')) {

const repoPath = path.join(distPath, dirent.name);

try {
const repoContents = fs.readdirSync(repoPath, { withFileTypes: true });

for (const releaseDir of repoContents) {
if (releaseDir.isDirectory() && !releaseDir.name.startsWith('v')) {
const releasePath = path.join(repoPath, releaseDir.name);
console.log(`Removing non-v-prefixed release directory: ${releasePath}`);
fs.rmSync(releasePath, { recursive: true, force: true });
}
}
} catch (repoError) {
console.log(`Error processing repository ${dirent.name}:`, repoError.message);
}
cleanupRepositoryReleases(path.join(distPath, dirent.name));
}
}

Expand Down
2 changes: 1 addition & 1 deletion .github/scripts/download-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ async function downloadAssetWithRetry(url, filePath, token, maxRetries = 3) {
throw error;
}
// Wait before retry (exponential backoff)
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, attempt - 1)));
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, attempt - 1))); // NOSONAR javascript:S9382: Retry backoff must finish before the next attempt.
}
}
return false;
Expand Down
4 changes: 2 additions & 2 deletions .github/scripts/file-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
* File System Utilities
* Handles directory creation and file operations
*/
const fs = require('fs');
const path = require('path');
const fs = require('node:fs');
const path = require('node:path');

/**
* Ensure directory exists, create if it doesn't
Expand Down
6 changes: 3 additions & 3 deletions .github/scripts/generate-packages.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const fs = require('fs');
const path = require('path');
const fs = require('node:fs');
const path = require('node:path');

/**
* Encode path segments for a browser URL without changing the dist directory layout.
Expand Down Expand Up @@ -58,7 +58,7 @@ function normalizeRepositoryMetadata(repositoryMetadata = []) {
.map(release => {
const assets = Array.isArray(release.assets)
? release.assets
.filter(asset => asset && asset.name)
.filter(asset => asset?.name)
.map(asset => ({
name: asset.name,
size: asset.size,
Expand Down
2 changes: 1 addition & 1 deletion .github/scripts/hash-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Hash Generation Utilities
* Handles generating hash files for downloaded assets
*/
const crypto = require('crypto');
const crypto = require('node:crypto');
const { readFile, writeFile } = require('./file-utils');

/**
Expand Down
6 changes: 3 additions & 3 deletions .github/scripts/package-config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const fs = require('fs');
const path = require('path');
const fs = require('node:fs');
const path = require('node:path');

const CONFIG_FILENAME = 'packages.config.json';

Expand Down Expand Up @@ -72,7 +72,7 @@ function loadPackageConfig(startDir = process.cwd()) {
* @returns {string} Regex-escaped segment.
*/
function escapeRegex(value) {
return value.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
return value.replace(/[|\\{}()[\]^$+?.]/g, String.raw`\$&`);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion .github/scripts/sync-assets-with-metadata.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const fs = require('fs');
const fs = require('node:fs');
const { syncReleaseAssets } = require('./sync-assets.js');
const { buildPackagesData, writePackagesJson } = require('./generate-packages.js');

Expand Down
149 changes: 90 additions & 59 deletions .github/scripts/sync-assets.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
* Asset Synchronization Script
* Main script for downloading and organizing release assets
*/
const fs = require('fs');
const path = require('path');
const fs = require('node:fs');
const path = require('node:path');
const { ensureDir, fileExists } = require('./file-utils');
const { generateHashFiles } = require('./hash-utils');
const { downloadAssetWithRetry } = require('./download-utils');
Expand Down Expand Up @@ -116,52 +116,74 @@ function cleanupStoredAssets(distPath, packageConfig) {
}

const repoPath = path.join(distPath, repoDir.name);
const repoContents = fs.readdirSync(repoPath, { withFileTypes: true });
cleanupRepositoryAssets(repoPath, repoDir.name, packageConfig);

for (const releaseDir of repoContents) {
if (!releaseDir.isDirectory() || !releaseDir.name.startsWith('v')) {
continue;
}
if (fs.readdirSync(repoPath).length === 0) {
fs.rmSync(repoPath, { recursive: true, force: true });
console.log(`Removed empty repository directory: ${repoPath}`);
}
}

const releasePath = path.join(repoPath, releaseDir.name);
const releaseContents = fs.readdirSync(releasePath, { withFileTypes: true });
console.log('Package config cleanup completed successfully');
}

for (const assetFile of releaseContents) {
if (!assetFile.isFile() || isHashFile(assetFile.name)) {
continue;
}
/**
* Remove excluded assets from each v-prefixed release in a repository.
* @param {string} repoPath - Repository directory path.
* @param {string} repoName - Repository name used by the package config.
* @param {Object} packageConfig - Loaded package config.
*/
function cleanupRepositoryAssets(repoPath, repoName, packageConfig) {
const repoContents = fs.readdirSync(repoPath, { withFileTypes: true });

if (!shouldIncludeAsset(packageConfig, repoDir.name, assetFile.name)) {
removeAsset(path.join(releasePath, assetFile.name));
}
}
for (const releaseDir of repoContents) {
if (releaseDir.isDirectory() && releaseDir.name.startsWith('v')) {
cleanupReleaseAssets(path.join(repoPath, releaseDir.name), repoName, packageConfig);
}
}
}

for (const hashFile of fs.readdirSync(releasePath, { withFileTypes: true })) {
if (!hashFile.isFile() || !isHashFile(hashFile.name)) {
continue;
}
/**
* Remove excluded assets, orphan hashes, and an empty release directory.
* @param {string} releasePath - Release directory path.
* @param {string} repoName - Repository name used by the package config.
* @param {Object} packageConfig - Loaded package config.
*/
function cleanupReleaseAssets(releasePath, repoName, packageConfig) {
const releaseContents = fs.readdirSync(releasePath, { withFileTypes: true });

const assetName = hashFile.name.replace(/\.(sha256|sha512|md5)$/, '');
if (!fileExists(path.join(releasePath, assetName))) {
const hashPath = path.join(releasePath, hashFile.name);
fs.unlinkSync(hashPath);
console.log(`Removed orphan hash file: ${hashPath}`);
}
}
for (const assetFile of releaseContents) {
if (assetFile.isFile() && !isHashFile(assetFile.name) &&
!shouldIncludeAsset(packageConfig, repoName, assetFile.name)) {
removeAsset(path.join(releasePath, assetFile.name));
}
}

if (!hasAssetFiles(releasePath)) {
fs.rmSync(releasePath, { recursive: true, force: true });
console.log(`Removed empty release directory: ${releasePath}`);
}
removeOrphanHashFiles(releasePath);

if (!hasAssetFiles(releasePath)) {
fs.rmSync(releasePath, { recursive: true, force: true });
console.log(`Removed empty release directory: ${releasePath}`);
}
}

/**
* Remove generated hashes whose primary asset no longer exists.
* @param {string} releasePath - Release directory path.
*/
function removeOrphanHashFiles(releasePath) {
for (const hashFile of fs.readdirSync(releasePath, { withFileTypes: true })) {
if (!hashFile.isFile() || !isHashFile(hashFile.name)) {
continue;
}

if (fs.readdirSync(repoPath).length === 0) {
fs.rmSync(repoPath, { recursive: true, force: true });
console.log(`Removed empty repository directory: ${repoPath}`);
const assetName = hashFile.name.replace(/\.(sha256|sha512|md5)$/, '');
if (!fileExists(path.join(releasePath, assetName))) {
const hashPath = path.join(releasePath, hashFile.name);
fs.unlinkSync(hashPath);
console.log(`Removed orphan hash file: ${hashPath}`);
}
}

console.log('Package config cleanup completed successfully');
}

/**
Expand All @@ -171,14 +193,11 @@ function cleanupStoredAssets(distPath, packageConfig) {
* @param {Object} repo - Repository API response object.
* @param {Array} repositoryData - Accumulated package metadata.
* @param {number} totalAssets - Current total release asset count.
* @param {Object} packageConfig - Loaded package config.
* @param {boolean} isPullRequest - Whether this is a pull request run.
* @param {number|null} releaseLimit - Optional release limit for pull request runs.
* @param {number} maxNewAssets - Maximum new assets to download.
* @param {number} newAssetsDownloaded - Number of new assets already downloaded.
* @param {Object} options - Package config, release limits, and download counters for this run.
* @returns {Promise<{totalAssets: number, processedReleases: number, newAssetsDownloaded: number}>}
*/
async function processRepository(github, context, repo, repositoryData, totalAssets, packageConfig, isPullRequest = false, releaseLimit = null, maxNewAssets = 0, newAssetsDownloaded = 0) {
async function processRepository(github, context, repo, repositoryData, totalAssets, options) {
const { packageConfig, isPullRequest = false, releaseLimit = null, maxNewAssets = 0, newAssetsDownloaded = 0 } = options;
console.log(`Processing repository: ${repo.name}`);

let processedReleasesWithAssets = 0;
Expand Down Expand Up @@ -218,7 +237,9 @@ async function processRepository(github, context, repo, repositoryData, totalAss
break;
}

const result = await processRelease(repo.name, release, packageConfig, maxNewAssets, newAssetsDownloaded + repoNewAssets);
const result = await processRelease( // NOSONAR javascript:S9382: Releases share the PR count and download budget.
repo.name, release, packageConfig, maxNewAssets, newAssetsDownloaded + repoNewAssets
);
const assetCount = result.assetCount;
const newAssets = result.newAssets;

Expand Down Expand Up @@ -283,7 +304,7 @@ async function processRelease(repoName, release, packageConfig, maxNewAssets = 0
break;
}

const result = await processAsset(releaseDir, asset);
const result = await processAsset(releaseDir, asset); // NOSONAR javascript:S9382: Each download updates the remaining asset budget.
if (result.downloaded && result.isNew) {
newAssets++;
}
Expand All @@ -297,6 +318,21 @@ async function processRelease(repoName, release, packageConfig, maxNewAssets = 0
return { assetCount, newAssets, assets };
}

/**
* Remove a stored asset and its hashes when the release asset exceeds the size limit.
* @param {string} assetPath - Path to the primary asset file.
*/
function removeOversizedAsset(assetPath) {
if (fileExists(assetPath)) {
console.log(`Removing existing oversized file: ${assetPath}`);
try {
removeAsset(assetPath);
} catch (error) {
console.error(`Failed to remove oversized file ${assetPath}: ${error.message}`);
}
}
}

/**
* Process a single asset - download and generate hashes if not exists
* @param {string} releaseDir - Directory where the asset should be stored.
Expand All @@ -313,14 +349,7 @@ async function processAsset(releaseDir, asset) {
console.log(`Skipping ${asset.name} (${sizeMB}MB) - exceeds 50MB limit`);

// If the file already exists and is over the size limit, remove it
if (fileExists(assetPath)) {
console.log(`Removing existing oversized file: ${assetPath}`);
try {
removeAsset(assetPath);
} catch (error) {
console.error(`Failed to remove oversized file ${assetPath}: ${error.message}`);
}
}
removeOversizedAsset(assetPath);

return { downloaded: false, isNew: false };
}
Expand Down Expand Up @@ -416,17 +445,19 @@ async function syncReleaseAssets(github, context, isPullRequest = false, maxNewA

// Process each repository
for (const repo of repos) {
const result = await processRepository(
const result = await processRepository( // NOSONAR javascript:S9382: Repositories share the run's download budget.
github,
context,
repo,
repositoryData,
totalAssets,
packageConfig,
isPullRequest,
isPullRequest ? 2 : null,
maxNewAssets,
newAssetsDownloaded
{
packageConfig,
isPullRequest,
releaseLimit: isPullRequest ? 2 : null,
maxNewAssets,
newAssetsDownloaded
}
);
totalAssets = result.totalAssets;
totalProcessedReleases += result.processedReleases;
Expand Down
Loading
Loading