Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/desktop-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,13 @@ jobs:
# electron-builder notarizes and staples the .app but never the disk
# image around it; a quarantined, unnotarized DMG is what Gatekeeper
# reports as "damaged". Stapling changes the DMG bytes, so the same
# script also rewrites its latest-mac.yml entry and drops the stale
# script also rewrites the channel's update manifest and drops the stale
# blockmap, keeping the update manifest verification below honest.
- name: Notarize and staple macOS DMG
working-directory: apps/desktop
run: node --import tsx scripts/finalize-mac-artifacts.ts dist
env:
UPDATE_MANIFEST: ${{ needs.prepare.outputs.mac_manifest }}
run: node --import tsx scripts/finalize-mac-artifacts.ts dist "$UPDATE_MANIFEST"

- name: Verify macOS update artifacts
shell: bash
Expand Down
18 changes: 13 additions & 5 deletions apps/desktop/scripts/finalize-mac-artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,14 @@ export interface FinalizeMacArtifactsOptions {
readonly distDir: string
readonly env: NodeJS.ProcessEnv
readonly log?: (message: string) => void
// electron-builder names the manifest after the release channel, so only the
// stable channel produces latest-mac.yml.
readonly manifestName?: string
readonly runCommand?: CommandRunner
}

const DEFAULT_MAC_MANIFEST = 'latest-mac.yml'

function requiredValue(env: NodeJS.ProcessEnv, name: string): string {
return env[name]!.trim()
}
Expand Down Expand Up @@ -72,6 +77,7 @@ export function rewriteLatestMacYaml(
filename: string,
sha512: string,
size: number,
manifestName: string = DEFAULT_MAC_MANIFEST,
): string {
const lines = yaml.split('\n')
let fileEntryIndent: number | undefined
Expand Down Expand Up @@ -126,7 +132,7 @@ export function rewriteLatestMacYaml(
}

if (checksumUpdates === 0 || sizeUpdates === 0) {
throw new Error(`latest-mac.yml does not contain complete metadata for ${filename}`)
throw new Error(`${manifestName} does not contain complete metadata for ${filename}`)
}
return lines.join('\n')
}
Expand All @@ -147,7 +153,8 @@ export function finalizeMacArtifacts(options: FinalizeMacArtifactsOptions): void
.sort()
if (dmgs.length === 0) throw new Error(`No DMG artifacts found in ${options.distDir}`)

const metadataPath = join(options.distDir, 'latest-mac.yml')
const manifestName = options.manifestName ?? DEFAULT_MAC_MANIFEST
const metadataPath = join(options.distDir, manifestName)
let metadata = readFileSync(metadataPath, 'utf8')
const credentialArgs = buildNotarytoolArguments(options.env)

Expand Down Expand Up @@ -181,7 +188,7 @@ export function finalizeMacArtifacts(options: FinalizeMacArtifactsOptions): void

const size = statSync(dmgPath).size
const sha512 = createHash('sha512').update(readFileSync(dmgPath)).digest('base64')
metadata = rewriteLatestMacYaml(metadata, filename, sha512, size)
metadata = rewriteLatestMacYaml(metadata, filename, sha512, size, manifestName)

const blockmapPath = `${dmgPath}.blockmap`
if (existsSync(blockmapPath)) {
Expand All @@ -196,9 +203,10 @@ export function finalizeMacArtifacts(options: FinalizeMacArtifactsOptions): void
const invokedPath = process.argv[1]
if (invokedPath !== undefined && resolve(invokedPath) === fileURLToPath(import.meta.url)) {
const distDir = process.argv[2]
const manifestName = process.argv[3]
try {
if (distDir === undefined) throw new Error('Usage: finalize-mac-artifacts.ts <dist-directory>')
finalizeMacArtifacts({ distDir: resolve(distDir), env: process.env })
if (distDir === undefined) throw new Error('Usage: finalize-mac-artifacts.ts <dist-directory> [manifest-name]')
finalizeMacArtifacts({ distDir: resolve(distDir), env: process.env, manifestName })
} catch (error) {
console.error(error instanceof Error ? error.message : String(error))
process.exitCode = 1
Expand Down
26 changes: 26 additions & 0 deletions apps/desktop/tests/finalize-mac-artifacts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,32 @@ sha512: old
expect(runCommand).toHaveBeenNthCalledWith(2, 'xcrun', ['stapler', 'staple', join(distDir, filename)])
})

it('finalizes the channel manifest a prerelease build actually produces', () => {
const distDir = mkdtempSync(join(tmpdir(), 'pythinker-mac-artifacts-'))
directories.push(distDir)
const filename = 'Pythinker-0.11.1-nightly.304-arm64.dmg'
const dmg = Buffer.from('nightly dmg fixture')
writeFileSync(join(distDir, filename), dmg)
writeFileSync(join(distDir, 'nightly-mac.yml'), `files:
- url: ${filename}
sha512: old
size: 1
path: ${filename}
sha512: old
`)

finalizeMacArtifacts({
distDir,
env: { APPLE_KEYCHAIN_PROFILE: 'pythinker-notary' },
log: () => {},
manifestName: 'nightly-mac.yml',
runCommand: () => ({ status: 0, stderr: '', stdout: '{"status":"Accepted"}' }),
})

const checksum = createHash('sha512').update(dmg).digest('base64')
expect(readFileSync(join(distDir, 'nightly-mac.yml'), 'utf8')).toContain(`sha512: ${checksum}`)
})

it('prints and rejects a non-accepted notarytool result before stapling', () => {
const distDir = mkdtempSync(join(tmpdir(), 'pythinker-mac-artifacts-'))
directories.push(distDir)
Expand Down
Loading