feat(KAN-58): add macOS actions and Hetzner release pipeline
CI/CD / preconditions (pull_request) Successful in 2s
Native JavaScript checks / Native launcher and release tests (pull_request) Successful in 57s
CI/CD / test_and_check_app (pull_request) Successful in 6m33s
CI/CD / Publish immutable image (pull_request) Skipped
macOS checks / Native Rust tests and unsigned build (pull_request) Canceled after 0s

This commit is contained in:
2026-09-27 08:08:57 +00:00
parent 2f99b8e65c
commit fcba7673c2
8 changed files with 1245 additions and 43 deletions
+193
View File
@@ -0,0 +1,193 @@
import assert from 'node:assert/strict';
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import {
packageNotarizedApp,
releaseMetadata,
verifyNotarizedApp,
} from '../macos-ci.mjs';
function fixture(t) {
const directory = mkdtempSync(join(tmpdir(), 'kanbangit-ci-test-'));
t.after(() => rmSync(directory, { recursive: true, force: true }));
mkdirSync(join(directory, 'src-tauri'));
const config = {
version: '1.2.3',
productName: 'KanbanGit',
identifier: 'de.kanbangit.desktop',
};
writeFileSync(
join(directory, 'src-tauri/tauri.conf.json'),
JSON.stringify(config),
);
writeFileSync(
join(directory, 'package.json'),
JSON.stringify({ version: '1.2.3' }),
);
writeFileSync(
join(directory, 'src-tauri/Cargo.toml'),
'[package]\nname = "kanbangit-native"\nversion = "1.2.3"\n[dependencies]\nother = "2"\n',
);
const env = {
RELEASE_TAG: 'macos-v1.2.3',
APPLE_SIGNING_IDENTITY:
'Developer ID Application: Example (ZZX2KZGTHU)',
};
const calls = [];
const run = (command, args) => {
calls.push({ command, args });
if (command === 'codesign' && args.includes('--display')) {
return 'CodeDirectory flags=0x10000(runtime)\nCDHash=abcdef123456';
}
if (command === 'plutil') {
return '1.2.3';
}
if (command === 'lipo') {
return 'arm64';
}
if (command === 'ditto' && args.includes('-c')) {
writeFileSync(args.at(-1), 'notarized archive');
}
return '';
};
return { directory, env, config, run, calls };
}
test('release metadata binds tag, all app versions, identity and architecture', (t) => {
const f = fixture(t);
assert.deepEqual(releaseMetadata(f.env, f.directory), {
version: '1.2.3',
target: 'aarch64-apple-darwin',
arch: 'arm64',
filename: 'KanbanGit-1.2.3-aarch64-apple-darwin.zip',
});
assert.equal(
releaseMetadata(
{ ...f.env, MACOS_RELEASE_TARGET: 'x86_64-apple-darwin' },
f.directory,
).arch,
'x86_64',
);
});
for (const env of [
{ RELEASE_TAG: 'v1.2.3' },
{ RELEASE_TAG: 'macos-v1.2.4' },
{ RELEASE_TAG: 'macos-v1.2.3; echo bad' },
{ MACOS_RELEASE_TARGET: 'x86_64-unknown-linux-gnu' },
{ APPLE_SIGNING_IDENTITY: '-' },
{ APPLE_SIGNING_IDENTITY: 'Apple Development: Example (ZZX2KZGTHU)' },
{
APPLE_SIGNING_IDENTITY:
'Developer ID Application: Example (OLDTEAM123)',
},
]) {
test(`rejects invalid release inputs ${JSON.stringify(env)}`, (t) => {
const f = fixture(t);
assert.throws(() => releaseMetadata({ ...f.env, ...env }, f.directory));
});
}
for (const [file, contents] of [
['package.json', '{"version":"1.2.4"}'],
['src-tauri/Cargo.toml', '[package]\nversion = "1.2.4"'],
['src-tauri/tauri.conf.json', '{"version":"01.2.3"}'],
[
'src-tauri/tauri.conf.json',
'{"version":"1.2.3","identifier":"other","productName":"KanbanGit"}',
],
]) {
test(`rejects inconsistent metadata in ${file}: ${contents}`, (t) => {
const f = fixture(t);
writeFileSync(join(f.directory, file), contents);
assert.throws(() => releaseMetadata(f.env, f.directory));
});
}
test('packages only a verified app and checks the extracted download again', (t) => {
const f = fixture(t);
const output = packageNotarizedApp(f);
assert.equal(
readFileSync(join(output.directory, output.filename), 'utf8'),
'notarized archive',
);
assert.equal(
readFileSync(
join(output.directory, `${output.filename}.sha256`),
'utf8',
),
`${output.checksum} ${output.filename}\n`,
);
assert.equal(f.calls.filter((call) => call.command === 'spctl').length, 2);
assert.equal(f.calls.filter((call) => call.command === 'xcrun').length, 2);
assert.match(
f.calls.findLast((call) => call.command === 'spctl').args.at(-1),
/extracted\/KanbanGit\.app$/,
);
assert.throws(() => packageNotarizedApp(f), /already exists/);
});
for (const failure of ['codesign', 'lipo', 'xcrun', 'spctl', 'ditto']) {
test(`does not publish local output when ${failure} verification fails`, (t) => {
const f = fixture(t);
const run = f.run;
f.run = (command, args) => {
if (command === failure) {
throw new Error('verification failed');
}
return run(command, args);
};
assert.throws(() => packageNotarizedApp(f), /verification failed/);
assert.equal(
existsSync(
join(
f.directory,
'src-tauri/target/macos-release/1.2.3-aarch64-apple-darwin',
),
),
false,
);
});
}
test('rejects an app built for a different architecture', (t) => {
const f = fixture(t);
const metadata = releaseMetadata(f.env, f.directory);
assert.throws(
() =>
verifyNotarizedApp('KanbanGit.app', metadata, (command, args) =>
command === 'lipo' ? 'x86_64' : f.run(command, args),
),
/architecture/,
);
});
test('failed validation of the extracted app discards staged download', (t) => {
const f = fixture(t);
const run = f.run;
f.run = (command, args) => {
if (command === 'spctl' && args.at(-1).includes('extracted/')) {
throw new Error('roundtrip invalid');
}
return run(command, args);
};
assert.throws(() => packageNotarizedApp(f), /roundtrip invalid/);
assert.equal(
existsSync(
join(
f.directory,
'src-tauri/target/macos-release/1.2.3-aarch64-apple-darwin',
),
),
false,
);
});