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: 3 additions & 3 deletions .agents/skills/proof/release.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"format": 1,
"flatbreadVersion": "1.0.0",
"proofVersion": "1.0.0",
"gitTag": "v1.0.0"
"flatbreadVersion": "1.0.1",
"proofVersion": "1.0.1",
"gitTag": "v1.0.1"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
id: con-public-npm-releases-use-one-lockstep-version--0c4eg8frxys4fv2s
effort: eff-proof-and-contributor-operating-system--ahhgtafvdhg4dfve
title: Public npm releases use one lockstep version
kind: hard
created_at: '2026-08-15T23:14:13.605Z'
---

Every public package in the Flatbread monorepo must declare the same version before any package in a release is published. The bump command updates all public package manifests in one operation, and the publish preflight must reject missing or mixed versions before it calls npm publish.
30 changes: 15 additions & 15 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,14 @@ https://github.com/FlatbreadLabs/oven.

There are two steps:

1. Bump versions where there are changes
2. Publish the changed packages
1. Bump every public package to one version
2. Publish the release

### One-time note for 1.0
### Lockstep versions

The 1.0 release put every published package on `1.0.0` at once, rather than
letting each package carry its own alpha number. `pnpm bump` still works the
same way: it preselects the packages that changed plus their workspace
dependents. Only bump every package together again when a release calls for it.
Every public package shares one version. A release bumps the whole set even
when only one package changed. This keeps package combinations, the Proof skill
manifest, and the Git tag tied to one release.

`packages/proof/skills/proof/release.json` records the version an
end user installs. Edit that file, not the copy in `.agents/`. Then run
Expand All @@ -107,7 +106,7 @@ which fails unless `flatbreadVersion` and `proofVersion` match the current
`package.json` versions and `gitTag` equals `v<flatbreadVersion>`. `pnpm verify`
runs both checks.

### 1) Bump versions only where there are changes
### 1) Bump every public package

Use the interactive bump script:

Expand All @@ -117,22 +116,22 @@ pnpm bump

What the script does:

- Detects changes since last publish per package by:
- Detects whether any public package changed since the last publish by:
- Querying npm for the package's latest published version and its publish time
- Comparing git commits in `packages/<name>` since that time
- Ignoring commits that only change the `version` field in `package.json`
- Skipping packages that are not yet published on npm
- Preselects changed packages and their workspace dependents for you to bump
- Required workspace dependents must remain selected when a changed dependency is selected
- Runs `pnpm bumpp --no-commit --no-push --no-tag` in each selected package directory
- Passes every public package manifest to one `bumpp` command so one chosen
version is written across the set
- Stops before publishing if any public package version differs

Notes:

- Commit the version bumps after the script completes. For example:

```bash
git add packages/**/package.json
git commit -m "release: bump versions for changed packages"
git commit -m "release: bump public packages"
```

- Debugging: set `FLATBREAD_BUMP_DEBUG=1` to see detection details
Expand All @@ -141,7 +140,7 @@ Notes:
FLATBREAD_BUMP_DEBUG=1 pnpm bump
```

- New (unpublished) packages: these are excluded from the bump prompt. Ensure their `package.json` has the desired starting version before publishing (see below).
- New public packages join the same version as the rest of the release set.

### 2) Publish packages

Expand All @@ -162,7 +161,8 @@ skill files and package contents first. It then publishes ordinary packages,
`@flatbread/proof`, and finally `flatbread`, stopping at the first
failure.

Publish all public packages (the script builds first and then attempts to publish each package):
Publish all public packages (the script checks for one shared version, builds,
then attempts to publish each package):

```bash
pnpm publish:ci
Expand Down
2 changes: 1 addition & 1 deletion packages/codegen/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@flatbread/codegen",
"version": "1.0.0",
"version": "1.0.1",
"description": "Generates TypeScript types, typed document nodes, and a prototype read API from a Flatbread content model.",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@flatbread/config",
"version": "1.0.0",
"version": "1.0.1",
"description": "Loads, validates, and types flatbread.config.* files. Provides defineConfig().",
"type": "module",
"publishConfig": {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@flatbread/core",
"version": "1.0.0",
"version": "1.0.1",
"description": "Flatbread's engine: runs source and transformer plugins, validates records and refs, and builds the GraphQL schema plus JSON and CSV snapshots.",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/explorer/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@flatbread/explorer",
"version": "1.0.0",
"version": "1.0.1",
"description": "Single-page app for browsing a Flatbread content graph, served by flatbread start. Ships an Proof preset.",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/flatbread/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "flatbread",
"version": "1.0.0",
"version": "1.0.1",
"description": "Git-native memory for coding agents and relational content for TypeScript apps. Files in your repo become a typed graph you read over GraphQL, generated TypeScript, or the CLI.",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/proof/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@flatbread/proof",
"version": "1.0.0",
"version": "1.0.1",
"description": "Git-native memory records for coding agents. Writes typed Effort, Issue, Finding, Decision, Constraint, Risk, Citation, and Blob records to Markdown as journaled changes, and reads them back in bounded pages.",
"type": "module",
"scripts": {
Expand Down
6 changes: 3 additions & 3 deletions packages/proof/skills/proof/release.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"format": 1,
"flatbreadVersion": "1.0.0",
"proofVersion": "1.0.0",
"gitTag": "v1.0.0"
"flatbreadVersion": "1.0.1",
"proofVersion": "1.0.1",
"gitTag": "v1.0.1"
}
2 changes: 1 addition & 1 deletion packages/resolver-svimg/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@flatbread/resolver-svimg",
"version": "1.0.0",
"version": "1.0.1",
"description": "Flatbread field resolver that turns an image path into svimg attributes: responsive srcsets, WebP and AVIF variants, and a blurred placeholder.",
"type": "module",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/source-filesystem/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@flatbread/source-filesystem",
"version": "1.0.0",
"version": "1.0.1",
"description": "Flatbread source plugin that reads content files from directories on disk.",
"type": "module",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/transformer-markdown/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@flatbread/transformer-markdown",
"version": "1.0.0",
"version": "1.0.1",
"description": "Flatbread transformer plugin that parses Markdown frontmatter and body into content records.",
"type": "module",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/transformer-yaml/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@flatbread/transformer-yaml",
"version": "1.0.0",
"version": "1.0.1",
"description": "Flatbread transformer plugin that parses YAML files into content records.",
"type": "module",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@flatbread/utils",
"version": "1.0.0",
"version": "1.0.1",
"description": "Shared helpers for Flatbread packages, including package manager and lockfile detection.",
"type": "module",
"scripts": {
Expand Down
59 changes: 24 additions & 35 deletions scripts/bumpVersions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { execSync } from 'child_process';
import { execFileSync, execSync } from 'child_process';
import { promises as fs } from 'node:fs';
import inquirer from 'inquirer';
import colors from 'kleur';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
Expand Down Expand Up @@ -318,40 +317,30 @@ async function main(): Promise<void> {
return;
}

const { selectedPackages }: Record<string, PathedFlatbreadPackage[]> =
await inquirer.prompt([
{
type: 'checkbox',
name: 'selectedPackages',
message:
'Packages changed since last publish (including workspace dependents). Select which to bump:',
choices: changedPackages.map((pkg) => ({
name: `${getPkgName(pkg)}`,
value: pkg,
checked: true,
})),
},
]);
const missingDependents = validateBumpSelection(
allPackages,
changedPackages.map(getPkgName),
selectedPackages.map(getPkgName)
console.log(
colors
.bold()
.yellow(
`Changes detected in: ${changedPackages.map(getPkgName).join(', ')}`
)
);
console.log(
colors.bold().yellow('Bumping every public package as one release')
);
execFileSync(
'pnpm',
[
'exec',
'bumpp',
'--no-commit',
'--no-push',
'--no-tag',
...allPackages.map((pkg) =>
path.join('packages', pkg.dirName, 'package.json')
),
],
{ stdio: 'inherit' }
);
if (missingDependents.length > 0) {
throw new Error(
`Cannot deselect required workspace dependents: ${missingDependents.join(
', '
)}`
);
}

for (const selectedPackage of selectedPackages) {
console.log(colors.bold(colors.yellow(`Bumping ${selectedPackage.name}`)));
execSync('pnpm bumpp --no-commit --no-push --no-tag', {
stdio: 'inherit',
cwd: path.resolve(path.join('packages', selectedPackage.dirName)),
});
}

const flatbreadManifest = JSON.parse(
await fs.readFile('packages/flatbread/package.json', 'utf8')
Expand Down
34 changes: 34 additions & 0 deletions scripts/publish.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import test from 'ava';
import {
assertLockstepVersions,
classifyNpmViewResult,
parseNpmViewVersion,
sortPackages,
Expand Down Expand Up @@ -56,6 +57,39 @@ test('publish ordering rejects local dependency cycles', (t) => {
t.regex(error?.message ?? '', /@flatbread\/a/);
});

test('publish preflight accepts one shared public package version', (t) => {
t.is(
assertLockstepVersions([
{ name: '@flatbread/core', dirName: 'core', version: '1.0.1' },
{ name: 'flatbread', dirName: 'flatbread', version: '1.0.1' },
]),
'1.0.1'
);
});

test('publish preflight rejects fragmented public package versions', (t) => {
const error = t.throws(() =>
assertLockstepVersions([
{ name: '@flatbread/core', dirName: 'core', version: '1.0.0' },
{ name: 'flatbread', dirName: 'flatbread', version: '1.0.1' },
])
);
t.regex(error?.message ?? '', /one version across every public package/);
t.regex(error?.message ?? '', /1\.0\.0: @flatbread\/core/);
t.regex(error?.message ?? '', /1\.0\.1: flatbread/);
});

test('publish preflight rejects a public package without a version', (t) => {
const error = t.throws(() =>
assertLockstepVersions([
{ name: '@flatbread/core', dirName: 'core' },
{ name: 'flatbread', dirName: 'flatbread', version: '1.0.1' },
])
);
t.regex(error?.message ?? '', /must declare a version/);
t.regex(error?.message ?? '', /@flatbread\/core/);
});

test('npm view preflight recognizes an exact published version', (t) => {
t.is(parseNpmViewVersion('"1.0.0-alpha.1"\n'), '1.0.0-alpha.1');
t.is(
Expand Down
54 changes: 49 additions & 5 deletions scripts/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type NpmViewResult = {

export type PreflightStatus = 'publish' | 'already-published';

type PublishPackage = {
export type PublishPackage = {
name: string;
dirName: string;
version?: string;
Expand Down Expand Up @@ -130,6 +130,46 @@ export function sortPackages<T extends PublishPackage>(packages: T[]): T[] {
return result;
}

export function assertLockstepVersions(
packages: readonly PublishPackage[]
): string {
const missingVersions = packages
.filter((pkg) => !pkg.version)
.map((pkg) => pkg.name)
.sort();
if (missingVersions.length > 0) {
throw new Error(
`Every public package must declare a version. Missing: ${missingVersions.join(
', '
)}`
);
}

const versions = new Map<string, string[]>();
for (const pkg of packages) {
const names = versions.get(pkg.version!) ?? [];
names.push(pkg.name);
versions.set(pkg.version!, names);
}
if (versions.size !== 1) {
const groups = [...versions.entries()]
.sort(([left], [right]) => left.localeCompare(right))
.map(
([version, names]) =>
`${version}: ${names
.sort((left, right) => left.localeCompare(right))
.join(', ')}`
);
throw new Error(
`Release requires one version across every public package:\n${groups.join(
'\n'
)}`
);
}

return versions.keys().next().value!;
}

function comparePackageNames(
left: PublishPackage,
right: PublishPackage
Expand Down Expand Up @@ -183,13 +223,17 @@ export function preflightPackage(

export async function publishPackages(): Promise<void> {
const releaseSha = assertCleanRelease();
execSync('pnpm run build', { stdio: 'inherit' });
execSync('pnpm run skills:check', { stdio: 'inherit' });
execSync('pnpm run skills:pack-check', { stdio: 'inherit' });

const packages = sortPackages(
(await getMonorepoPublicPackages()) as unknown as PublishPackage[]
);
const releaseVersion = assertLockstepVersions(packages);
console.log(
colors.bold().green(`Public package release version: ${releaseVersion}`)
);

execSync('pnpm run build', { stdio: 'inherit' });
execSync('pnpm run skills:check', { stdio: 'inherit' });
execSync('pnpm run skills:pack-check', { stdio: 'inherit' });

for (const { dirName, name, version } of packages) {
try {
Expand Down
Loading