From 3c0709189e9ad3bf2983206820b922c7052150dc Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl Date: Sun, 30 Aug 2026 11:08:01 +0000 Subject: [PATCH] fix: validate release asset uploads --- src/api.js | 9 ++++++++- src/cli.js | 7 +++++-- test/api.test.js | 14 ++++++++++++++ test/cli.test.js | 9 +++++++++ 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/api.js b/src/api.js index 3fc874f..b88566f 100644 --- a/src/api.js +++ b/src/api.js @@ -18,6 +18,8 @@ const REQUEST_TIMEOUT_MS = 30000; // Repository migrations clone the full source repository and can legitimately // take minutes, so they get a much longer budget. const MIGRATE_TIMEOUT_MS = 10 * 60 * 1000; +// Release assets can be much larger than JSON API payloads, so uploads get a +// separate budget while retaining the standard timeout for ordinary calls. const UPLOAD_TIMEOUT_MS = 10 * 60 * 1000; class ForgejoClient { @@ -274,7 +276,12 @@ class ForgejoClient { async uploadReleaseAsset(owner, repo, releaseId, filePath, name) { const form = new FormData(); - const file = await fs.openAsBlob(filePath); + let file; + try { + file = await fs.openAsBlob(filePath); + } catch (err) { + throw new Error(`Could not read asset file ${filePath}: ${err.message}`); + } form.append('attachment', file, name); const query = new URLSearchParams({ name }); return this.uploadRequest( diff --git a/src/cli.js b/src/cli.js index 67300c6..9c8cfbd 100755 --- a/src/cli.js +++ b/src/cli.js @@ -97,7 +97,10 @@ function collectOption(value, previous) { return previous.concat(value); } -function validateAssetOptions(options) { +function validateAssetOptions(options, { requireAsset = false } = {}) { + if (requireAsset && options.asset.length === 0) { + throw new Error('At least one --asset is required.'); + } if (options.assetName && options.asset.length !== 1) { throw new Error('--asset-name requires exactly one --asset.'); } @@ -1150,7 +1153,7 @@ release .option('--asset-name ', 'override the uploaded filename (exactly one asset)') .action(async (options) => { try { - validateAssetOptions(options); + validateAssetOptions(options, { requireAsset: true }); const config = loadConfig(); const client = ForgejoClient.fromConfig(config); const releaseResult = await client.getReleaseByTag(options.owner, options.repo, options.tag); diff --git a/test/api.test.js b/test/api.test.js index 56cd7c4..aa0e461 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -305,6 +305,20 @@ test('uploadReleaseAsset streams multipart data without forcing a JSON content t } }); +test('uploadReleaseAsset identifies a missing local asset path', async () => { + const assetPath = path.join(os.tmpdir(), `stoke-missing-asset-${process.pid}.bin`); + const client = new ForgejoClient('https://forge.test', 'tok'); + + await assert.rejects( + client.uploadReleaseAsset('owner', 'repo', 42, assetPath, 'artifact.bin'), + (err) => { + assert.match(err.message, /Could not read asset file/); + assert.match(err.message, new RegExp(assetPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); + return true; + }, + ); +}); + test('uploadReleaseAsset uses the upload timeout instead of the 30 second JSON timeout', async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-upload-timeout-')); const assetPath = path.join(dir, 'large.bin'); diff --git a/test/cli.test.js b/test/cli.test.js index 3589196..817cc06 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -85,6 +85,15 @@ test('release create rejects one asset name for multiple assets before reading c assert.doesNotMatch(res.stderr, /Not authenticated/); }); +test('release upload rejects zero assets before reading config', () => { + const res = run([ + 'release', 'upload', '-o', 'o', '-r', 'r', '--tag', 'v1', + ], { STOKE_CONFIG_FILE: path.join(os.tmpdir(), `stoke-none-${process.pid}-release.json`) }); + assert.equal(res.status, 1); + assert.match(res.stderr, /at least one --asset is required/i); + assert.doesNotMatch(res.stderr, /Not authenticated/); +}); + test('release create prints the id and uploads every asset as multipart data', async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-release-create-')); const cfg = path.join(dir, 'config.json');