Files
Kaloyan DanchevandClaude Opus 4.6 cbea5a322f
CI / Identify a merged version PR (push) Canceled after 0s
CI / Rust checks, rendering, and native package (push) Canceled after 0s
CI / Publish release and Homebrew feed (push) Canceled after 0s
Release PR / Maintain Version Packages PR (push) Canceled after 0s
Merge Luma launcher fork into monorepo
Move trampoline files (boot-hook, watcher, overlay) into trampoline/
subdirectory. Add upstream Luma v2.3.1 source at repo root with our
additions:

- Page::Ha (4th nav tab) with placeholder empty state
- HA tab config schema and default entity mapping (ha-home/)
- Nav keys renamed: "home" → "start", new "ha" tab between Apps and Settings
- All state.rs/view.rs/main.rs/tests.rs updated for 4-tab navigation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-24 11:52:55 +03:00

221 lines
8.7 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import test from 'node:test';
import { syncVersions } from '../tools/sync-version.mjs';
import { isReleaseMerge, isVersionIncrease } from '../tools/release-gate.mjs';
const root = path.resolve(import.meta.dirname, '..');
const metadata = ['appinfo.json', 'Cargo.toml', 'Cargo.lock', 'readme.md'];
async function fixture(t, version = '2.0.4') {
const scratch = path.join(root, '.build');
await fs.mkdir(scratch, { recursive: true });
const directory = await fs.mkdtemp(path.join(scratch, 'release-test-'));
t.after(() => fs.rm(directory, { recursive: true, force: true }));
const project = JSON.parse(await fs.readFile(path.join(root, 'package.json'), 'utf8'));
project.version = '1.2.3';
await fs.writeFile(path.join(directory, 'package.json'), JSON.stringify(project));
await Promise.all(
metadata.map((filename) =>
fs.copyFile(path.join(root, filename), path.join(directory, filename)),
),
);
await syncVersions(directory);
project.version = version;
await fs.writeFile(path.join(directory, 'package.json'), JSON.stringify(project));
return directory;
}
const readMetadata = (directory) =>
Promise.all(metadata.map((filename) => fs.readFile(path.join(directory, filename), 'utf8')));
test('version sync changes only release versions and is idempotent', async (t) => {
const directory = await fixture(t, '3.1.0');
const before = await readMetadata(directory);
await assert.rejects(syncVersions(directory, { check: true }), /out of sync/);
assert.deepEqual(await readMetadata(directory), before, 'check mode must not write files');
assert.deepEqual((await syncVersions(directory)).changed, metadata);
const after = await readMetadata(directory);
const previous = JSON.parse(before[0]).version;
assert.equal(JSON.parse(after[0]).version, '3.1.0');
assert.equal(after[0], before[0].replace(`"version": "${previous}"`, '"version": "3.1.0"'));
assert.equal(after[1], before[1].replace(`version = "${previous}"`, 'version = "3.1.0"'));
assert.equal(
after[2],
before[2].replace(`name = "luma"\nversion = "${previous}"`, 'name = "luma"\nversion = "3.1.0"'),
'dependency versions and helper crates must be preserved',
);
assert.equal(
after[3],
before[3].replaceAll(`com.ecklf.luma_${previous}_arm.ipk`, 'com.ecklf.luma_3.1.0_arm.ipk'),
);
assert.deepEqual((await syncVersions(directory, { check: true })).changed, []);
assert.deepEqual((await syncVersions(directory)).changed, []);
});
test('invalid release versions and missing packages fail before writing metadata', async (t) => {
for (const version of ['3.0.0-beta.1', '3.0', '03.0.0']) {
const directory = await fixture(t, version);
const before = await readMetadata(directory);
await assert.rejects(syncVersions(directory), /numeric major.minor.patch/);
assert.deepEqual(await readMetadata(directory), before);
}
const directory = await fixture(t);
const lockfile = path.join(directory, 'Cargo.lock');
await fs.writeFile(
lockfile,
(await fs.readFile(lockfile, 'utf8')).replace('name = "luma"', 'name = "other"'),
);
const before = await readMetadata(directory);
await assert.rejects(syncVersions(directory), /one luma package in Cargo.lock/);
assert.deepEqual(await readMetadata(directory), before);
});
test('the real release command combines changesets and synchronizes every file', async (t) => {
const directory = await fixture(t, '1.2.3');
await syncVersions(directory);
await fs.mkdir(path.join(directory, 'tools'));
await fs.mkdir(path.join(directory, '.changeset'));
for (const filename of [
'tools/sync-version.mjs',
'.changeset/config.json',
'pnpm-workspace.yaml',
'.oxfmtrc.json',
]) {
await fs.copyFile(path.join(root, filename), path.join(directory, filename));
}
await fs.symlink(path.join(root, 'node_modules'), path.join(directory, 'node_modules'), 'dir');
await fs.writeFile(
path.join(directory, '.changeset/fix.md'),
'---\n"lg-luma": patch\n---\n\nFix release behavior.\n',
);
await fs.writeFile(
path.join(directory, '.changeset/feature.md'),
'---\n"lg-luma": minor\n---\n\nAdd a release feature.\n',
);
const result = spawnSync('pnpm', ['version:release'], {
cwd: directory,
encoding: 'utf8',
timeout: 30000,
env: { ...process.env, CI: 'true' },
});
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
assert.equal((await syncVersions(directory, { check: true })).version, '1.3.0');
const changelog = await fs.readFile(path.join(directory, 'CHANGELOG.md'), 'utf8');
assert.match(changelog, /1\.3\.0/);
assert.match(changelog, /Fix release behavior/);
assert.match(changelog, /Add a release feature/);
assert.deepEqual(await fs.readdir(path.join(directory, '.changeset')), ['config.json']);
});
function gateFixture({ pull = {}, previous = '2.0.3', current = '2.0.4', event = {} } = {}) {
const context = {
eventName: 'push',
ref: 'refs/heads/main',
repo: { owner: 'ecklf', repo: 'lg-luma' },
sha: 'merged-commit',
payload: { before: 'previous-commit', forced: false, deleted: false },
...event,
};
const releasePull = {
merged_at: '2026-10-23T12:00:00Z',
merge_commit_sha: context.sha,
base: { ref: 'main', repo: { full_name: 'ecklf/lg-luma' } },
head: { ref: 'changeset-release/main', repo: { full_name: 'ecklf/lg-luma' } },
...pull,
};
const reads = [];
const github = {
paginate: async (_method, parameters) => {
assert.equal(parameters.commit_sha, context.sha);
return [releasePull];
},
rest: {
repos: {
listPullRequestsAssociatedWithCommit() {},
async getContent({ ref, path: filename }) {
assert.equal(filename, 'package.json');
reads.push(ref);
const version = ref === context.sha ? current : previous;
return {
data: {
encoding: 'base64',
content: Buffer.from(JSON.stringify({ version })).toString('base64'),
},
};
},
},
},
};
return { github, context, reads };
}
test('a merged same-repository version PR with an increased version can publish', async () => {
const fixture = gateFixture();
assert.equal(await isReleaseMerge(fixture), true);
assert.deepEqual(fixture.reads, ['previous-commit', 'merged-commit']);
});
test('ordinary merges, unmerged PRs, forks and manual runs cannot publish', async (t) => {
const cases = [
['feature PR', { pull: { head: { ref: 'feature', repo: { full_name: 'ecklf/lg-luma' } } } }],
['unmerged version PR', { pull: { merged_at: null } }],
['different merge commit', { pull: { merge_commit_sha: 'other-commit' } }],
[
'fork version branch',
{ pull: { head: { ref: 'changeset-release/main', repo: { full_name: 'other/lg-luma' } } } },
],
['deleted head repository', { pull: { head: { ref: 'changeset-release/main', repo: null } } }],
[
'different base',
{ pull: { base: { ref: 'develop', repo: { full_name: 'ecklf/lg-luma' } } } },
],
['manual CI run', { event: { eventName: 'workflow_dispatch' } }],
['PR checks', { event: { eventName: 'pull_request' } }],
['other branch', { event: { ref: 'refs/heads/changeset-release/main' } }],
['force push', { event: { payload: { before: 'previous-commit', forced: true } } }],
[
'initial push',
{ event: { payload: { before: '0000000000000000000000000000000000000000' } } },
],
];
for (const [name, options] of cases) {
await t.test(name, async () => {
const fixture = gateFixture(options);
assert.equal(await isReleaseMerge(fixture), false);
assert.deepEqual(fixture.reads, []);
});
}
});
test('publishing requires a numeric version increase', async () => {
for (const current of ['2.0.3', '2.0.2', '1.99.99']) {
assert.equal(await isReleaseMerge(gateFixture({ current })), false);
}
for (const [previous, current] of [
['2.0.3', '2.0.4'],
['2.9.9', '2.10.0'],
['2.99.99', '3.0.0'],
]) {
assert.equal(isVersionIncrease(previous, current), true);
}
await assert.rejects(
isReleaseMerge(gateFixture({ current: '3.0.0-beta.1' })),
/Invalid numeric release version/,
);
});
test('missing PRs and failed GitHub lookups never permit publication', async () => {
const fixture = gateFixture();
fixture.github.paginate = async () => [];
assert.equal(await isReleaseMerge(fixture), false);
assert.deepEqual(fixture.reads, []);
fixture.github.paginate = async () => {
throw new Error('GitHub unavailable');
};
await assert.rejects(isReleaseMerge(fixture), /GitHub unavailable/);
});