diff --git a/.github/scripts/app.test.js b/.github/scripts/app.test.js new file mode 100644 index 00000000..43ab94ac --- /dev/null +++ b/.github/scripts/app.test.js @@ -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'); +}); diff --git a/.github/scripts/cleanup-releases.js b/.github/scripts/cleanup-releases.js index 7ac3571c..c3b20ad9 100644 --- a/.github/scripts/cleanup-releases.js +++ b/.github/scripts/cleanup-releases.js @@ -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 @@ -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)); } } diff --git a/.github/scripts/download-utils.js b/.github/scripts/download-utils.js index af54f607..a68dd3fc 100644 --- a/.github/scripts/download-utils.js +++ b/.github/scripts/download-utils.js @@ -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; diff --git a/.github/scripts/file-utils.js b/.github/scripts/file-utils.js index 52232cac..ba07b6b6 100644 --- a/.github/scripts/file-utils.js +++ b/.github/scripts/file-utils.js @@ -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 diff --git a/.github/scripts/generate-packages.js b/.github/scripts/generate-packages.js index 0f5ae09b..6c6f513b 100644 --- a/.github/scripts/generate-packages.js +++ b/.github/scripts/generate-packages.js @@ -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. @@ -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, diff --git a/.github/scripts/hash-utils.js b/.github/scripts/hash-utils.js index 55a164d0..d5d72641 100644 --- a/.github/scripts/hash-utils.js +++ b/.github/scripts/hash-utils.js @@ -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'); /** diff --git a/.github/scripts/package-config.js b/.github/scripts/package-config.js index 1e94e12e..2fddbf16 100644 --- a/.github/scripts/package-config.js +++ b/.github/scripts/package-config.js @@ -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'; @@ -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`\$&`); } /** diff --git a/.github/scripts/sync-assets-with-metadata.js b/.github/scripts/sync-assets-with-metadata.js index a9244b8d..258ea86e 100644 --- a/.github/scripts/sync-assets-with-metadata.js +++ b/.github/scripts/sync-assets-with-metadata.js @@ -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'); diff --git a/.github/scripts/sync-assets.js b/.github/scripts/sync-assets.js index 2732d6bd..5648852c 100644 --- a/.github/scripts/sync-assets.js +++ b/.github/scripts/sync-assets.js @@ -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'); @@ -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'); } /** @@ -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; @@ -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; @@ -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++; } @@ -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. @@ -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 }; } @@ -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; diff --git a/.github/scripts/sync-assets.test.js b/.github/scripts/sync-assets.test.js new file mode 100644 index 00000000..fffa20a9 --- /dev/null +++ b/.github/scripts/sync-assets.test.js @@ -0,0 +1,236 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const test = require('node:test'); +const { cleanupNonVPrefixedReleases } = require('./cleanup-releases'); +const { syncReleaseAssets } = require('./sync-assets'); + +const context = { repo: { owner: 'Example' } }; + +function createFixture(t, config = { defaultInclude: true, repositories: {} }) { + const originalDir = process.cwd(); + const tempRoot = path.resolve(os.tmpdir()); + const fixturePath = fs.mkdtempSync(path.join(tempRoot, 'packages-sonar-')); + const distPath = path.join(fixturePath, 'dist'); + fs.mkdirSync(distPath); + fs.writeFileSync(path.join(fixturePath, 'packages.config.json'), JSON.stringify(config)); + process.chdir(distPath); + t.mock.method(console, 'log', () => {}); + t.mock.method(console, 'error', () => {}); + + t.after(() => { + process.chdir(originalDir); + const resolvedPath = path.resolve(fixturePath); + assert.equal(path.dirname(resolvedPath), tempRoot); + assert.ok(path.basename(resolvedPath).startsWith('packages-sonar-')); + fs.rmSync(resolvedPath, { recursive: true, force: true }); + }); + + return function writeFixtureFile(relativePath, content = 'asset') { + const filePath = path.resolve(distPath, relativePath); + assert.ok(filePath.startsWith(`${distPath}${path.sep}`)); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, content); + }; +} + +function createGithub(releasesByRepo) { + const listForOrg = () => {}; + const listReleases = () => {}; + return { + rest: { repos: { listForOrg, listReleases } }, + async paginate(method, params) { + if (method === listForOrg) { + return Object.keys(releasesByRepo).map(name => ({ + name, archived: false, html_url: `https://example.com/${name}` + })); + } + const releases = releasesByRepo[params.repo]; + if (releases instanceof Error) { + throw releases; + } + return releases; + } + }; +} + +function createRelease(tag, assets = []) { + return { + tag_name: tag, draft: false, prerelease: false, + html_url: `https://example.com/releases/${tag}`, + published_at: '2026-10-08T00:00:00Z', + assets: assets.map(asset => ({ + size: 5, browser_download_url: `https://example.com/assets/${asset.name}`, ...asset + })) + }; +} + +function mockDownloads(t) { + const downloads = []; + t.mock.method(globalThis, 'fetch', async url => { + downloads.push(url); + return new Response('asset'); + }); + return downloads; +} + +test('cleanup retains v-prefixed releases, hidden roots, and repository files', t => { + const write = createFixture(t); + write('Alpha/v1/keep.zip'); + write('Alpha/nightly/remove.zip'); + write('Alpha/README.md'); + write('.git/refs/heads/dist'); + write('.hidden/nightly/keep.zip'); + write('packages.json'); + + cleanupNonVPrefixedReleases('.'); + + for (const retained of ['Alpha/v1/keep.zip', 'Alpha/README.md', '.git/refs/heads/dist', + '.hidden/nightly/keep.zip', 'packages.json']) { + assert.ok(fs.existsSync(retained), retained); + } + assert.equal(fs.existsSync('Alpha/nightly'), false); +}); + +test('cleanup continues after a repository read error and propagates a root read error', t => { + const write = createFixture(t); + write('Alpha/nightly/keep.zip'); + write('Beta/nightly/remove.zip'); + const readDirectory = fs.readdirSync; + t.mock.method(fs, 'readdirSync', (directory, options) => { + if (directory === 'Alpha' || directory === 'missing') { + throw new Error('read failed'); + } + return readDirectory(directory, options); + }); + + cleanupNonVPrefixedReleases('.'); + assert.ok(fs.existsSync('Alpha/nightly/keep.zip')); + assert.equal(fs.existsSync('Beta/nightly'), false); + assert.throws(() => cleanupNonVPrefixedReleases('missing'), /read failed/); +}); + +test('config cleanup preserves retained assets and hashes while removing excluded files and empty directories', async t => { + const write = createFixture(t, { + defaultInclude: false, repositories: { Alpha: { includeAssets: ['keep.zip'] } } + }); + write('Alpha/v1/keep.zip'); + write('Alpha/v1/drop.zip'); + for (const suffix of ['sha256', 'sha512', 'md5']) { + write(`Alpha/v1/keep.zip.${suffix}`); + write(`Alpha/v1/drop.zip.${suffix}`); + write(`Alpha/v1/orphan.zip.${suffix}`); + } + write('Alpha/v2/drop.zip'); + write('Beta/v1/drop.zip'); + write('.git/keep'); + write('.hidden/v1/keep'); + write('packages.json'); + + await syncReleaseAssets(createGithub({}), context); + + for (const retained of ['Alpha/v1/keep.zip', '.git/keep', '.hidden/v1/keep', 'packages.json']) { + assert.ok(fs.existsSync(retained), retained); + } + for (const suffix of ['sha256', 'sha512', 'md5']) { + assert.ok(fs.existsSync(`Alpha/v1/keep.zip.${suffix}`)); + assert.equal(fs.existsSync(`Alpha/v1/drop.zip.${suffix}`), false); + assert.equal(fs.existsSync(`Alpha/v1/orphan.zip.${suffix}`), false); + } + for (const removed of ['Alpha/v1/drop.zip', 'Alpha/v2', 'Beta']) { + assert.equal(fs.existsSync(removed), false, removed); + } +}); + +test('the download budget is shared across assets, releases, and repositories while metadata remains complete', async t => { + createFixture(t); + const downloads = mockDownloads(t); + const github = createGithub({ + Alpha: [createRelease('v3', [{ name: 'a.zip' }, { name: 'b.zip' }]), + createRelease('v2', [{ name: 'c.zip' }])], + Beta: [createRelease('v1', [{ name: 'd.zip' }])] + }); + + const data = await syncReleaseAssets(github, context, false, 2); + + assert.equal(downloads.length, 2); + assert.deepEqual(data.map(repo => repo.releases.map(release => release.assetCount)), [[2, 1], [1]]); + assert.equal(data[0].releases[0].assets[0].directUrl, 'Alpha/v3/a.zip'); + assert.equal(data[0].releases[1].assets[0].directUrl, undefined); + assert.equal(data[1].releases[0].assets[0].directUrl, undefined); + for (const name of ['a.zip', 'b.zip']) { + assert.ok(fs.existsSync(`Alpha/v3/${name}`)); + for (const suffix of ['sha256', 'sha512', 'md5']) { + assert.ok(fs.existsSync(`Alpha/v3/${name}.${suffix}`)); + } + } +}); + +test('PR mode counts only published v-prefixed releases with assets toward the two-release limit', async t => { + createFixture(t, { defaultInclude: false, repositories: {} }); + const downloads = mockDownloads(t); + const github = createGithub({ Alpha: [ + { ...createRelease('v6', [{ name: 'draft.zip' }]), draft: true }, + { ...createRelease('v5', [{ name: 'preview.zip' }]), prerelease: true }, + createRelease('nightly', [{ name: 'nightly.zip' }]), + createRelease('v4'), + createRelease('v3', [{ name: 'a.zip' }]), + createRelease('v2', [{ name: 'b.zip' }]), + createRelease('v1', [{ name: 'c.zip' }]) + ] }); + + const data = await syncReleaseAssets(github, context, true); + + assert.deepEqual(data[0].releases.map(release => release.tag), ['v3', 'v2']); + assert.equal(downloads.length, 0); +}); + +test('a failed repository does not prevent the next repository from being processed', async t => { + createFixture(t, { defaultInclude: false, repositories: {} }); + const data = await syncReleaseAssets(createGithub({ + Alpha: new Error('API unavailable'), Beta: [createRelease('v1', [{ name: 'b.zip' }])] + }), context); + assert.deepEqual(data.map(repo => repo.name), ['Beta']); +}); + +test('oversized stored assets and their hashes are removed without consuming the download budget', async t => { + const write = createFixture(t); + write('Alpha/v1/huge.zip'); + for (const suffix of ['sha256', 'sha512', 'md5']) { + write(`Alpha/v1/huge.zip.${suffix}`); + } + const downloads = mockDownloads(t); + const data = await syncReleaseAssets(createGithub({ Alpha: [createRelease('v1', [ + { name: 'huge.zip', size: 51 * 1024 * 1024 }, { name: 'small.zip' } + ])] }), context, false, 1); + + assert.equal(downloads.length, 1); + assert.equal(fs.existsSync('Alpha/v1/huge.zip'), false); + for (const suffix of ['sha256', 'sha512', 'md5']) { + assert.equal(fs.existsSync(`Alpha/v1/huge.zip.${suffix}`), false); + } + assert.equal(data[0].releases[0].assets.find(asset => asset.name === 'huge.zip').directUrl, undefined); + assert.ok(fs.existsSync('Alpha/v1/small.zip')); +}); + +test('oversized removal failures are contained and subsequent assets are downloaded', async t => { + const write = createFixture(t); + write('Alpha/v1/huge.zip'); + const unlink = fs.unlinkSync; + t.mock.method(fs, 'unlinkSync', filePath => { + if (filePath === path.join('Alpha', 'v1', 'huge.zip')) { + throw new Error('file locked'); + } + return unlink(filePath); + }); + const downloads = mockDownloads(t); + + await syncReleaseAssets(createGithub({ Alpha: [createRelease('v1', [ + { name: 'huge.zip', size: 51 * 1024 * 1024 }, { name: 'small.zip' } + ])] }), context, false, 1); + + assert.equal(downloads.length, 1); + assert.ok(fs.existsSync('Alpha/v1/huge.zip')); + assert.ok(fs.existsSync('Alpha/v1/small.zip')); +}); diff --git a/gh-pages-template/assets/js/app.js b/gh-pages-template/assets/js/app.js index aced22cc..84b7538f 100644 --- a/gh-pages-template/assets/js/app.js +++ b/gh-pages-template/assets/js/app.js @@ -653,5 +653,5 @@ class LizardByteAssetsApp { // Initialize the application when the DOM is loaded document.addEventListener('DOMContentLoaded', () => { const app = new LizardByteAssetsApp(); - app.init(); + app.init().catch(error => console.error('Failed to start application:', error)); });