diff --git a/internal/landing-preconditions.mjs b/internal/landing-preconditions.mjs index 96c5cea91..7af77b344 100644 --- a/internal/landing-preconditions.mjs +++ b/internal/landing-preconditions.mjs @@ -31,6 +31,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' import { resolve, dirname } from 'node:path' import { fileURLToPath } from 'node:url' import { TREE_NAME } from './_tree-name.mjs' +import { bundleGraph, areasForBundle } from './i18n-areas.mjs' const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const refuse = (why, detail = '') => { @@ -106,10 +107,31 @@ if (process.env.DOXA_SKIP_FRESHNESS === '1') { // every composite, plus each bundle whose own folder the landing touched. const base = run('git', ['merge-base', 'HEAD', `origin/${process.env.MAIN_CAPTURE_BRANCH || 'main'}`]).stdout.trim() const changed = base ? run('git', ['diff', '--name-only', base, 'HEAD']).stdout.split('\n').filter(Boolean) : [] - const shared = !base || changed.some((f) => /^(library\/|vite\.config\.js$|package\.json$|bun\.lockb?$|vite\.doxa-maps-build-config\.json$)/.test(f)) - const own = [...new Set(changed.map((f) => (f.match(/^plugin-designs\/([^/]+)\//) || [])[1]).filter(Boolean))] - const scope = shared ? ['--all'] : (own.length ? ['--bundles', own.join(',')] : []) - console.log(`LANDING-PRECONDITIONS: 4 fresh — scope: ${shared ? 'EVERY bundle (shared code changed' + (base ? '' : '; no merge-base') + ')' : 'composites' + (own.length ? ' + ' + own.join(', ') : '')}`) + // ★ A library/ change no longer means EVERY bundle. It means every bundle whose SOURCE GRAPH contains a + // changed file, or that reads a changed i18n area: measured, a one-file library fix reached 1 of 24 + // bundles, and rebuilding all 24 cost ~10 min per landing round. Only the build itself (config, + // dependencies, the tree generator) can reach every bundle, so only that still means --all. + const pkgDepsChanged = () => { + if (!changed.includes('package.json') || !base) return false + const deps = (ref) => { try { const j = JSON.parse(run('git', ['show', `${ref}:package.json`]).stdout); return JSON.stringify([j.dependencies, j.devDependencies]) } catch { return '' } } + return deps(base) !== deps('HEAD') + } + const everything = !base || pkgDepsChanged() || changed.some((f) => /^(vite\.config\.js$|bun\.lockb?$|vite\.doxa-maps-build-config\.json$)/.test(f)) + const libChanged = changed.filter((f) => f.startsWith('library/')) + const shared = everything || libChanged.length > 0 + const own = new Set(changed.map((f) => (f.match(/^plugin-designs\/([^/]+)\//) || [])[1]).filter(Boolean)) + if (!everything && libChanged.length) { + const libSet = new Set(libChanged) + const areas = new Set(libChanged.map((f) => (f.match(/^library\/i18n\/locales\/[^/]+\/([^/]+)\.json$/) || [])[1]).filter(Boolean)) + const manifest = JSON.parse(readFileSync(resolve(ROOT, 'public', TREE_NAME, 'manifest.json'), 'utf8')) + for (const { bundle } of manifest.bundles) { + if (!existsSync(resolve(ROOT, 'plugin-designs', bundle, 'index.js'))) continue + const graph = bundleGraph(ROOT, bundle).map((f) => f.slice(ROOT.length + 1)) + if (graph.some((f) => libSet.has(f)) || (areas.size && areasForBundle(ROOT, bundle).areas.some((a) => areas.has(a)))) own.add(bundle) + } + } + const scope = everything ? ['--all'] : (own.size ? ['--bundles', [...own].sort().join(',')] : []) + console.log(`LANDING-PRECONDITIONS: 4 fresh — scope: ${everything ? 'EVERY bundle (the build itself changed' + (base ? '' : '; no merge-base') + ')' : (shared ? 'the bundles this library change reaches' : 'composites') + (own.size ? ': ' + [...own].sort().join(', ') : '')}`) // ★ ONE PASS PER TREE, NOT ONE PER SCAN. This check rebuilds every bundle in its scope (~10 min for // --all), and it used to run INSIDE the merge-queue lock, so a build-owner landing held main for 30+ min // while every other landing queued (2026-09-25). The verdict is a function of the committed tree and the