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
3 changes: 3 additions & 0 deletions org.coloradomesh.MeshClient.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions scripts/check-flatpak.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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, '..');
Expand Down Expand Up @@ -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}/`;
Expand Down
36 changes: 36 additions & 0 deletions scripts/flatpak-pnpm-bin.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -15,4 +19,36 @@ describe('Flatpak pnpm standalone install', () => {
);
expect(yaml).toMatch(/cp -a pnpm-vendor\/dist \/run\/build\/mesh-client\/\.pnpm-bin\/dist/);
});

it('requires unquoted offline pnpm booleans in mesh-client build-options.env', () => {
const yaml = fs.readFileSync(MANIFEST, 'utf8');
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);
});
});
75 changes: 48 additions & 27 deletions scripts/flatpak-pnpm-install.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -17,40 +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', '--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();
}
8 changes: 8 additions & 0 deletions scripts/flatpak-pnpm-install.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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)),
Expand All @@ -18,4 +19,11 @@ 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', () => {
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');
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
98 changes: 98 additions & 0 deletions scripts/flatpakOfflinePnpmEnv.mjs
Original file line number Diff line number Diff line change
@@ -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<string, boolean | string> | 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;
Comment on lines +22 to +23

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Scope env to the build-options block.

This accepts an env: nested under any module field at the same indentation, so the contract can pass while mesh-client.build-options.env is absent and pnpm receives neither required setting. First isolate the sibling-bounded build-options: block, then search for env: within it; add a regression fixture for an unrelated nested env: mapping.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/flatpakOfflinePnpmEnv.mjs` around lines 22 - 23, Update the parsing
logic around envMatch to first isolate the sibling-bounded build-options: block,
then search for env: only within that block. Ensure unrelated env: mappings
nested under other module fields are ignored, and add a regression fixture
covering that case while preserving extraction of mesh-client.build-options.env.


const envIndent = 6;
const afterEnv = moduleBlock.slice(envMatch.index + envMatch[0].length);
/** @type {Record<string, boolean | string>} */
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;
}
Loading