From f2e1bd6a0d844f0f3b7d2db2c2b7a75bc79e8828 Mon Sep 17 00:00:00 2001 From: Twyco Date: Sun, 27 Sep 2026 12:05:11 +0200 Subject: [PATCH] fix(KAN-58): accept empty unversioned bucket response --- native/publish-macos.mjs | 21 ++++++++++++++++++++- native/tests/publish-macos.test.js | 30 +++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/native/publish-macos.mjs b/native/publish-macos.mjs index 3ce5cf1..8722c24 100644 --- a/native/publish-macos.mjs +++ b/native/publish-macos.mjs @@ -94,7 +94,26 @@ export async function publishMacos({ ...args, ]); } - const versioning = JSON.parse(s3('get-bucket-versioning', [])); + const versioningOutput = s3('get-bucket-versioning', [ + '--output', + 'json', + ]).trim(); + let versioning; + try { + // AWS CLI prints nothing for a successful empty VersioningConfiguration. + versioning = JSON.parse(versioningOutput || '{}'); + } catch { + throw new Error('Invalid JSON from S3 bucket versioning check.'); + } + if ( + !versioning || + typeof versioning !== 'object' || + Array.isArray(versioning) + ) { + throw new Error( + 'Invalid S3 bucket versioning response: expected an object.', + ); + } if (versioning.Status) { throw new Error( 'Use an unversioned release bucket: Hetzner does not support conditional writes on versioned buckets.', diff --git a/native/tests/publish-macos.test.js b/native/tests/publish-macos.test.js index 2dec55a..10a0d1e 100644 --- a/native/tests/publish-macos.test.js +++ b/native/tests/publish-macos.test.js @@ -38,7 +38,8 @@ function fixture(t) { const key = value('--key'); events.push({ operation, key }); if (operation === 'get-bucket-versioning') { - return '{}'; + assert.equal(value('--output'), 'json'); + return ''; } if (operation === 'put-object') { assert.equal(value('--if-none-match'), '*'); @@ -103,6 +104,33 @@ test('uploads immutable files and writes completion record after public verifica }); }); +for (const response of ['', ' \n\t', '{}']) { + test(`unversioned bucket response ${JSON.stringify(response)} permits a verified upload`, async (t) => { + const f = fixture(t); + const run = f.run; + f.run = (command, args) => { + const result = run(command, args); + return args.includes('get-bucket-versioning') ? response : result; + }; + await publishMacos(f); + assert.equal(f.objects.size, 3); + assert.equal(f.events.at(-1).operation, 'public-get'); + assert(f.events.at(-1).key.endsWith('/release.json')); + }); +} + +for (const response of ['{', 'null', '[]', '"Enabled"', 'false']) { + test(`invalid versioning response ${response} aborts before any upload`, async (t) => { + const f = fixture(t); + f.run = (_command, args) => { + assert(args.includes('get-bucket-versioning')); + return response; + }; + await assert.rejects(publishMacos(f), /Invalid .*versioning/); + assert.equal(f.objects.size, 0); + }); +} + test('retry accepts byte-identical objects without replacing them', async (t) => { const f = fixture(t); await publishMacos(f); -- 2.54.0