From a99f1603d87257c3be102e202416f16421fddf09 Mon Sep 17 00:00:00 2001 From: Joey Stanford Date: Mon, 20 Jul 2026 21:11:22 -0600 Subject: [PATCH 1/2] fix: skip pnpm supply-chain re-verify in Flatpak offline builds - Pass --config.trust-lockfile=true during Flatpak offline install so pnpm 11 does not re-check minimumReleaseAge against the registry. - Set PNPM_CONFIG_TRUST_LOCKFILE and disable verifyDepsBeforeRun in the Flatpak manifest so later pnpm run steps stay offline too. - Enforce the contract in check:flatpak and add source/manifest tests. After #701 fixed dist/pnpm.mjs, Flatpak CI hung for ~1h on "Verifying lockfile against supply-chain policies" with EAI_AGAIN DNS retries inside the sandbox, then was cancelled. --- org.coloradomesh.MeshClient.yml | 3 +++ scripts/check-flatpak.mjs | 16 ++++++++++++++++ scripts/flatpak-pnpm-bin.test.mjs | 6 ++++++ scripts/flatpak-pnpm-install.mjs | 15 ++++++++++++++- scripts/flatpak-pnpm-install.test.mjs | 7 +++++++ 5 files changed, 46 insertions(+), 1 deletion(-) diff --git a/org.coloradomesh.MeshClient.yml b/org.coloradomesh.MeshClient.yml index 1f5ff9308..924172cf5 100644 --- a/org.coloradomesh.MeshClient.yml +++ b/org.coloradomesh.MeshClient.yml @@ -38,6 +38,9 @@ modules: env: PNPM_HOME: /run/build/mesh-client/.pnpm pnpm_config_cache: /run/build/mesh-client/.npm + # pnpm 11 supply-chain / verifyDepsBeforeRun must not hit the registry offline. + PNPM_CONFIG_TRUST_LOCKFILE: 'true' + PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: 'false' build-commands: # Install pnpm standalone from archived release (no network needed). # Archive extracts to pnpm-vendor/ (strip-components: 0) so the root `pnpm` binary is diff --git a/scripts/check-flatpak.mjs b/scripts/check-flatpak.mjs index 3ae4d4699..7564a13be 100644 --- a/scripts/check-flatpak.mjs +++ b/scripts/check-flatpak.mjs @@ -169,6 +169,22 @@ function checkManifestPnpmVersion(pkg) { }); } + // Offline Flatpak builds cannot re-verify minimumReleaseAge / trustPolicy against npm. + if (!/PNPM_CONFIG_TRUST_LOCKFILE:\s*['"]?true['"]?/.test(yaml)) { + violations.push({ + file: rel, + message: + 'manifest build-options.env must set PNPM_CONFIG_TRUST_LOCKFILE: true (skip registry supply-chain re-verify offline)', + }); + } + if (!/PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN:\s*['"]?false['"]?/.test(yaml)) { + violations.push({ + file: rel, + message: + 'manifest build-options.env must set PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: false (pnpm run must not auto-install offline)', + }); + } + return violations; } diff --git a/scripts/flatpak-pnpm-bin.test.mjs b/scripts/flatpak-pnpm-bin.test.mjs index e638cc328..f70387ba4 100644 --- a/scripts/flatpak-pnpm-bin.test.mjs +++ b/scripts/flatpak-pnpm-bin.test.mjs @@ -15,4 +15,10 @@ describe('Flatpak pnpm standalone install', () => { ); expect(yaml).toMatch(/cp -a pnpm-vendor\/dist \/run\/build\/mesh-client\/\.pnpm-bin\/dist/); }); + + it('disables registry supply-chain re-verify and verifyDepsBeforeRun for offline builds', () => { + const yaml = fs.readFileSync(MANIFEST, 'utf8'); + expect(yaml).toMatch(/PNPM_CONFIG_TRUST_LOCKFILE:\s*['"]?true['"]?/); + expect(yaml).toMatch(/PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN:\s*['"]?false['"]?/); + }); }); diff --git a/scripts/flatpak-pnpm-install.mjs b/scripts/flatpak-pnpm-install.mjs index 7e18fefd8..55db173a8 100644 --- a/scripts/flatpak-pnpm-install.mjs +++ b/scripts/flatpak-pnpm-install.mjs @@ -8,6 +8,11 @@ * Failure point: host/CI runs without the Flatpak-vendored store at STORE_DIR. * pnpm 11+ may report "Already up to date" from an existing node_modules even when * --store-dir is missing; refuse to proceed so Flatpak always uses the offline store. + * + * Failure point: pnpm 11 re-verifies minimumReleaseAge / trustPolicy against the + * registry for every lockfile entry ("Verifying lockfile against supply-chain + * policies"). Flatpak build has no usable DNS, so those GETs hang on EAI_AGAIN. + * Fallback: --config.trust-lockfile=true (CI already resolved the lockfile online). */ import fs from 'fs'; import { spawnSync } from 'child_process'; @@ -36,7 +41,15 @@ for (let attempt = 1; attempt <= maxAttempts; attempt++) { const result = spawnSync( 'pnpm', - ['install', '--frozen-lockfile', '--offline', '--ignore-scripts', '--store-dir', STORE_DIR], + [ + 'install', + '--frozen-lockfile', + '--offline', + '--ignore-scripts', + '--config.trust-lockfile=true', + '--store-dir', + STORE_DIR, + ], { cwd: projectRoot, stdio: 'inherit', diff --git a/scripts/flatpak-pnpm-install.test.mjs b/scripts/flatpak-pnpm-install.test.mjs index a8d421a7d..ef81db54c 100644 --- a/scripts/flatpak-pnpm-install.test.mjs +++ b/scripts/flatpak-pnpm-install.test.mjs @@ -1,4 +1,5 @@ // @vitest-environment node +import fs from 'fs'; import { spawnSync } from 'child_process'; import { fileURLToPath } from 'url'; import path from 'path'; @@ -18,4 +19,10 @@ describe('flatpak-pnpm-install.mjs', () => { expect(result.status).not.toBe(0); expect(result.stderr + result.stdout).toMatch(/flatpak-pnpm|ERR_PNPM|offline|no such file/i); }); + + it('passes trust-lockfile so offline install skips registry supply-chain re-verify', () => { + const source = fs.readFileSync(scriptPath, 'utf8'); + expect(source).toContain('--config.trust-lockfile=true'); + expect(source).toContain('--offline'); + }); }); From 447183c383a85a4d7432a27492a051f9ee66295d Mon Sep 17 00:00:00 2001 From: Joey Stanford Date: Mon, 20 Jul 2026 21:20:26 -0600 Subject: [PATCH 2/2] fix: tighten Flatpak offline pnpm contract checks - Parse mesh-client build-options.env via a shared helper and require unquoted YAML booleans for trust-lockfile / verifyDepsBeforeRun. - Export Flatpak pnpm install argv so tests assert real spawn args, not documentation comments. - Reject quoted or commented env values in contract tests. Addresses CodeRabbit review on #702. --- org.coloradomesh.MeshClient.yml | 4 +- scripts/check-flatpak.mjs | 20 ++---- scripts/flatpak-pnpm-bin.test.mjs | 36 +++++++++- scripts/flatpak-pnpm-install.mjs | 78 +++++++++++---------- scripts/flatpak-pnpm-install.test.mjs | 9 +-- scripts/flatpakOfflinePnpmEnv.mjs | 98 +++++++++++++++++++++++++++ 6 files changed, 185 insertions(+), 60 deletions(-) create mode 100644 scripts/flatpakOfflinePnpmEnv.mjs diff --git a/org.coloradomesh.MeshClient.yml b/org.coloradomesh.MeshClient.yml index 924172cf5..1a15fa927 100644 --- a/org.coloradomesh.MeshClient.yml +++ b/org.coloradomesh.MeshClient.yml @@ -39,8 +39,8 @@ modules: PNPM_HOME: /run/build/mesh-client/.pnpm pnpm_config_cache: /run/build/mesh-client/.npm # pnpm 11 supply-chain / verifyDepsBeforeRun must not hit the registry offline. - PNPM_CONFIG_TRUST_LOCKFILE: 'true' - PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: 'false' + PNPM_CONFIG_TRUST_LOCKFILE: true + PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: false build-commands: # Install pnpm standalone from archived release (no network needed). # Archive extracts to pnpm-vendor/ (strip-components: 0) so the root `pnpm` binary is diff --git a/scripts/check-flatpak.mjs b/scripts/check-flatpak.mjs index 7564a13be..435a1fb25 100644 --- a/scripts/check-flatpak.mjs +++ b/scripts/check-flatpak.mjs @@ -2,6 +2,7 @@ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; +import { offlinePnpmEnvContractViolations } from './flatpakOfflinePnpmEnv.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, '..'); @@ -132,6 +133,9 @@ function checkManifestPnpmVersion(pkg) { const rel = path.relative(ROOT, MANIFEST); const pnpmVersion = pnpmVersionFromPackage(pkg); + // Offline Flatpak builds cannot re-verify minimumReleaseAge / trustPolicy against npm. + violations.push(...offlinePnpmEnvContractViolations(yaml, rel)); + if (!pnpmVersion) return violations; const releaseUrlPrefix = `pnpm/pnpm/releases/download/v${pnpmVersion}/`; @@ -169,22 +173,6 @@ function checkManifestPnpmVersion(pkg) { }); } - // Offline Flatpak builds cannot re-verify minimumReleaseAge / trustPolicy against npm. - if (!/PNPM_CONFIG_TRUST_LOCKFILE:\s*['"]?true['"]?/.test(yaml)) { - violations.push({ - file: rel, - message: - 'manifest build-options.env must set PNPM_CONFIG_TRUST_LOCKFILE: true (skip registry supply-chain re-verify offline)', - }); - } - if (!/PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN:\s*['"]?false['"]?/.test(yaml)) { - violations.push({ - file: rel, - message: - 'manifest build-options.env must set PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: false (pnpm run must not auto-install offline)', - }); - } - return violations; } diff --git a/scripts/flatpak-pnpm-bin.test.mjs b/scripts/flatpak-pnpm-bin.test.mjs index f70387ba4..10f3c3717 100644 --- a/scripts/flatpak-pnpm-bin.test.mjs +++ b/scripts/flatpak-pnpm-bin.test.mjs @@ -3,6 +3,10 @@ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { describe, expect, it } from 'vitest'; +import { + offlinePnpmEnvContractViolations, + parseMeshClientModuleBuildEnv, +} from './flatpakOfflinePnpmEnv.mjs'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const MANIFEST = path.join(ROOT, 'org.coloradomesh.MeshClient.yml'); @@ -16,9 +20,35 @@ describe('Flatpak pnpm standalone install', () => { expect(yaml).toMatch(/cp -a pnpm-vendor\/dist \/run\/build\/mesh-client\/\.pnpm-bin\/dist/); }); - it('disables registry supply-chain re-verify and verifyDepsBeforeRun for offline builds', () => { + it('requires unquoted offline pnpm booleans in mesh-client build-options.env', () => { const yaml = fs.readFileSync(MANIFEST, 'utf8'); - expect(yaml).toMatch(/PNPM_CONFIG_TRUST_LOCKFILE:\s*['"]?true['"]?/); - expect(yaml).toMatch(/PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN:\s*['"]?false['"]?/); + const env = parseMeshClientModuleBuildEnv(yaml); + expect(env).not.toBeNull(); + expect(env?.PNPM_CONFIG_TRUST_LOCKFILE).toBe(true); + expect(env?.PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN).toBe(false); + expect(offlinePnpmEnvContractViolations(yaml)).toEqual([]); + }); + + it('rejects quoted or commented offline pnpm env values', () => { + const quoted = ` +modules: + - name: mesh-client + build-options: + env: + PNPM_CONFIG_TRUST_LOCKFILE: 'true' + PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: 'false' +`; + expect(offlinePnpmEnvContractViolations(quoted).length).toBe(2); + + const commentedOnly = ` +modules: + - name: mesh-client + build-options: + env: + # PNPM_CONFIG_TRUST_LOCKFILE: true + # PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: false + PNPM_HOME: /run/build/mesh-client/.pnpm +`; + expect(offlinePnpmEnvContractViolations(commentedOnly).length).toBe(2); }); }); diff --git a/scripts/flatpak-pnpm-install.mjs b/scripts/flatpak-pnpm-install.mjs index 55db173a8..f00318d5c 100644 --- a/scripts/flatpak-pnpm-install.mjs +++ b/scripts/flatpak-pnpm-install.mjs @@ -22,48 +22,56 @@ import { cleanJsrTempDirs } from './clean-jsr-temp-dirs.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const projectRoot = path.resolve(__dirname, '..'); -const STORE_DIR = '/run/build/mesh-client/flatpak-node/pnpm-store'; +export const STORE_DIR = '/run/build/mesh-client/flatpak-node/pnpm-store'; const maxAttempts = 3; -if (!fs.existsSync(STORE_DIR)) { - console.error( - `[flatpak-pnpm] offline store missing: ${STORE_DIR} (Flatpak sandbox path required)`, - ); - process.exit(1); -} - -let lastStatus = 1; +/** Args passed to `pnpm` for Flatpak offline install (exported for contract tests). */ +export const FLATPAK_PNPM_INSTALL_ARGS = [ + 'install', + '--frozen-lockfile', + '--offline', + '--ignore-scripts', + '--config.trust-lockfile=true', + '--store-dir', + STORE_DIR, +]; -for (let attempt = 1; attempt <= maxAttempts; attempt++) { - if (attempt > 1) { - cleanJsrTempDirs(path.join(projectRoot, 'node_modules')); +export function runFlatpakPnpmInstall() { + if (!fs.existsSync(STORE_DIR)) { + console.error( + `[flatpak-pnpm] offline store missing: ${STORE_DIR} (Flatpak sandbox path required)`, + ); + process.exit(1); } - const result = spawnSync( - 'pnpm', - [ - 'install', - '--frozen-lockfile', - '--offline', - '--ignore-scripts', - '--config.trust-lockfile=true', - '--store-dir', - STORE_DIR, - ], - { + let lastStatus = 1; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + if (attempt > 1) { + cleanJsrTempDirs(path.join(projectRoot, 'node_modules')); + } + + const result = spawnSync('pnpm', FLATPAK_PNPM_INSTALL_ARGS, { cwd: projectRoot, stdio: 'inherit', - }, - ); - lastStatus = result.status ?? 1; - if (lastStatus === 0) { - process.exit(0); - } - if (attempt < maxAttempts) { - console.warn( - `[flatpak-pnpm] pnpm install failed (attempt ${attempt}/${maxAttempts}), retrying…`, - ); + }); + lastStatus = result.status ?? 1; + if (lastStatus === 0) { + process.exit(0); + } + if (attempt < maxAttempts) { + console.warn( + `[flatpak-pnpm] pnpm install failed (attempt ${attempt}/${maxAttempts}), retrying…`, + ); + } } + + process.exit(lastStatus); } -process.exit(lastStatus); +const isDirectRun = + process.argv[1] != null && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); + +if (isDirectRun) { + runFlatpakPnpmInstall(); +} diff --git a/scripts/flatpak-pnpm-install.test.mjs b/scripts/flatpak-pnpm-install.test.mjs index ef81db54c..dac5571f3 100644 --- a/scripts/flatpak-pnpm-install.test.mjs +++ b/scripts/flatpak-pnpm-install.test.mjs @@ -1,9 +1,9 @@ // @vitest-environment node -import fs from 'fs'; import { spawnSync } from 'child_process'; import { fileURLToPath } from 'url'; import path from 'path'; import { describe, expect, it } from 'vitest'; +import { FLATPAK_PNPM_INSTALL_ARGS } from './flatpak-pnpm-install.mjs'; const scriptPath = path.join( path.dirname(fileURLToPath(import.meta.url)), @@ -21,8 +21,9 @@ describe('flatpak-pnpm-install.mjs', () => { }); it('passes trust-lockfile so offline install skips registry supply-chain re-verify', () => { - const source = fs.readFileSync(scriptPath, 'utf8'); - expect(source).toContain('--config.trust-lockfile=true'); - expect(source).toContain('--offline'); + expect(FLATPAK_PNPM_INSTALL_ARGS).toContain('--config.trust-lockfile=true'); + expect(FLATPAK_PNPM_INSTALL_ARGS).toContain('--offline'); + expect(FLATPAK_PNPM_INSTALL_ARGS).toContain('--frozen-lockfile'); + expect(FLATPAK_PNPM_INSTALL_ARGS).toContain('--ignore-scripts'); }); }); diff --git a/scripts/flatpakOfflinePnpmEnv.mjs b/scripts/flatpakOfflinePnpmEnv.mjs new file mode 100644 index 000000000..cf7645103 --- /dev/null +++ b/scripts/flatpakOfflinePnpmEnv.mjs @@ -0,0 +1,98 @@ +/** + * Shared Flatpak manifest contract for offline pnpm 11 install. + * + * Failure point: loose YAML regex can match commented or quoted keys outside the + * mesh-client module env map. Fallback: parse only that scoped env block and + * require exact unquoted YAML booleans. + */ + +/** + * @param {string} yaml + * @returns {Record | null} + */ +export function parseMeshClientModuleBuildEnv(yaml) { + const moduleMatch = yaml.match(/^ {2}- name: mesh-client\s*$/m); + if (!moduleMatch || moduleMatch.index == null) return null; + + const fromModule = yaml.slice(moduleMatch.index); + const nextModuleOffset = fromModule.slice(1).search(/^ {2}- name: /m); + const moduleBlock = + nextModuleOffset === -1 ? fromModule : fromModule.slice(0, nextModuleOffset + 1); + + const envMatch = moduleBlock.match(/^ {6}env:\s*$/m); + if (!envMatch || envMatch.index == null) return null; + + const envIndent = 6; + const afterEnv = moduleBlock.slice(envMatch.index + envMatch[0].length); + /** @type {Record} */ + const env = {}; + + for (const line of afterEnv.split('\n')) { + if (line.trim() === '') continue; + const indent = line.match(/^ */)[0].length; + if (indent <= envIndent) break; + + const trimmed = line.trim(); + if (trimmed.startsWith('#')) continue; + + const m = trimmed.match(/^([A-Za-z0-9_]+):\s*(.+)$/); + if (!m) continue; + + const [, key, raw] = m; + if (raw === 'true') { + env[key] = true; + } else if (raw === 'false') { + env[key] = false; + } else if ( + (raw.startsWith("'") && raw.endsWith("'")) || + (raw.startsWith('"') && raw.endsWith('"')) + ) { + // Quoted values are strings, not YAML booleans — reject for required keys. + env[key] = raw.slice(1, -1); + } else { + env[key] = raw; + } + } + + return env; +} + +/** + * @param {string} yaml + * @param {string} [fileRel] + * @returns {{ file: string, message: string }[]} + */ +export function offlinePnpmEnvContractViolations( + yaml, + fileRel = 'org.coloradomesh.MeshClient.yml', +) { + const env = parseMeshClientModuleBuildEnv(yaml); + /** @type {{ file: string, message: string }[]} */ + const violations = []; + + if (!env) { + violations.push({ + file: fileRel, + message: 'manifest mesh-client module build-options.env is missing', + }); + return violations; + } + + if (env.PNPM_CONFIG_TRUST_LOCKFILE !== true) { + violations.push({ + file: fileRel, + message: + 'manifest mesh-client build-options.env must set PNPM_CONFIG_TRUST_LOCKFILE: true (unquoted boolean; skip registry supply-chain re-verify offline)', + }); + } + + if (env.PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN !== false) { + violations.push({ + file: fileRel, + message: + 'manifest mesh-client build-options.env must set PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: false (unquoted boolean; pnpm run must not auto-install offline)', + }); + } + + return violations; +}