also looking at this
feat: add Linux and Windows named-window routing
#325
29 files
+2250
-200
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -65,6 +65,20 @@ test('npm publication follows artifact validation, and macOS downloads run on bo | ||
| 65 | 65 | expect(steps(release.jobs['attach-release-sbom'])).toContain('cmp "sbom/$document" "$VERIFY_DIR/$document"') |
| 66 | 66 | }) |
| 67 | 67 | |
| 68 | test('Windows release stages the pinned WebView2 loader before scanning and publishing', () => { | |
| 69 | const native = release.jobs.pantry.steps! | |
| 70 | const staged = native.findIndex(step => step.name === 'Stage Windows WebView2 loader') | |
| 71 | const scan = native.findIndex(step => step.name === 'Scan binaries prepared for publication') | |
| 72 | const publish = native.findIndex(step => step.name === 'Publish & Release') | |
| 73 | expect(staged).toBeGreaterThan(native.findIndex(step => step.name === 'Cross-compile additional targets (Linux)')) | |
| 74 | expect(scan).toBeGreaterThan(staged) | |
| 75 | expect(publish).toBeGreaterThan(scan) | |
| 76 | const command = native[staged]?.run ?? '' | |
| 77 | expect(command).toContain('1.0.4191.47') | |
| 78 | expect(command).toContain('f492bbf547d0da329553b6727435b677579b1e9f91cc9e4a1ad029366d5f23d0') | |
| 79 | expect(command).toContain('zig-out/cross/windows-x64/WebView2Loader.dll') | |
| 80 | }) | |
| 81 | ||
| 68 | 82 | test('a registry network failure remains a visible best-effort warning', () => { |
| 69 | 83 | const command = steps(release.jobs['notify-registry']) |
| 70 | 84 | expect(command).not.toBe('') |
| @@ -70,13 +70,20 @@ test('native inventory binds exact binaries and Zig runtime version', () => { | ||
| 70 | 70 | const root = fixture() |
| 71 | 71 | binary(root, 'bin/craft', '7f454c46') |
| 72 | 72 | binary(root, 'cross/windows-x64/craft.exe', '4d5a') |
| 73 | binary(root, 'cross/windows-x64/WebView2Loader.dll', '4d5a') | |
| 73 | 74 | const inventory = nativeInventory(root, 'linux', '1.2.3', '0.17.0-dev.1963') |
| 74 | expect(inventory.components.map(component => component.name)).toEqual(['craft-linux-x64', 'craft-windows-x64', 'zig-std']) | |
| 75 | expect(inventory.components.map(component => component.name)).toEqual(['craft-linux-x64', 'craft-windows-x64', 'WebView2Loader', 'zig-std']) | |
| 75 | 76 | expect(inventory.components[0]?.hashes).toEqual([{ alg: 'SHA-256', content: createHash('sha256').update(readFileSync(join(root, 'bin/craft'))).digest('hex') }]) |
| 77 | expect(inventory.components[2]?.version).toBe('1.0.4191.47') | |
| 78 | expect(inventory.components[2]?.type).toBe('library') | |
| 79 | expect(inventory.dependencies).toContainEqual({ ref: 'urn:craft:binary:craft-windows-x64', dependsOn: ['urn:craft:zig-std', 'urn:craft:binary:WebView2Loader'] }) | |
| 76 | 80 | writeFileSync(join(root, 'cross/windows-x64/craft.exe'), 'not a binary') |
| 77 | 81 | expect(() => nativeInventory(root, 'linux', '1.2.3', '0.17.0-dev.1963')).toThrow('missing or empty') |
| 78 | 82 | binary(root, 'cross/windows-x64/craft.exe', '7f454c46') |
| 79 | 83 | expect(() => nativeInventory(root, 'linux', '1.2.3', '0.17.0-dev.1963')).toThrow('unexpected executable format') |
| 84 | binary(root, 'cross/windows-x64/craft.exe', '4d5a') | |
| 85 | writeFileSync(join(root, 'cross/windows-x64/WebView2Loader.dll'), 'not a binary') | |
| 86 | expect(() => nativeInventory(root, 'linux', '1.2.3', '0.17.0-dev.1963')).toThrow('WebView2Loader: missing or empty') | |
| 80 | 87 | }) |
| 81 | 88 | |
| 82 | 89 | test('publisher uses only hash-verified archives and skips an existing version', () => { |
| @@ -7,6 +7,7 @@ import { enforceScanPolicy, scanSbom } from './scan-sbom' | ||
| 7 | 7 | type Archive = { name: string, version: string, file: string, sha256: string } |
| 8 | 8 | type Component = { name: string, version?: string, 'bom-ref'?: string, [key: string]: unknown } |
| 9 | 9 | type Inventory = { bomFormat: string, components: Component[], [key: string]: unknown } |
| 10 | type NativeArtifact = { path: string, name: string, magic: string[], type: 'application' | 'library', version?: string } | |
| 10 | 11 | |
| 11 | 12 | const root = resolve(import.meta.dir, '..') |
| 12 | 13 | const sha256 = (path: string) => createHash('sha256').update(readFileSync(path)).digest('hex') |
| @@ -108,18 +109,21 @@ function scanNpm(directory: string, output: string, syft: string, grype: string) | ||
| 108 | 109 | finally { rmSync(consumer, { recursive: true, force: true }) } |
| 109 | 110 | } |
| 110 | 111 | |
| 112 | function nativeArtifacts(platform: 'macos' | 'linux'): NativeArtifact[] { | |
| 113 | return platform === 'macos' | |
| 114 | ? [{ path: 'bin/craft', name: 'craft-darwin-arm64', magic: ['cffaedfe', 'feedfacf'], type: 'application' }, { path: 'cross/darwin-x64/craft', name: 'craft-darwin-x64', magic: ['cffaedfe', 'feedfacf'], type: 'application' }] | |
| 115 | : [{ path: 'bin/craft', name: 'craft-linux-x64', magic: ['7f454c46'], type: 'application' }, { path: 'cross/windows-x64/craft.exe', name: 'craft-windows-x64', magic: ['4d5a'], type: 'application' }, { path: 'cross/windows-x64/WebView2Loader.dll', name: 'WebView2Loader', magic: ['4d5a'], type: 'library', version: '1.0.4191.47' }] | |
| 116 | } | |
| 117 | ||
| 111 | 118 | export function nativeInventory(binaryRoot: string, platform: 'macos' | 'linux', version: string, zigVersion: string, discovered: Component[][] = []): Inventory { |
| 112 | const entries = platform === 'macos' | |
| 113 | ? [{ path: 'bin/craft', name: 'craft-darwin-arm64', magic: ['cffaedfe', 'feedfacf'] }, { path: 'cross/darwin-x64/craft', name: 'craft-darwin-x64', magic: ['cffaedfe', 'feedfacf'] }] | |
| 114 | : [{ path: 'bin/craft', name: 'craft-linux-x64', magic: ['7f454c46'] }, { path: 'cross/windows-x64/craft.exe', name: 'craft-windows-x64', magic: ['4d5a'] }] | |
| 115 | const binaries: Component[] = entries.map(({ path, name, magic }) => { | |
| 119 | const artifacts: Component[] = nativeArtifacts(platform).map(({ path, name, magic, type, version: artifactVersion }) => { | |
| 116 | 120 | const absolute = join(binaryRoot, path) |
| 117 | 121 | if (!existsSync(absolute) || !statSync(absolute).isFile() || statSync(absolute).size < 1024) |
| 118 | 122 | throw new Error(`${name}: missing or empty release binary`) |
| 119 | 123 | const bytes = readFileSync(absolute) |
| 120 | 124 | if (!magic.some(prefix => bytes.subarray(0, prefix.length / 2).toString('hex') === prefix)) |
| 121 | 125 | throw new Error(`${name}: unexpected executable format`) |
| 122 | return { 'bom-ref': `urn:craft:binary:${name}`, type: 'application', name, version, | |
| 126 | return { 'bom-ref': `urn:craft:binary:${name}`, type, name, version: artifactVersion ?? version, | |
| 123 | 127 | hashes: [{ alg: 'SHA-256', content: createHash('sha256').update(bytes).digest('hex') }], |
| 124 | 128 | properties: [{ name: 'craft:artifact-path', value: path }], |
| 125 | 129 | } |
| @@ -128,8 +132,11 @@ export function nativeInventory(binaryRoot: string, platform: 'macos' | 'linux', | ||
| 128 | 132 | return { |
| 129 | 133 | bomFormat: 'CycloneDX', specVersion: '1.6', version: 1, |
| 130 | 134 | metadata: { component: { type: 'application', name: 'craft-native-release', version } }, |
| 131 | components: [...binaries, zig, ...discovered.flat()], | |
| 132 | dependencies: binaries.map(binary => ({ ref: binary['bom-ref'], dependsOn: [zig['bom-ref']] })), | |
| 135 | components: [...artifacts, zig, ...discovered.flat()], | |
| 136 | dependencies: artifacts.filter(artifact => artifact.type === 'application').map(artifact => ({ | |
| 137 | ref: artifact['bom-ref'], | |
| 138 | dependsOn: artifact.name === 'craft-windows-x64' ? [zig['bom-ref'], 'urn:craft:binary:WebView2Loader'] : [zig['bom-ref']], | |
| 139 | })), | |
| 133 | 140 | } |
| 134 | 141 | } |
| 135 | 142 | |
| @@ -137,7 +144,7 @@ function scanNative(binaryRoot: string, platform: 'macos' | 'linux', output: str | ||
| 137 | 144 | mkdirSync(output, { recursive: true }) |
| 138 | 145 | const { version } = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { version: string } |
| 139 | 146 | const zigVersion = Bun.JSONC.parse(readFileSync(join(root, 'pantry.jsonc'), 'utf8')).dependencies['ziglang.org'] as string |
| 140 | const paths = platform === 'macos' ? ['bin/craft', 'cross/darwin-x64/craft'] : ['bin/craft', 'cross/windows-x64/craft.exe'] | |
| 147 | const paths = nativeArtifacts(platform).map(artifact => artifact.path) | |
| 141 | 148 | const discovered = paths.map((path, index) => { |
| 142 | 149 | const report = join(output, `native-${index}.cyclonedx.json`) |
| 143 | 150 | run([syft, join(binaryRoot, path), '-o', `cyclonedx-json=${report}`]) |
| @@ -152,7 +159,7 @@ function scanNative(binaryRoot: string, platform: 'macos' | 'linux', output: str | ||
| 152 | 159 | const summary = scanSbom(sbom, join(output, 'grype'), grype) |
| 153 | 160 | enforceScanPolicy(summary.counts, true) |
| 154 | 161 | const after = nativeInventory(binaryRoot, platform, version, zigVersion) |
| 155 | for (const [index, binary] of after.components.slice(0, 2).entries()) { | |
| 162 | for (const [index, binary] of after.components.slice(0, paths.length).entries()) { | |
| 156 | 163 | if (JSON.stringify(binary.hashes) !== JSON.stringify(inventory.components[index]!.hashes)) |
| 157 | 164 | throw new Error(`${binary.name}: release binary changed after scanning`) |
| 158 | 165 | } |