From f55ba6e6aa07b4184e3f4270bd97e0d151c09451 Mon Sep 17 00:00:00 2001 From: EmersonBraun Date: Wed, 7 Oct 2026 20:47:52 -0300 Subject: [PATCH] fix(ci): scope packed consumers to built packages --- .github/workflows/ci.yml | 6 +++++- CONTRIBUTING.md | 6 ++++++ scripts/check-packed-consumers.mjs | 16 +++++++++++++-- scripts/lib/packed-consumers.mjs | 22 ++++++++++++++++++++ scripts/packed-consumers.test.mjs | 32 ++++++++++++++++++++++++++++++ 5 files changed, 79 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3d2cdc11..1cf8282e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,12 +106,16 @@ jobs: - name: Build affected packages and dependents if: steps.package-scope.outputs.run_all == 'false' - run: pnpm exec turbo run build --filter='...[origin/main]' --concurrency=2 + run: | + pnpm exec turbo run build --filter='...[origin/main]' --dry=json > "$RUNNER_TEMP/packed-consumers-build-plan.json" + pnpm exec turbo run build --filter='...[origin/main]' --concurrency=2 - name: Publication metadata contract run: pnpm check:publication-surface - name: Packed consumer publication contract + env: + PACKED_CONSUMERS_BUILD_PLAN: ${{ steps.package-scope.outputs.run_all == 'false' && format('{0}/packed-consumers-build-plan.json', runner.temp) || '' }} run: pnpm check:packed-consumers - name: Published manifest workspace-protocol guard diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dd92d8867..726139718 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -128,6 +128,12 @@ ADR/RFC index sync, locale parity). `pnpm check:all` adds typecheck + build + test. Husky runs the commit-message check before each commit and the gates + typecheck + build before each push. +The packed-consumer gate checks every public package by default, including in +release builds. For an affected-only build, set `PACKED_CONSUMERS_BUILD_PLAN` +to the JSON file produced by `pnpm exec turbo run build --filter='...[origin/main]' --dry=json` +and use that same filter for the build. The gate checks every public package in +the task graph, including dependency builds; missing dist outputs still fail. + `@agentskit/core` is capped at **10KB gzipped** by [Manifesto principle 1](./MANIFESTO.md). The `size` workflow blocks PRs that exceed any package's budget. ### 5. Add a changeset (any user-facing change) diff --git a/scripts/check-packed-consumers.mjs b/scripts/check-packed-consumers.mjs index 9abb95a48..7ee187c03 100644 --- a/scripts/check-packed-consumers.mjs +++ b/scripts/check-packed-consumers.mjs @@ -10,6 +10,9 @@ * Prerequisite: package dist outputs must already exist * (`pnpm --filter "./packages/*" build`). * + * PACKED_CONSUMERS_BUILD_PLAN may point to `turbo run build --dry=json` + * output for the same filter used to build. Unset checks all public packages. + * * Never installs dependencies, never hits the registry, never executes bins. */ @@ -41,6 +44,7 @@ import { isPathInsidePackage, isUnsafeArchiveEntry, shouldTypecheckEntry, + selectPackedConsumerPackages, stripPackagePrefix, validateTarballEntries, } from './lib/packed-consumers.mjs' @@ -693,12 +697,20 @@ async function runTypecheckFixtures(consumerRoot, planEntries) { } async function main() { - const packages = await discoverPublicPackages() - if (packages.length === 0) { + const publicPackages = await discoverPublicPackages() + if (publicPackages.length === 0) { console.error('packed-consumer check: no public packages discovered under packages/') process.exit(1) } + const planPath = process.env.PACKED_CONSUMERS_BUILD_PLAN + const buildPlan = planPath ? JSON.parse(await readFile(planPath, 'utf8')) : undefined + const packages = selectPackedConsumerPackages(publicPackages, buildPlan) + if (packages.length === 0) { + console.log('packed-consumer check: no public packages in the build scope') + return + } + await assertBuildPrerequisite(packages) const workRoot = await mkdtemp(path.join(tmpdir(), 'agentskit-packed-consumers-')) diff --git a/scripts/lib/packed-consumers.mjs b/scripts/lib/packed-consumers.mjs index f042c9df6..09308ba28 100644 --- a/scripts/lib/packed-consumers.mjs +++ b/scripts/lib/packed-consumers.mjs @@ -15,6 +15,28 @@ import { toImportSpecifier, } from './packed-consumers-matrix.mjs' +/** + * Select public packages from Turbo's build task graph, including dependency builds. + * @template {{ packageName: string }} T + * @param {T[]} packages + * @param {{ tasks: { task: string, package: string }[] } | undefined} buildPlan + * @returns {T[]} + */ +export function selectPackedConsumerPackages(packages, buildPlan) { + if (buildPlan === undefined) return packages + if (!buildPlan || !Array.isArray(buildPlan.tasks)) { + throw new Error('packed-consumer scope requires a Turbo build plan with tasks') + } + const names = new Set() + for (const task of buildPlan.tasks) { + if (!task || task.task !== 'build' || typeof task.package !== 'string' || !task.package) { + throw new Error('packed-consumer scope contains an invalid build task') + } + names.add(task.package) + } + return packages.filter((pkg) => names.has(pkg.packageName)) +} + const DEPENDENCY_FIELDS = Object.freeze([ 'dependencies', 'optionalDependencies', diff --git a/scripts/packed-consumers.test.mjs b/scripts/packed-consumers.test.mjs index 4ea384dd6..fb47df8e9 100644 --- a/scripts/packed-consumers.test.mjs +++ b/scripts/packed-consumers.test.mjs @@ -20,6 +20,7 @@ import { isPathInsidePackage, isUnsafeArchiveEntry, shouldTypecheckEntry, + selectPackedConsumerPackages, stripPackagePrefix, validateTarballEntries, } from './lib/packed-consumers.mjs' @@ -349,3 +350,34 @@ describe('shebang and diagnostics', () => { ) }) }) + +describe('packed-consumer build scope', () => { + const packages = ['core', 'rag', 'cli', 'angular'].map((name) => ({ + packageName: `@agentskit/${name}`, + })) + const buildTask = (name) => ({ task: 'build', package: `@agentskit/${name}` }) + + test('checks every public package when no scope is supplied', () => { + assert.deepEqual(selectPackedConsumerPackages(packages), packages) + }) + + test('uses build tasks including dependencies, not only directly affected packages', () => { + assert.deepEqual(selectPackedConsumerPackages(packages, { + packages: ['@agentskit/rag', '@agentskit/cli'], + tasks: [buildTask('rag'), buildTask('cli'), buildTask('core'), buildTask('rag'), buildTask('docs-next')], + }), packages.slice(0, 3)) + }) + + test('allows an empty public scope for docs-only builds', () => { + assert.deepEqual(selectPackedConsumerPackages(packages, { tasks: [] }), []) + assert.deepEqual(selectPackedConsumerPackages(packages, { tasks: [buildTask('docs-next')] }), []) + }) + + test('rejects malformed plans instead of silently skipping validation', () => { + for (const plan of [null, {}, { tasks: null }, { tasks: [null] }, + { tasks: [{ task: 'test', package: '@agentskit/rag' }] }, + { tasks: [{ task: 'build' }] }, { tasks: [{ task: 'build', package: '' }] }]) { + assert.throws(() => selectPackedConsumerPackages(packages, plan), /scope/) + } + }) +})