From 12cb3166d376139974beadc387b3c77e2a5c47cf Mon Sep 17 00:00:00 2001 From: AstroHan Date: Fri, 4 Sep 2026 09:48:59 +0800 Subject: [PATCH 1/9] fix(release): decouple Nightly from registry audit Generated-by: Codex --- .github/workflows/desktop-nightly.yml | 9 ++----- .github/workflows/npm-publication.yml | 2 +- .../desktop-nightly-workflow-policy.test.mjs | 25 +++++++++++++++++++ scripts/release-cli-package.mjs | 6 +++-- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/.github/workflows/desktop-nightly.yml b/.github/workflows/desktop-nightly.yml index 9616f9d957..12538c08cb 100644 --- a/.github/workflows/desktop-nightly.yml +++ b/.github/workflows/desktop-nightly.yml @@ -129,12 +129,7 @@ jobs: cache: npm - name: Install dependencies - run: npm ci - - - name: Audit the shipped Desktop dependency closure - run: | - npm audit --omit=dev --audit-level=moderate - node scripts/audit-shipped-dependencies.mjs + run: npm ci --no-audit - name: Update stable Rust for native Desktop artifacts run: rustup update stable --no-self-update @@ -254,7 +249,7 @@ jobs: cache: npm - name: Install publisher dependencies - run: npm ci --ignore-scripts + run: npm ci --ignore-scripts --no-audit - name: Download every verified Desktop build uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 diff --git a/.github/workflows/npm-publication.yml b/.github/workflows/npm-publication.yml index 1fc32f99a1..86b0dd335f 100644 --- a/.github/workflows/npm-publication.yml +++ b/.github/workflows/npm-publication.yml @@ -123,7 +123,7 @@ jobs: cache: npm - name: Install publisher dependencies - run: npm ci --ignore-scripts + run: npm ci --ignore-scripts --no-audit - name: Select the publication npm toolchain run: npm install --global --no-audit --no-fund "$(node -p 'require("./package.json").packageManager')" diff --git a/scripts/desktop-nightly-workflow-policy.test.mjs b/scripts/desktop-nightly-workflow-policy.test.mjs index 5c561cf47d..6b0f72c959 100644 --- a/scripts/desktop-nightly-workflow-policy.test.mjs +++ b/scripts/desktop-nightly-workflow-policy.test.mjs @@ -57,6 +57,31 @@ test('npm publication owns both npm channels and no Desktop authority', async () assert.doesNotMatch(JSON.stringify(workflow), /NODE_AUTH_TOKEN|NPM_TOKEN/u); }); +test('Nightly packaging does not depend on the registry audit service', async () => { + const formal = await readWorkflow('release-cli-stage.yml'); + assert.equal(formal.jobs.validate.with.package_version, undefined); + + const nightlyWorkflows = [ + await readWorkflow('npm-publication.yml'), + await readWorkflow('desktop-nightly.yml'), + ]; + const desktop = nightlyWorkflows[1]; + const commands = desktop.jobs.desktop.steps + .map((step) => step.run) + .filter((run) => typeof run === 'string') + .join('\n'); + assert.doesNotMatch(commands, /npm audit|audit-shipped-dependencies/u); + + for (const workflow of nightlyWorkflows) { + const installs = Object.values(workflow.jobs) + .flatMap((job) => job.steps ?? []) + .flatMap((step) => (typeof step.run === 'string' ? step.run.split('\n') : [])) + .filter((command) => command.trimStart().startsWith('npm ci')); + assert.ok(installs.length > 0); + for (const install of installs) assert.match(install, /--no-audit/u); + } +}); + test('Desktop Nightly starts only from a successful published npm identity', async () => { const workflow = await readWorkflow('desktop-nightly.yml'); assert.deepEqual(workflow.on, { diff --git a/scripts/release-cli-package.mjs b/scripts/release-cli-package.mjs index c3976e694c..da8bfc2b11 100644 --- a/scripts/release-cli-package.mjs +++ b/scripts/release-cli-package.mjs @@ -133,7 +133,7 @@ function main() { ); } buildRuntimeWorkspaces({ clean: true }); - checkProductionAudit(); + if (!nightlyVersion) checkProductionAudit(); runNpm(['run', 'check:cli-third-party-notices']); runNpm(['run', 'check:runtime-host-peer-dependencies']); runNpm(['run', 'check:runtime-host-peer-notices']); @@ -220,9 +220,11 @@ function buildFromCleanDependencyTree() { const preparedPeerPrebuilds = copyPeerPrebuildInputToCleanTree(cleanRoot); console.log('[release-cli] installing the committed dependency tree with npm ci'); const cleanEnvironment = releaseNpmEnvironment(process.env, join(cleanRoot, '.npmrc')); + const installArguments = ['ci']; + if (nightlyVersion) installArguments.push('--no-audit'); execFileSync( 'npm', - ['ci'], + installArguments, npmSpawnOptions({ cwd: cleanRoot, env: cleanEnvironment, stdio: 'inherit' }), ); execFileSync(process.execPath, [join(cleanRoot, 'scripts/release-cli-package.mjs')], { From b271e4f81a4e175e0d6ac1cda03cc13798e9ce66 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Fri, 4 Sep 2026 09:55:25 +0800 Subject: [PATCH 2/9] test(release): avoid pinning Nightly audit implementation Generated-by: Codex --- .../desktop-nightly-workflow-policy.test.mjs | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/scripts/desktop-nightly-workflow-policy.test.mjs b/scripts/desktop-nightly-workflow-policy.test.mjs index 6b0f72c959..5c561cf47d 100644 --- a/scripts/desktop-nightly-workflow-policy.test.mjs +++ b/scripts/desktop-nightly-workflow-policy.test.mjs @@ -57,31 +57,6 @@ test('npm publication owns both npm channels and no Desktop authority', async () assert.doesNotMatch(JSON.stringify(workflow), /NODE_AUTH_TOKEN|NPM_TOKEN/u); }); -test('Nightly packaging does not depend on the registry audit service', async () => { - const formal = await readWorkflow('release-cli-stage.yml'); - assert.equal(formal.jobs.validate.with.package_version, undefined); - - const nightlyWorkflows = [ - await readWorkflow('npm-publication.yml'), - await readWorkflow('desktop-nightly.yml'), - ]; - const desktop = nightlyWorkflows[1]; - const commands = desktop.jobs.desktop.steps - .map((step) => step.run) - .filter((run) => typeof run === 'string') - .join('\n'); - assert.doesNotMatch(commands, /npm audit|audit-shipped-dependencies/u); - - for (const workflow of nightlyWorkflows) { - const installs = Object.values(workflow.jobs) - .flatMap((job) => job.steps ?? []) - .flatMap((step) => (typeof step.run === 'string' ? step.run.split('\n') : [])) - .filter((command) => command.trimStart().startsWith('npm ci')); - assert.ok(installs.length > 0); - for (const install of installs) assert.match(install, /--no-audit/u); - } -}); - test('Desktop Nightly starts only from a successful published npm identity', async () => { const workflow = await readWorkflow('desktop-nightly.yml'); assert.deepEqual(workflow.on, { From 0c781f27f7a7dec461e311b9325648d8443f23bc Mon Sep 17 00:00:00 2001 From: AstroHan Date: Fri, 4 Sep 2026 11:20:51 +0800 Subject: [PATCH 3/9] fix(release): gate Nightly on shipped advisories Generated-by: Codex --- .github/workflows/npm-publication.yml | 26 +++- package.json | 2 +- scripts/audit-shipped-dependencies.mjs | 155 +++++++++++++------- scripts/audit-shipped-dependencies.test.mjs | 73 +++++++++ 4 files changed, 198 insertions(+), 58 deletions(-) create mode 100644 scripts/audit-shipped-dependencies.test.mjs diff --git a/.github/workflows/npm-publication.yml b/.github/workflows/npm-publication.yml index 86b0dd335f..eceef00dc2 100644 --- a/.github/workflows/npm-publication.yml +++ b/.github/workflows/npm-publication.yml @@ -88,12 +88,36 @@ jobs: cli: name: Validate npm Nightly - needs: identity + needs: [identity, audit] uses: ./.github/workflows/cli-package-validation.yml with: source_commit: ${{ needs.identity.outputs.source_commit }} package_version: ${{ needs.identity.outputs.version }} + audit: + name: Audit shipped Nightly dependencies + needs: identity + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - name: Check out the exact Nightly source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.identity.outputs.source_commit }} + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + cache: npm + + - name: Install dependency graph without an implicit audit + run: npm ci --ignore-scripts --no-audit + + - name: Block shipped advisories without blocking on audit availability + run: node scripts/audit-shipped-dependencies.mjs --allow-unavailable + publish: name: Publish npm Nightly needs: [identity, cli] diff --git a/package.json b/package.json index 86b97fc986..592d1c7661 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "check:runtime-host-peer-dependencies": "node scripts/generate-runtime-host-peer-dependencies.mjs --check", "generate:runtime-host-peer-notices": "node scripts/generate-runtime-host-peer-notices.mjs", "check:runtime-host-peer-notices": "node scripts/generate-runtime-host-peer-notices.mjs --check", - "check:release": "npm run check:stale && npm run check:third-party-notices && npm run check:cli-third-party-notices && npm run check:model-metadata && npm run check:product-release-identity && npm run check:asf-npm && node --test scripts/product-nightly.test.mjs scripts/desktop-release-targets.test.mjs scripts/verify-linux-harness.test.mjs scripts/desktop-nightly.test.mjs scripts/desktop-nightly-stage.test.mjs scripts/desktop-nightly-release.test.mjs scripts/desktop-nightly-workflow-policy.test.mjs scripts/product-release.test.mjs scripts/product-release-authority.test.mjs scripts/release-cli-file-policy.test.mjs scripts/release-cli-artifact-policy.test.mjs scripts/release-cli-eval-support.test.mjs scripts/release-cli-publication.test.mjs scripts/release-cli-runtime-host-diagnostics.test.mjs scripts/qualify-released-cli-state-root.test.mjs scripts/release-cli-workflow-policy.test.mjs scripts/verify-packaged-app.test.mjs scripts/third-party-closure.test.mjs scripts/generate-third-party-notices.test.mjs scripts/source-legal-inventory.test.mjs scripts/sync-model-metadata.test.mjs scripts/prepare-windows-upgrade-baseline.test.mjs scripts/windows-package-source-closure.test.mjs", + "check:release": "npm run check:stale && npm run check:third-party-notices && npm run check:cli-third-party-notices && npm run check:model-metadata && npm run check:product-release-identity && npm run check:asf-npm && node --test scripts/product-nightly.test.mjs scripts/desktop-release-targets.test.mjs scripts/verify-linux-harness.test.mjs scripts/desktop-nightly.test.mjs scripts/desktop-nightly-stage.test.mjs scripts/desktop-nightly-release.test.mjs scripts/desktop-nightly-workflow-policy.test.mjs scripts/audit-shipped-dependencies.test.mjs scripts/product-release.test.mjs scripts/product-release-authority.test.mjs scripts/release-cli-file-policy.test.mjs scripts/release-cli-artifact-policy.test.mjs scripts/release-cli-eval-support.test.mjs scripts/release-cli-publication.test.mjs scripts/release-cli-runtime-host-diagnostics.test.mjs scripts/qualify-released-cli-state-root.test.mjs scripts/release-cli-workflow-policy.test.mjs scripts/verify-packaged-app.test.mjs scripts/third-party-closure.test.mjs scripts/generate-third-party-notices.test.mjs scripts/source-legal-inventory.test.mjs scripts/sync-model-metadata.test.mjs scripts/prepare-windows-upgrade-baseline.test.mjs scripts/windows-package-source-closure.test.mjs", "package:macos-arm64": "node scripts/package-macos.mjs arm64", "package:macos-x64": "node scripts/package-macos.mjs x64", "verify:macos": "node scripts/verify-macos-dmg.mjs", diff --git a/scripts/audit-shipped-dependencies.mjs b/scripts/audit-shipped-dependencies.mjs index e4bf08e81b..5da78584cc 100644 --- a/scripts/audit-shipped-dependencies.mjs +++ b/scripts/audit-shipped-dependencies.mjs @@ -17,15 +17,15 @@ * under the License. */ -// Audits what the desktop artifact ships, not what npm labels production. +// Audits what the product artifacts ship, not what npm labels production. // // `npm audit --omit=dev` covers the Node production closure, but the renderer // roots live in `devDependencies` (so electron-builder keeps their unread // sources out of `app.asar`) while vite still bundles them into // `dist-renderer`. A vulnerability in react would therefore ship without the -// production audit ever seeing it. This audits the full npm report and fails -// on anything that lands in the shipped desktop closure — Node production -// plus everything reachable from the declared renderer roots. +// production audit ever seeing it. This audits one full npm report and fails +// on anything that lands in the shipped CLI or Desktop closure — Node +// production plus everything reachable from the declared renderer roots. // // An advisory names a package and npm resolves it to the installed copies it // actually reaches (`nodes`). Only a copy whose exact version is in the @@ -33,9 +33,10 @@ // on a tooling-only path (electron → @electron/get → undici) is not shipped // and must not turn this red. When npm elides the paths, the name alone // fails, since the miss would otherwise be silent. -import { execFileSync } from 'node:child_process'; +import { spawnSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; import { join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { npmSpawnOptions } from './npm-spawn.mjs'; import { collectWorkspaceClosure } from './third-party-closure.mjs'; @@ -43,38 +44,9 @@ const repoRoot = resolve(import.meta.dirname, '..'); const SEVERITY_RANK = { info: 0, low: 1, moderate: 2, high: 3, critical: 4 }; // Matches the `--audit-level=moderate` the production audit step uses. const FAIL_RANK = SEVERITY_RANK.moderate; +const AUDIT_TIMEOUT_MS = 60_000; -function npmAuditReport() { - const options = npmSpawnOptions({ - cwd: repoRoot, - encoding: 'utf8', - maxBuffer: 64 * 1024 * 1024, - }); - try { - return JSON.parse(execFileSync('npm', ['audit', '--json'], options)); - } catch (error) { - // npm audit exits nonzero when it finds anything; the JSON is complete. - if (typeof error.stdout === 'string' && error.stdout.trim().startsWith('{')) { - return JSON.parse(error.stdout); - } - throw error; - } -} - -const closure = collectWorkspaceClosure({ - workspaceName: '@maka/desktop', - manifestPath: join(repoRoot, 'apps', 'desktop', 'package.json'), -}); -const shippedVersions = new Map(); -for (const { name, version } of closure) { - if (!shippedVersions.has(name)) shippedVersions.set(name, new Set()); - shippedVersions.get(name).add(version); -} - -const lockPackages = - JSON.parse(readFileSync(join(repoRoot, 'package-lock.json'), 'utf8')).packages ?? {}; - -function shippedCopies(vulnerability) { +function shippedCopies(vulnerability, shippedVersions, lockPackages) { const shipped = shippedVersions.get(vulnerability.name); if (!shipped) return []; const nodes = vulnerability.nodes ?? []; @@ -84,26 +56,97 @@ function shippedCopies(vulnerability) { .filter((version) => version !== undefined && shipped.has(version)); } -const report = npmAuditReport(); -const flagged = Object.values(report.vulnerabilities ?? {}) - .filter( - (vulnerability) => - (SEVERITY_RANK[vulnerability.severity] ?? SEVERITY_RANK.critical) >= FAIL_RANK, - ) - .map((vulnerability) => ({ vulnerability, copies: shippedCopies(vulnerability) })) - .filter(({ copies }) => copies.length > 0) - .sort((left, right) => left.vulnerability.name.localeCompare(right.vulnerability.name)); +function unavailableDetail(audit, report) { + const registryError = report?.error; + if (registryError && JSON.stringify(registryError) !== '{}') { + const detail = JSON.stringify(registryError); + if (detail !== '{"summary":"","detail":""}') return detail; + } + if (audit.error instanceof Error) return audit.error.message; + if (typeof audit.stderr === 'string' && audit.stderr.trim()) return audit.stderr.trim(); + return `npm audit exited with status ${audit.status ?? 'unknown'}`; +} -console.log( - `[audit-shipped] desktop shipped closure: ${shippedVersions.size} packages; ` + - `advisories reaching it at moderate or above: ${flagged.length}`, -); -for (const { vulnerability, copies } of flagged) { - const causes = (vulnerability.via ?? []) - .map((via) => (typeof via === 'string' ? `via ${via}` : `${via.title} (${via.url})`)) - .join('; '); - console.error( - `[audit-shipped] ${vulnerability.name}@${copies.join(', ')}: ${vulnerability.severity} — ${causes}`, +export function evaluateShippedAudit(audit, shippedVersions, lockPackages) { + let report; + try { + report = JSON.parse(audit.stdout || '{}'); + } catch { + return { outcome: 'unavailable', detail: unavailableDetail(audit) }; + } + if (!report.vulnerabilities || typeof report.vulnerabilities !== 'object') { + return { outcome: 'unavailable', detail: unavailableDetail(audit, report) }; + } + const flagged = Object.values(report.vulnerabilities) + .filter( + (vulnerability) => + (SEVERITY_RANK[vulnerability.severity] ?? SEVERITY_RANK.critical) >= FAIL_RANK, + ) + .map((vulnerability) => ({ + vulnerability, + copies: shippedCopies(vulnerability, shippedVersions, lockPackages), + })) + .filter(({ copies }) => copies.length > 0) + .sort((left, right) => left.vulnerability.name.localeCompare(right.vulnerability.name)); + return { outcome: flagged.length > 0 ? 'blocked' : 'clean', flagged }; +} + +function collectShippedVersions() { + const closures = [ + collectWorkspaceClosure({ workspaceName: 'maka-agent' }), + collectWorkspaceClosure({ + workspaceName: '@maka/desktop', + manifestPath: join(repoRoot, 'apps', 'desktop', 'package.json'), + }), + ]; + const shippedVersions = new Map(); + for (const closure of closures) { + for (const { name, version } of closure) { + if (!shippedVersions.has(name)) shippedVersions.set(name, new Set()); + shippedVersions.get(name).add(version); + } + } + return shippedVersions; +} + +function main() { + const allowUnavailable = process.argv.includes('--allow-unavailable'); + const audit = spawnSync( + 'npm', + ['audit', '--json'], + npmSpawnOptions({ + cwd: repoRoot, + encoding: 'utf8', + maxBuffer: 64 * 1024 * 1024, + timeout: AUDIT_TIMEOUT_MS, + }), ); + const shippedVersions = collectShippedVersions(); + const lockPackages = + JSON.parse(readFileSync(join(repoRoot, 'package-lock.json'), 'utf8')).packages ?? {}; + const result = evaluateShippedAudit(audit, shippedVersions, lockPackages); + if (result.outcome === 'unavailable') { + const message = `[audit-shipped] registry audit unavailable: ${result.detail}`; + if (allowUnavailable) { + console.warn(`${message}; continuing Product Nightly`); + return; + } + throw new Error(message); + } + + console.log( + `[audit-shipped] product shipped closure: ${shippedVersions.size} packages; ` + + `advisories reaching it at moderate or above: ${result.flagged.length}`, + ); + for (const { vulnerability, copies } of result.flagged) { + const causes = (vulnerability.via ?? []) + .map((via) => (typeof via === 'string' ? `via ${via}` : `${via.title} (${via.url})`)) + .join('; '); + console.error( + `[audit-shipped] ${vulnerability.name}@${copies.join(', ')}: ${vulnerability.severity} — ${causes}`, + ); + } + if (result.outcome === 'blocked') process.exitCode = 1; } -if (flagged.length > 0) process.exit(1); + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main(); diff --git a/scripts/audit-shipped-dependencies.test.mjs b/scripts/audit-shipped-dependencies.test.mjs new file mode 100644 index 0000000000..cb5c679c88 --- /dev/null +++ b/scripts/audit-shipped-dependencies.test.mjs @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { evaluateShippedAudit } from './audit-shipped-dependencies.mjs'; + +test('blocks a valid report that reaches a shipped dependency', () => { + const result = evaluateShippedAudit( + { + status: 1, + stdout: JSON.stringify({ + vulnerabilities: { + react: { + name: 'react', + severity: 'high', + nodes: ['node_modules/react'], + via: [], + }, + tooling: { + name: 'tooling', + severity: 'critical', + nodes: ['node_modules/tooling'], + via: [], + }, + }, + }), + }, + new Map([['react', new Set(['19.2.4'])]]), + { + 'node_modules/react': { version: '19.2.4' }, + 'node_modules/tooling': { version: '1.0.0' }, + }, + ); + + assert.equal(result.outcome, 'blocked'); + assert.deepEqual( + result.flagged.map(({ vulnerability }) => vulnerability.name), + ['react'], + ); +}); + +test('classifies an audit service error as unavailable', () => { + const result = evaluateShippedAudit( + { + status: 1, + stdout: JSON.stringify({ error: { summary: '', detail: '' } }), + stderr: 'npm warn audit network timeout', + error: new Error('npm audit timed out'), + }, + new Map(), + {}, + ); + + assert.equal(result.outcome, 'unavailable'); + assert.equal(result.detail, 'npm audit timed out'); +}); From c112a37b799a26c9745dc601bcce4143f6e29d9a Mon Sep 17 00:00:00 2001 From: AstroHan Date: Fri, 4 Sep 2026 13:42:55 +0800 Subject: [PATCH 4/9] refactor(release): fold Nightly audit into publisher Generated-by: Codex --- .github/workflows/npm-publication.yml | 29 ++++---------------------- scripts/audit-shipped-dependencies.mjs | 7 ++----- 2 files changed, 6 insertions(+), 30 deletions(-) diff --git a/.github/workflows/npm-publication.yml b/.github/workflows/npm-publication.yml index eceef00dc2..721cff4e31 100644 --- a/.github/workflows/npm-publication.yml +++ b/.github/workflows/npm-publication.yml @@ -88,36 +88,12 @@ jobs: cli: name: Validate npm Nightly - needs: [identity, audit] + needs: identity uses: ./.github/workflows/cli-package-validation.yml with: source_commit: ${{ needs.identity.outputs.source_commit }} package_version: ${{ needs.identity.outputs.version }} - audit: - name: Audit shipped Nightly dependencies - needs: identity - runs-on: ubuntu-24.04 - timeout-minutes: 15 - steps: - - name: Check out the exact Nightly source - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ needs.identity.outputs.source_commit }} - persist-credentials: false - - - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - cache: npm - - - name: Install dependency graph without an implicit audit - run: npm ci --ignore-scripts --no-audit - - - name: Block shipped advisories without blocking on audit availability - run: node scripts/audit-shipped-dependencies.mjs --allow-unavailable - publish: name: Publish npm Nightly needs: [identity, cli] @@ -149,6 +125,9 @@ jobs: - name: Install publisher dependencies run: npm ci --ignore-scripts --no-audit + - name: Block shipped advisories without blocking on audit availability + run: node scripts/audit-shipped-dependencies.mjs --allow-unavailable + - name: Select the publication npm toolchain run: npm install --global --no-audit --no-fund "$(node -p 'require("./package.json").packageManager')" diff --git a/scripts/audit-shipped-dependencies.mjs b/scripts/audit-shipped-dependencies.mjs index 5da78584cc..e3ee0a1619 100644 --- a/scripts/audit-shipped-dependencies.mjs +++ b/scripts/audit-shipped-dependencies.mjs @@ -57,11 +57,8 @@ function shippedCopies(vulnerability, shippedVersions, lockPackages) { } function unavailableDetail(audit, report) { - const registryError = report?.error; - if (registryError && JSON.stringify(registryError) !== '{}') { - const detail = JSON.stringify(registryError); - if (detail !== '{"summary":"","detail":""}') return detail; - } + const registryDetail = [report?.error?.summary, report?.error?.detail].filter(Boolean).join(': '); + if (registryDetail) return registryDetail; if (audit.error instanceof Error) return audit.error.message; if (typeof audit.stderr === 'string' && audit.stderr.trim()) return audit.stderr.trim(); return `npm audit exited with status ${audit.status ?? 'unknown'}`; From 1c83a97e90b40774c91420de6d64e0db9f82d5bd Mon Sep 17 00:00:00 2001 From: AstroHan Date: Fri, 4 Sep 2026 13:47:10 +0800 Subject: [PATCH 5/9] fix(ci): route audit regression to release contract Generated-by: Codex --- scripts/ci-test-plan.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/ci-test-plan.mjs b/scripts/ci-test-plan.mjs index a1acf8d6fe..7df650adf8 100644 --- a/scripts/ci-test-plan.mjs +++ b/scripts/ci-test-plan.mjs @@ -56,6 +56,7 @@ const RELEASE_CONTRACT_FILES = new Set([ '.github/workflows/release-linux-check.yml', '.github/workflows/release-windows-check.yml', '.github/workflows/windows-recovery.yml', + 'scripts/audit-shipped-dependencies.test.mjs', 'scripts/package-macos.mjs', 'scripts/package-macos-autoupdate-next.mjs', 'scripts/package-macos-arm64-cli.mjs', From d7b2c66d14191e5bd8e43ff96240a0a8a2e7ef3a Mon Sep 17 00:00:00 2001 From: AstroHan Date: Fri, 4 Sep 2026 14:24:00 +0800 Subject: [PATCH 6/9] test(ci): route shipped audit owner to release contract Generated-by: Codex --- scripts/ci-test-plan.mjs | 1 + scripts/ci-test-plan.test.mjs | 1 + 2 files changed, 2 insertions(+) diff --git a/scripts/ci-test-plan.mjs b/scripts/ci-test-plan.mjs index 7df650adf8..b958b4a13c 100644 --- a/scripts/ci-test-plan.mjs +++ b/scripts/ci-test-plan.mjs @@ -56,6 +56,7 @@ const RELEASE_CONTRACT_FILES = new Set([ '.github/workflows/release-linux-check.yml', '.github/workflows/release-windows-check.yml', '.github/workflows/windows-recovery.yml', + 'scripts/audit-shipped-dependencies.mjs', 'scripts/audit-shipped-dependencies.test.mjs', 'scripts/package-macos.mjs', 'scripts/package-macos-autoupdate-next.mjs', diff --git a/scripts/ci-test-plan.test.mjs b/scripts/ci-test-plan.test.mjs index 8bb342981c..7797abb776 100644 --- a/scripts/ci-test-plan.test.mjs +++ b/scripts/ci-test-plan.test.mjs @@ -223,6 +223,7 @@ test('release authority changes select their dedicated contract gate', () => { '.github/workflows/release-cli-finalize.yml', '.github/workflows/release-cli-stage.yml', '.github/workflows/release.yml', + 'scripts/audit-shipped-dependencies.mjs', 'scripts/package-macos.mjs', 'scripts/package-macos-autoupdate-next.mjs', 'scripts/package-macos-arm64-cli.mjs', From 2140f0c1eaf4d9bc014c80e3289efbb8ff1f2092 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Fri, 4 Sep 2026 15:21:13 +0800 Subject: [PATCH 7/9] refactor(release): remove duplicate registry audits Generated-by: Codex --- .github/workflows/dependency-audit.yml | 5 +---- .github/workflows/release.yml | 8 +------- scripts/audit-shipped-dependencies.mjs | 14 +++++++------- scripts/release-cli-package.mjs | 2 +- 4 files changed, 10 insertions(+), 19 deletions(-) diff --git a/.github/workflows/dependency-audit.yml b/.github/workflows/dependency-audit.yml index 9219469ff9..f96352537e 100644 --- a/.github/workflows/dependency-audit.yml +++ b/.github/workflows/dependency-audit.yml @@ -70,10 +70,7 @@ jobs: # the renderer roots, which npm labels dev even though they ship. run: npm ci --ignore-scripts - - name: Audit production dependencies - run: npm audit --omit=dev --audit-level=moderate - - - name: Audit shipped desktop closure + - name: Audit shipped product closure run: node scripts/audit-shipped-dependencies.mjs - name: Verify registry signatures diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c8e95038dc..6ab7bb3f58 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -141,10 +141,7 @@ jobs: - name: Install dependencies with the pinned npm run: npm ci - - name: Audit production dependencies - run: npm audit --omit=dev --audit-level=moderate - - - name: Audit shipped desktop closure + - name: Audit shipped product closure run: node scripts/audit-shipped-dependencies.mjs - name: Update stable Rust for Desktop native artifacts @@ -349,9 +346,6 @@ jobs: - name: Install dependencies with the pinned npm run: npm ci - - name: Audit production dependencies - run: npm audit --omit=dev --audit-level=moderate - - name: Build the CLI production workspace closure run: | npm --workspace maka-agent run build:workspace-deps diff --git a/scripts/audit-shipped-dependencies.mjs b/scripts/audit-shipped-dependencies.mjs index e3ee0a1619..a321b7b57e 100644 --- a/scripts/audit-shipped-dependencies.mjs +++ b/scripts/audit-shipped-dependencies.mjs @@ -19,13 +19,13 @@ // Audits what the product artifacts ship, not what npm labels production. // -// `npm audit --omit=dev` covers the Node production closure, but the renderer -// roots live in `devDependencies` (so electron-builder keeps their unread -// sources out of `app.asar`) while vite still bundles them into -// `dist-renderer`. A vulnerability in react would therefore ship without the -// production audit ever seeing it. This audits one full npm report and fails -// on anything that lands in the shipped CLI or Desktop closure — Node -// production plus everything reachable from the declared renderer roots. +// The renderer roots live in `devDependencies` (so electron-builder keeps +// their unread sources out of `app.asar`) while vite still bundles them into +// `dist-renderer`. An `npm audit --omit=dev` would therefore miss shipped +// vulnerabilities such as one in react. This is the product audit authority: +// one full npm report, filtered to anything that lands in the shipped CLI or +// Desktop closure — Node production plus everything reachable from the +// declared renderer roots. // // An advisory names a package and npm resolves it to the installed copies it // actually reaches (`nodes`). Only a copy whose exact version is in the diff --git a/scripts/release-cli-package.mjs b/scripts/release-cli-package.mjs index da8bfc2b11..43168ca281 100644 --- a/scripts/release-cli-package.mjs +++ b/scripts/release-cli-package.mjs @@ -133,7 +133,7 @@ function main() { ); } buildRuntimeWorkspaces({ clean: true }); - if (!nightlyVersion) checkProductionAudit(); + if (!nightlyVersion && !allowDirty) checkProductionAudit(); runNpm(['run', 'check:cli-third-party-notices']); runNpm(['run', 'check:runtime-host-peer-dependencies']); runNpm(['run', 'check:runtime-host-peer-notices']); From a793e2b471535d19375484b54dd830dd068b30d3 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Fri, 4 Sep 2026 15:39:47 +0800 Subject: [PATCH 8/9] fix(ci): tolerate audit service outages Generated-by: Codex --- .github/workflows/dependency-audit.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dependency-audit.yml b/.github/workflows/dependency-audit.yml index f96352537e..4ce01265ff 100644 --- a/.github/workflows/dependency-audit.yml +++ b/.github/workflows/dependency-audit.yml @@ -71,7 +71,9 @@ jobs: run: npm ci --ignore-scripts - name: Audit shipped product closure - run: node scripts/audit-shipped-dependencies.mjs + # A valid moderate+ advisory still fails. Registry availability must + # not lock every pull request out of the repository. + run: node scripts/audit-shipped-dependencies.mjs --allow-unavailable - name: Verify registry signatures # The full tree, not `--omit=dev`. Two reasons it has to be both: From 29d75247c67f7e4dd9380dfd1b0215f09c2f708e Mon Sep 17 00:00:00 2001 From: AstroHan Date: Fri, 4 Sep 2026 15:46:49 +0800 Subject: [PATCH 9/9] fix(audit): report unavailable result generically Generated-by: Codex --- scripts/audit-shipped-dependencies.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/audit-shipped-dependencies.mjs b/scripts/audit-shipped-dependencies.mjs index a321b7b57e..80b10ac100 100644 --- a/scripts/audit-shipped-dependencies.mjs +++ b/scripts/audit-shipped-dependencies.mjs @@ -125,7 +125,7 @@ function main() { if (result.outcome === 'unavailable') { const message = `[audit-shipped] registry audit unavailable: ${result.detail}`; if (allowUnavailable) { - console.warn(`${message}; continuing Product Nightly`); + console.warn(`${message}; continuing without an advisory result`); return; } throw new Error(message);