diff --git a/packages/targets/desktop-linux/src/index.test.ts b/packages/targets/desktop-linux/src/index.test.ts index eca096d2..a7012083 100644 --- a/packages/targets/desktop-linux/src/index.test.ts +++ b/packages/targets/desktop-linux/src/index.test.ts @@ -1,4 +1,4 @@ -import { contractTestTarget, fakeBuildContext, smokeTest } from '@profullstack/sh1pt-core/testing'; +import { contractTestTarget, fakeBuildContext, fakeShipContext, smokeTest } from '@profullstack/sh1pt-core/testing'; import { mkdtemp, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -48,4 +48,28 @@ describe('Linux desktop target planning', () => { direct: { host: 'github-releases', project: 'acme/app' }, }); }); + + it('rejects invalid app identifiers while building', async () => { + const outDir = await mkdtemp(join(tmpdir(), 'sh1pt-linux-')); + tempDirs.push(outDir); + + await expect(adapter.build(fakeBuildContext({ + outDir, + version: '1.2.3', + channel: 'stable', + }) as any, { + ...sampleConfig, + appId: '../acme', + })).rejects.toThrow('desktop-linux appId must be a valid reverse-DNS identifier'); + }); + + it('rejects invalid app identifiers while shipping', async () => { + await expect(adapter.ship(fakeShipContext({ + version: '1.2.3', + dryRun: false, + }) as any, { + ...sampleConfig, + appId: 'com.acme/app', + })).rejects.toThrow('desktop-linux appId must be a valid reverse-DNS identifier'); + }); }); diff --git a/packages/targets/desktop-linux/src/index.ts b/packages/targets/desktop-linux/src/index.ts index 9f7d1668..1ac6e642 100644 --- a/packages/targets/desktop-linux/src/index.ts +++ b/packages/targets/desktop-linux/src/index.ts @@ -16,18 +16,30 @@ interface Config { direct?: { host: 'github-releases' | 'cdn'; project?: string }; } +const APP_ID_PATTERN = /^[A-Za-z][A-Za-z0-9-]*(\.[A-Za-z][A-Za-z0-9-]*)+$/; + +function requireAppId(config: Config): string { + const appId = config.appId?.trim(); + if (!appId) throw new Error('desktop-linux requires appId'); + if (!APP_ID_PATTERN.test(appId)) { + throw new Error('desktop-linux appId must be a valid reverse-DNS identifier'); + } + return appId; +} + export default defineTarget({ id: 'desktop-linux', kind: 'desktop', label: 'Linux (AppImage / Snap / Flatpak / deb / rpm)', async build(ctx, config) { + const appId = requireAppId(config); const arches = config.architectures ?? ['x64', 'arm64']; ctx.log(`build ${config.formats.join(',')} ยท arches=${arches.join(',')}`); const artifactDir = join(ctx.outDir, 'linux'); const planPath = join(artifactDir, 'linux-package-plan.json'); await mkdir(artifactDir, { recursive: true }); await writeFile(planPath, `${JSON.stringify({ - appId: config.appId, + appId, version: ctx.version, channel: ctx.channel, formats: config.formats, @@ -41,6 +53,7 @@ export default defineTarget({ return { artifact: planPath }; }, async ship(ctx, config) { + const appId = requireAppId(config); const channels = config.formats .map((f) => { if (f === 'snap') return `Snapcraft:${config.snap?.channel ?? 'stable'}`; @@ -57,7 +70,7 @@ export default defineTarget({ // - flatpak: open PR against flathub repo (like pkg-homebrew pattern) // - appimage: upload to GitHub release or CDN + refresh update.json // - deb/rpm: aptly / createrepo + sign + push to configured repo - return { id: `${config.appId}@${ctx.version}` }; + return { id: `${appId}@${ctx.version}` }; }, async status(id) { return { state: 'live', version: id };