feat(KAN-65): add signed macOS updates and DMG distribution
CI/CD / preconditions (pull_request) Successful in 2s
Native JavaScript checks / Native launcher and release tests (pull_request) Successful in 1m0s
CI/CD / test_and_check_app (pull_request) Successful in 7m2s
CI/CD / Publish immutable image (pull_request) Skipped
macOS checks / Native Rust tests and unsigned build (pull_request) Successful in 1m35s

This commit is contained in:
2026-09-27 21:40:23 +02:00
parent 3e5c316b8f
commit 20f9314659
28 changed files with 2454 additions and 130 deletions
+125 -36
View File
@@ -6,6 +6,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { publishMacos, storageConfiguration } from '../publish-macos.mjs';
import { signedFixture } from './updater-fixture.mjs';
function fixture(t) {
const directory = mkdtempSync(join(tmpdir(), 'kanbangit-upload-test-'));
@@ -13,7 +14,8 @@ function fixture(t) {
const metadata = {
version: '1.2.3',
target: 'aarch64-apple-darwin',
filename: 'KanbanGit-1.2.3-aarch64-apple-darwin.zip',
filename: 'KanbanGit-1.2.3-aarch64-apple-darwin.dmg',
updaterFilename: 'KanbanGit-1.2.3-aarch64-apple-darwin.app.tar.gz',
};
const archive = Buffer.from('verified release');
const checksum = createHash('sha256').update(archive).digest('hex');
@@ -22,6 +24,17 @@ function fixture(t) {
join(directory, `${metadata.filename}.sha256`),
`${checksum} ${metadata.filename}\n`,
);
const updater = Buffer.from('signed update');
const signing = signedFixture(updater);
writeFileSync(join(directory, metadata.updaterFilename), updater);
writeFileSync(
join(directory, `${metadata.updaterFilename}.sig`),
signing.signature,
);
writeFileSync(
join(directory, `${metadata.updaterFilename}.sha256`),
`${createHash('sha256').update(updater).digest('hex')} ${metadata.updaterFilename}\n`,
);
const env = {
S3_BUCKET: 'release-bucket',
S3_REGION: 'nbg1',
@@ -42,18 +55,23 @@ function fixture(t) {
return '';
}
if (operation === 'put-object') {
assert.equal(value('--if-none-match'), '*');
if (!args.includes('--if-match')) {
assert.equal(value('--if-none-match'), '*');
}
if (objects.has(key)) {
throw new Error(
'aws failed: (PreconditionFailed) object exists',
);
if (!args.includes('--if-match')) {
throw new Error(
'aws failed: (PreconditionFailed) object exists',
);
}
assert.equal(value('--if-match'), '"previous-etag"');
}
objects.set(key, readFileSync(value('--body')));
return '{}';
}
if (operation === 'get-object') {
writeFileSync(args.at(-1), objects.get(key));
return '{}';
return JSON.stringify({ ETag: '"previous-etag"' });
}
throw new Error(`Unexpected S3 operation: ${operation}`);
};
@@ -72,6 +90,9 @@ function fixture(t) {
};
return {
directory,
publicKey: signing.publicKey,
feedBase:
'https://release-bucket.nbg1.your-objectstorage.com/releases/macos',
metadata,
env,
objects,
@@ -89,28 +110,32 @@ test('uploads immutable files and writes completion record after public verifica
url,
`https://release-bucket.nbg1.your-objectstorage.com/releases/macos/aarch64-apple-darwin/1.2.3/${f.metadata.filename}`,
);
assert.equal(
f.events.filter((event) => event.operation === 'put-object').length,
8,
);
assert.equal(f.objects.size, 8);
assert(f.events.at(-1).key.endsWith('/latest.json'));
assert.equal(
f.events.at(-1).key,
'releases/macos/aarch64-apple-darwin/latest.json',
);
assert.deepEqual(
f.events.map((event) => event.operation),
[
'get-bucket-versioning',
'put-object',
'public-get',
'put-object',
'public-get',
'put-object',
'public-get',
],
JSON.parse(
f.objects.get(
'releases/macos/aarch64-apple-darwin/1.2.3/release.json',
),
),
{
version: '1.2.3',
target: f.metadata.target,
url,
sha256: createHash('sha256')
.update('verified release')
.digest('hex'),
checksumUrl: `${url}.sha256`,
},
);
assert.equal(f.objects.size, 3);
assert(f.events.at(-1).key.endsWith('/release.json'));
assert(![...f.objects.keys()].some((key) => key.includes('latest')));
assert.deepEqual(JSON.parse(f.objects.get(f.events.at(-1).key)), {
version: '1.2.3',
target: f.metadata.target,
url,
sha256: createHash('sha256').update('verified release').digest('hex'),
checksumUrl: `${url}.sha256`,
});
});
for (const response of ['', ' \n\t', '{}']) {
@@ -122,9 +147,9 @@ for (const response of ['', ' \n\t', '{}']) {
return args.includes('get-bucket-versioning') ? response : result;
};
await publishMacos(f);
assert.equal(f.objects.size, 3);
assert.equal(f.objects.size, 8);
assert.equal(f.events.at(-1).operation, 'public-get');
assert(f.events.at(-1).key.endsWith('/release.json'));
assert(f.events.at(-1).key.endsWith('/latest.json'));
});
}
@@ -148,7 +173,7 @@ test('retry accepts byte-identical objects without replacing them', async (t) =>
assert.deepEqual([...f.objects.entries()], originals);
assert.equal(
f.events.filter((event) => event.operation === 'get-object').length,
3,
8,
);
});
@@ -227,10 +252,10 @@ test('a partial upload can resume with the same files', async (t) => {
return run(command, args);
};
await assert.rejects(publishMacos(f), /interrupted/);
assert.equal(f.objects.size, 1);
assert.equal(f.objects.size, 3);
f.run = run;
await publishMacos(f);
assert.equal(f.objects.size, 3);
assert.equal(f.objects.size, 8);
});
for (const status of [403, 404, 408, 429, 500, 502, 503, 504, 'network']) {
@@ -257,8 +282,8 @@ for (const status of [403, 404, 408, 429, 500, 502, 503, 504, 'network']) {
await publishMacos(f);
assert.deepEqual(waits, [2000, 2000]);
assert.equal(cancelled, status === 'network' ? 0 : 2);
assert.equal(f.objects.size, 3);
assert(f.events.at(-1).key.endsWith('/release.json'));
assert.equal(f.objects.size, 8);
assert(f.events.at(-1).key.endsWith('/latest.json'));
});
}
@@ -293,15 +318,15 @@ test('checksum access retries finish before publishing the completion record', a
const fetchPublic = f.fetchPublic;
let failures = 0;
f.fetchPublic = async (url, options) => {
if (url.endsWith('.sha256') && ++failures <= 2) {
assert.equal(f.objects.size, 2);
if (url.endsWith(`${f.metadata.filename}.sha256`) && ++failures <= 2) {
assert.equal(f.objects.size, 5);
return { ok: false, status: 403 };
}
return fetchPublic(url, options);
};
await publishMacos(f);
assert.equal(failures, 3);
assert.equal(f.objects.size, 3);
assert.equal(f.objects.size, 8);
});
for (const overrides of [
@@ -338,3 +363,67 @@ test('derives Hetzner endpoints and supports an explicit public proxy URL', () =
assert.equal(config.publicBase, 'https://downloads.example.com');
}
});
test('refuses a destination that differs from the built-in update source', async (t) => {
const f = fixture(t);
f.feedBase = 'https://another.example/releases/macos';
await assert.rejects(publishMacos(f), /does not match/);
assert.equal(f.objects.size, 0);
});
test('rejects a corrupt updater archive before publishing any object', async (t) => {
const f = fixture(t);
writeFileSync(join(f.directory, f.metadata.updaterFilename), 'tampered');
await assert.rejects(publishMacos(f), /signature/);
assert.equal(f.objects.size, 0);
});
test('atomically promotes a newer feed only after all immutable objects are public', async (t) => {
const f = fixture(t);
const latestKey = `releases/macos/${f.metadata.target}/latest.json`;
f.objects.set(latestKey, Buffer.from(JSON.stringify({ version: '1.0.0' })));
await publishMacos(f);
assert.equal(JSON.parse(f.objects.get(latestKey)).version, '1.2.3');
const immutableVerified = f.events.findIndex(
(event) =>
event.operation === 'public-get' &&
event.key.endsWith('/update.json'),
);
const latestPut = f.events.findIndex(
(event) => event.operation === 'put-object' && event.key === latestKey,
);
assert(latestPut > immutableVerified);
});
for (const previous of [
{ version: '2.0.0' },
{ version: '1.2.3', notes: 'different' },
]) {
test(`does not replace conflicting update feed ${previous.version}`, async (t) => {
const f = fixture(t);
const key = `releases/macos/${f.metadata.target}/latest.json`;
const original = Buffer.from(JSON.stringify(previous));
f.objects.set(key, original);
await assert.rejects(publishMacos(f), /downgrade|differs/);
assert.deepEqual(f.objects.get(key), original);
});
}
test('concurrent feed replacement fails without overwriting the winner', async (t) => {
const f = fixture(t);
const key = `releases/macos/${f.metadata.target}/latest.json`;
f.objects.set(key, Buffer.from(JSON.stringify({ version: '1.0.0' })));
const run = f.run;
f.run = (command, args) => {
if (args.includes('--if-match')) {
f.objects.set(
key,
Buffer.from(JSON.stringify({ version: '2.0.0' })),
);
throw new Error('(PreconditionFailed) concurrent publication');
}
return run(command, args);
};
await assert.rejects(publishMacos(f), /concurrent publication/);
assert.equal(JSON.parse(f.objects.get(key)).version, '2.0.0');
});