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
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 14 additions & 2 deletions scripts/check-packed-consumers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/

Expand Down Expand Up @@ -41,6 +44,7 @@ import {
isPathInsidePackage,
isUnsafeArchiveEntry,
shouldTypecheckEntry,
selectPackedConsumerPackages,
stripPackagePrefix,
validateTarballEntries,
} from './lib/packed-consumers.mjs'
Expand Down Expand Up @@ -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-'))
Expand Down
22 changes: 22 additions & 0 deletions scripts/lib/packed-consumers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
32 changes: 32 additions & 0 deletions scripts/packed-consumers.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
isPathInsidePackage,
isUnsafeArchiveEntry,
shouldTypecheckEntry,
selectPackedConsumerPackages,
stripPackagePrefix,
validateTarballEntries,
} from './lib/packed-consumers.mjs'
Expand Down Expand Up @@ -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/)
}
})
})
Loading