From 04d46c1912b34303d686909753880bb2c2a5200b Mon Sep 17 00:00:00 2001 From: winewill Date: Tue, 13 Oct 2026 14:01:39 +0800 Subject: [PATCH] fix(desktop): use native shells for cross-platform publication --- .github/workflows/desktop-publish.yml | 14 ++-------- apps/desktop/scripts/read-desktop-app-key.mjs | 14 ++++++++++ .../tests/read-desktop-app-key.spec.ts | 26 +++++++++++++++++++ scripts/desktop-publish-workflow.spec.ts | 3 +++ 4 files changed, 45 insertions(+), 12 deletions(-) create mode 100644 apps/desktop/scripts/read-desktop-app-key.mjs create mode 100644 apps/desktop/tests/read-desktop-app-key.spec.ts diff --git a/.github/workflows/desktop-publish.yml b/.github/workflows/desktop-publish.yml index b5f6d1ad90..919170bdd4 100644 --- a/.github/workflows/desktop-publish.yml +++ b/.github/workflows/desktop-publish.yml @@ -46,9 +46,6 @@ jobs: env: DESKTOP_PUBLISH_ROUTE: ${{ needs.route.outputs.route }} DSH_TELEMETRY_DISABLED: '1' - defaults: - run: - shell: bash steps: - uses: actions/checkout@v7.0.1 with: @@ -73,15 +70,8 @@ jobs: id: key env: APP_KEY_FILE: ${{ steps.check.outputs.app-key-file }} - run: | - node --input-type=module <<'NODE' - import { readFileSync, appendFileSync } from 'node:fs'; - import { randomUUID } from 'node:crypto'; - const key = readFileSync(process.env.APP_KEY_FILE, 'utf8'); - console.log('::add-mask::' + key.replaceAll('%', '%25').replaceAll('\r', '%0D').replaceAll('\n', '%0A')); - const delimiter = randomUUID(); - appendFileSync(process.env.GITHUB_OUTPUT, `private-key<<${delimiter}\n${key}\n${delimiter}\n`); - NODE + working-directory: desktop-publisher + run: node apps/desktop/scripts/read-desktop-app-key.mjs - name: Create short-lived tag token if: needs.route.outputs.preset == 'production' id: app diff --git a/apps/desktop/scripts/read-desktop-app-key.mjs b/apps/desktop/scripts/read-desktop-app-key.mjs new file mode 100644 index 0000000000..590f7898d2 --- /dev/null +++ b/apps/desktop/scripts/read-desktop-app-key.mjs @@ -0,0 +1,14 @@ +/** Hand a machine App key to the token action after Environment approval, masking before output. */ +import { readFileSync, appendFileSync } from 'node:fs' +import { randomUUID } from 'node:crypto' + +try { + if (!process.env.APP_KEY_FILE || !process.env.GITHUB_OUTPUT) throw new Error('Actions key file and output are required') + const key = readFileSync(process.env.APP_KEY_FILE, 'utf8') + console.log('::add-mask::' + key.replaceAll('%', '%25').replaceAll('\r', '%0D').replaceAll('\n', '%0A')) + const delimiter = randomUUID() + appendFileSync(process.env.GITHUB_OUTPUT, `private-key<<${delimiter}\n${key}\n${delimiter}\n`) +} catch { + process.stderr.write('Desktop publication: cannot hand the configured App key to the token action.\n') + process.exitCode = 1 +} diff --git a/apps/desktop/tests/read-desktop-app-key.spec.ts b/apps/desktop/tests/read-desktop-app-key.spec.ts new file mode 100644 index 0000000000..4b67345f7e --- /dev/null +++ b/apps/desktop/tests/read-desktop-app-key.spec.ts @@ -0,0 +1,26 @@ +import { execFile } from 'node:child_process' +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { promisify } from 'node:util' +import { expect, it } from 'vitest' + +it('masks multiline key bytes and passes the exact fixture to the token action without a shell', async () => { + const root = await mkdtemp(join(tmpdir(), 'desktop-app-key-')) + try { + const path = join(root, 'fixture.pem') + const output = join(root, 'output') + const key = 'fixture%key\r\nsecond-line\n' + await writeFile(path, key) + const result = await promisify(execFile)(process.execPath, + [fileURLToPath(new URL('../scripts/read-desktop-app-key.mjs', import.meta.url))], { + env: { ...process.env, APP_KEY_FILE: path, GITHUB_OUTPUT: output }, encoding: 'utf8', + }) + expect(result.stdout).toBe('::add-mask::fixture%25key%0D%0Asecond-line%0A\n') + expect(result.stderr).toBe('') + const contents = await readFile(output, 'utf8') + const delimiter = contents.slice('private-key<<'.length, contents.indexOf('\n')) + expect(contents).toBe(`private-key<<${delimiter}\n${key}\n${delimiter}\n`) + } finally { await rm(root, { recursive: true, force: true }) } +}) diff --git a/scripts/desktop-publish-workflow.spec.ts b/scripts/desktop-publish-workflow.spec.ts index 2db19a7dc7..2796f234e2 100644 --- a/scripts/desktop-publish-workflow.spec.ts +++ b/scripts/desktop-publish-workflow.spec.ts @@ -7,6 +7,7 @@ const workflow = yaml.load(readFileSync(resolve(import.meta.dirname, '../.github on: Record permissions: Record jobs: Record step.id === 'key')?.run).toBe('node apps/desktop/scripts/read-desktop-app-key.mjs') expect(publish.if).toBe("needs.route.outputs.publish == 'true'") expect(publish['runs-on']).toEqual(['self-hosted', '${{ needs.route.outputs.runner }}']) expect(publish.environment).toBe("${{ needs.route.outputs.preset == 'production' && 'desktop-production' || 'desktop-test' }}")