fix(release): validate minified telemetry and prune target natives first

This commit is contained in:
Jinwoo-H
2026-09-03 19:26:51 -04:00
parent c806336dc4
commit 35f1b0ebb9
4 changed files with 37 additions and 17 deletions
+7 -10
View File
@@ -266,16 +266,6 @@ module.exports = {
}
},
afterPack: async (context) => {
// Why: a Linux runner-image glibc bump silently shipped a node-pty pty.node
// requiring GLIBC_2.34, crashing the app on startup on Ubuntu 20.04 (#9902).
// Fail packaging if any bundled native binary exceeds the supported floor.
if (context.electronPlatformName === 'linux') {
// Why the arch is passed: symbol-version checks pass happily on a wrong-architecture binary,
// so a cross-built slice could ship the host's pty.node and only fail at runtime.
verifyLinuxGlibcFloor(context.appOutDir, {
targetArch: { 1: 'x64', 3: 'arm64' }[context.arch]
})
}
const resourcesDir =
context.electronPlatformName === 'darwin'
? join(
@@ -313,6 +303,13 @@ module.exports = {
}
stampPackagedCliVersion(resourcesDir, context.packager.appInfo.version)
prunePackagedRuntimeNodeModules(resourcesDir, context.electronPlatformName, context.arch)
// Prune optional native variants before checking the glibc/architecture
// floor; cross-builds intentionally install all optional packages.
if (context.electronPlatformName === 'linux') {
verifyLinuxGlibcFloor(context.appOutDir, {
targetArch: { 1: 'x64', 3: 'arm64' }[context.arch]
})
}
verifyPackagedMainRuntimeDeps(resourcesDir)
// Why: boot the packaged daemon-entry under plain Node, but only for the
// slice matching the packaging host's arch — daemon-entry.js is JS, yet it
@@ -1,2 +1,6 @@
export const BUILD_IDENTITY_RE = /\b(?:const|let|var)\s+BUILD_IDENTITY\s*=\s*"(rc|stable)"/
export const WRITE_KEY_RE = /\b(?:const|let|var)\s+WRITE_KEY\s*=\s*"(phc_[A-Za-z0-9_-]+)"/
// The unminified bundle keeps these names. Production minification may rename
// them, but the injected identity and key remain adjacent in the declaration.
export const BUILD_IDENTITY_RE = /\b(?:const|let|var)\s+BUILD_IDENTITY\s*=\s*["`](rc|stable)["`]/
export const WRITE_KEY_RE = /\b(?:const|let|var)\s+WRITE_KEY\s*=\s*["`](phc_[A-Za-z0-9_-]+)["`]/
export const MINIFIED_TELEMETRY_RE =
/\b(?:const|let|var)\s+[$\w]+\s*=\s*["`](rc|stable)["`]\s*,\s*[$\w]+\s*=\s*["`](phc_[A-Za-z0-9_-]+)["`]/
@@ -1,5 +1,9 @@
import { describe, expect, it } from 'vitest'
import { BUILD_IDENTITY_RE, WRITE_KEY_RE } from './telemetry-bundle-constant-patterns.mjs'
import {
BUILD_IDENTITY_RE,
MINIFIED_TELEMETRY_RE,
WRITE_KEY_RE
} from './telemetry-bundle-constant-patterns.mjs'
describe('telemetry bundle constant patterns', () => {
it.each(['const', 'let', 'var'])('accepts %s declarations', (declaration) => {
@@ -13,4 +17,9 @@ describe('telemetry bundle constant patterns', () => {
expect('const WRITE_KEY = null').not.toMatch(WRITE_KEY_RE)
expect('const WRITE_KEY = "example-key"').not.toMatch(WRITE_KEY_RE)
})
it('accepts minified adjacent declarations', () => {
const bundle = 'var dde=`stable`,fde=`phc_example-key_123`,pde=(dde===`stable`)'
expect(bundle).toMatch(MINIFIED_TELEMETRY_RE)
})
})
+14 -4
View File
@@ -38,7 +38,11 @@ import { join, resolve } from 'node:path'
// `node_modules`). If electron-builder ever drops it, promote this to a
// direct devDependency in package.json.
import { extractFile, listPackage } from '@electron/asar'
import { BUILD_IDENTITY_RE, WRITE_KEY_RE } from './telemetry-bundle-constant-patterns.mjs'
import {
BUILD_IDENTITY_RE,
MINIFIED_TELEMETRY_RE,
WRITE_KEY_RE
} from './telemetry-bundle-constant-patterns.mjs'
// Why resolve from import.meta.url instead of cwd: a release runner (or a
// developer debugging locally) may invoke this script from a non-root cwd.
@@ -156,8 +160,14 @@ function verifyAsar(asarPath) {
const buildIdentityMatch = BUILD_IDENTITY_RE.exec(indexJs)
const writeKeyMatch = WRITE_KEY_RE.exec(indexJs)
const minifiedTelemetryMatch = MINIFIED_TELEMETRY_RE.exec(indexJs)
if (!buildIdentityMatch) {
// Rolldown renames module-local constants in production output. In that
// form, verify the adjacent injected identity/key declaration instead.
const verifiedIdentity = buildIdentityMatch?.[1] ?? minifiedTelemetryMatch?.[1]
const verifiedWriteKey = writeKeyMatch?.[1] ?? minifiedTelemetryMatch?.[2]
if (!verifiedIdentity) {
console.error(`::error::BUILD_IDENTITY constant missing or unexpected value in ${asarPath}`)
const sample = indexJs.match(/.{0,80}BUILD_IDENTITY.{0,80}/g)?.slice(0, 5) ?? []
for (const line of sample) {
@@ -165,7 +175,7 @@ function verifyAsar(asarPath) {
}
return null
}
if (!writeKeyMatch) {
if (!verifiedWriteKey) {
console.error(`::error::PostHog WRITE_KEY missing from ${asarPath}`)
const sample = indexJs.match(/.{0,80}WRITE_KEY.{0,80}/g)?.slice(0, 5) ?? []
for (const line of sample) {
@@ -174,7 +184,7 @@ function verifyAsar(asarPath) {
return null
}
return { asarPath, buildIdentity: buildIdentityMatch[1], writeKey: writeKeyMatch[1] }
return { asarPath, buildIdentity: verifiedIdentity, writeKey: verifiedWriteKey }
}
// Why verify every match (not just the first): macOS dual-arch produces one