diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f41e7e..99faf4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,10 @@ jobs: # Installs from the committed lockfile, so CI matches local installs. - run: npm ci + # Every package.json must declare the same version — releases are tagged + # from it, and a mismatch fails the release halfway through. + - run: npm run check:versions + - run: npm run lint # Must precede the tests: the examples workspace resolves the testing diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 41d3d5e..532021e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,7 +49,10 @@ jobs: - run: npm ci - # Guard: the tag must match the version both packages declare, so the + # Guard 1: every package.json agrees with every other one. + - run: npm run check:versions + + # Guard 2: the tag must match the version both packages declare, so the # release name can never disagree with the tarballs attached to it. - name: Check package versions match the tag run: | diff --git a/CLAUDE.md b/CLAUDE.md index 55853d8..e1d47b5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,6 +35,7 @@ npm workspaces monorepo: - `npm run build` — tsup builds both packages **in dependency order**, which the root script spells out explicitly (`-w testing && -w fixture-generator`). Don't switch it back to `--workspaces`: npm runs workspaces in listed/glob order, not topological order, and the fixture generator's declaration build imports `FixtureData` from the testing package's built `dist/index.d.ts`. Building out of order fails with `TS2307: Cannot find module '@usdr/airtable-interface-testing'` — only on clean checkouts (CI), since a stale local `dist/` hides it. Build before running the testing package's dist smoke test or the CLI. - `npm test` — every workspace's Jest suite. All suites must pass before committing. - `npm run pack:release` — builds, then packs both tarballs into `release/` (gitignored). Always pack through this script: `npm pack` alone ships whatever stale `dist/` happens to be on disk. +- `npm run check:versions` — asserts every `package.json` (root, both packages, the example) declares the same version. Runs in CI before lint and again in the release workflow; a drift here is what breaks a release mid-flight. Bump with `npm version --workspaces --include-workspace-root --no-git-tag-version` (it refuses when some packages already match — bump those with `-w `). - `npm run lint` — ESLint over all TypeScript sources (flat config in `eslint.config.mjs`; `.js`/`.cjs`/`.mjs` tool configs and generated `fixtures/` are ignored). - **Build before test.** `npm test` requires a prior `npm run build`: the examples workspace resolves the testing package's Jest preset out of `dist/`, and the dist smoke test needs the built artifact. CI (`.github/workflows/ci.yml`) runs lint → build → test in that order on every PR and on pushes to `main`, across Node 20.19 and 22. - `npx tsc` inside a workspace — type check (no emit). @@ -66,6 +67,8 @@ Security rules adapted from [TikiTribe/claude-secure-coding-rules](https://githu ## Work log +- **2026-08-01** — All four `package.json` files bumped to 0.2.0 and kept in lockstep from now on: `scripts/check-versions.mjs` (root script `check:versions`) fails when any two disagree, wired into ci.yml before lint and release.yml before the tag guard. A `testing` 0.2.0 / `fixture-generator` 0.1.0 split is what broke the first release. Release-URL examples in all four docs moved to v0.2.0; the earlier "root stays at 0.0.0" guidance is retired. + - **2026-07-23** — Switched the pinned SDK from `interface-alpha-next` to `interface-alpha` (what Airtable's templates install). Required a real fix: `interface-alpha` doesn't export `./package.json`, so `sdk_internals.ts` gained `resolveSdkRoot()` with an entry-point fallback. Full suite verified green against both dist-tags. - **2026-07-23** — Releases now trigger on merge: `.github/workflows/tag-on-merge.yml` tags `main` with the version from `packages/testing/package.json` (no-op when that tag exists) and invokes `release.yml`, which gained a `workflow_call` trigger. Documented the GITHUB_TOKEN-doesn't-trigger-workflows constraint that forces the direct call. diff --git a/README.md b/README.md index 910537a..9b0343b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ You get a `TestDriver` with the same shape as the v1 library — fixture data in These packages aren't on npm yet — we attach tarballs to [GitHub Releases](https://github.com/usdigitalresponse/airtable-interface-extension-testing/releases) instead. Open the latest release, copy the link to the `.tgz` you want under **Assets**, and hand it to npm: ```bash -npm install --save-dev https://github.com/usdigitalresponse/airtable-interface-extension-testing/releases/download/v0.1.0/usdr-airtable-interface-testing-0.1.0.tgz +npm install --save-dev https://github.com/usdigitalresponse/airtable-interface-extension-testing/releases/download/v0.2.0/usdr-airtable-interface-testing-0.2.0.tgz ``` Release assets always follow the same shape, so you can bump the version in that URL directly: @@ -61,13 +61,15 @@ A husky pre-commit hook runs `npm run lint` and the test suite before each commi Releasing is a side effect of merging. Bump the `version` in both `packages/*/package.json` as part of your PR, and when it merges to `main` the [tag-on-merge workflow](.github/workflows/tag-on-merge.yml) tags the merge commit with that version and kicks off the release. Merges that don't touch the version do nothing — the tag already exists, so the workflow stops there. ```bash -npm version 0.2.0 -w @usdr/airtable-interface-testing -w @usdr/airtable-interface-testing-fixtures --no-git-tag-version +npm version 0.3.0 --workspaces --include-workspace-root --no-git-tag-version ``` -That bumps both released packages — and leaves the private example alone — so you can commit the change in your PR and merge. +**Every `package.json` in the repo must carry the same version** — the root, both packages, and the example. CI fails the build if they drift, because a mismatch is what breaks a release halfway through. Run `npm run check:versions` locally to check. + +If some packages are already at the target version, `npm version` refuses to run ("Version not changed"); bump the stragglers individually with `-w `. The [release workflow](.github/workflows/release.yml) then verifies the tag matches both package versions, runs lint, build, and the full test suite, packs both tarballs, and publishes a GitHub Release with generated notes and the tarballs attached. If anything fails, nothing is published. -**Note:** the version in `packages/testing/package.json` is what drives the tag. The root `package.json` is private, stays at `0.0.0`, and is never released. +**Note:** the version in `packages/testing/package.json` is what drives the tag. The root and example packages are private and never published, but they carry the same version so the check above can enforce one number across the repo. You can still release by hand — push a `v*` tag yourself, or run the release workflow from the Actions tab against an existing tag. To build the tarballs locally without releasing, run `npm run pack:release` and look in `release/`. diff --git a/docs/getting-started.md b/docs/getting-started.md index 3fa8086..799fa0d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -13,7 +13,7 @@ For a live example, check out the [Example extension](../examples/todo-list). This package isn't on npm yet — we publish tarballs on [GitHub Releases](https://github.com/usdigitalresponse/airtable-interface-extension-testing/releases) instead. After setting up your Airtable project, install the latest release: ```bash -npm install --save-dev https://github.com/usdigitalresponse/airtable-interface-extension-testing/releases/download/v0.1.0/usdr-airtable-interface-testing-0.1.0.tgz +npm install --save-dev https://github.com/usdigitalresponse/airtable-interface-extension-testing/releases/download/v0.2.0/usdr-airtable-interface-testing-0.2.0.tgz ``` **Finding the URL ---** open the [releases page](https://github.com/usdigitalresponse/airtable-interface-extension-testing/releases), pick a release, and look under **Assets**. Copy the link address of `usdr-airtable-interface-testing-.tgz` — that's the URL you pass to `npm install`. The URLs always follow the same pattern, so you can also just edit the version in the command above: diff --git a/examples/todo-list/package.json b/examples/todo-list/package.json index 9440604..551fb88 100644 --- a/examples/todo-list/package.json +++ b/examples/todo-list/package.json @@ -1,7 +1,7 @@ { "name": "@usdr/example-todo-list", "private": true, - "version": "0.1.0", + "version": "0.2.0", "description": "Example Airtable interface extension with an automated test suite using @usdr/airtable-interface-testing", "type": "module", "scripts": { diff --git a/package-lock.json b/package-lock.json index fc6eb62..673be5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "airtable-interface-extension-testing", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "airtable-interface-extension-testing", - "version": "0.1.0", + "version": "0.2.0", "workspaces": [ "packages/*", "examples/*" @@ -25,7 +25,7 @@ }, "examples/todo-list": { "name": "@usdr/example-todo-list", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "@airtable/blocks": "interface-alpha", "react": "^19.1.0", @@ -9872,7 +9872,7 @@ }, "packages/fixture-generator": { "name": "@usdr/airtable-interface-testing-fixtures", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "@inquirer/prompts": "^7.0.0" }, @@ -9897,7 +9897,7 @@ }, "packages/testing": { "name": "@usdr/airtable-interface-testing", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "@babel/core": "^7.26.0", "@babel/preset-env": "^7.26.0", diff --git a/package.json b/package.json index c09ffed..0b888dd 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "test": "npm test --workspaces --if-present", "build": "npm run build -w @usdr/airtable-interface-testing && npm run build -w @usdr/airtable-interface-testing-fixtures", "lint": "eslint .", + "check:versions": "node scripts/check-versions.mjs", "pack:release": "npm run build && node -e \"require('fs').mkdirSync('release',{recursive:true})\" && npm pack -w @usdr/airtable-interface-testing -w @usdr/airtable-interface-testing-fixtures --pack-destination release", "prepare": "husky" }, diff --git a/packages/fixture-generator/README.md b/packages/fixture-generator/README.md index 6ee4f40..aa326ae 100644 --- a/packages/fixture-generator/README.md +++ b/packages/fixture-generator/README.md @@ -7,7 +7,7 @@ Generate [`FixtureData`](../testing/README.md#writing-fixture-data) for interfac Inside this repo the CLI is already linked, so `npx airtable-testing-fixtures` works from the repo root. From another project, install the tarball attached to a [GitHub Release](https://github.com/usdigitalresponse/airtable-interface-extension-testing/releases) — copy the link to `usdr-airtable-interface-testing-fixtures-.tgz` under **Assets**: ```bash -npm install --save-dev https://github.com/usdigitalresponse/airtable-interface-extension-testing/releases/download/v0.1.0/usdr-airtable-interface-testing-fixtures-0.1.0.tgz +npm install --save-dev https://github.com/usdigitalresponse/airtable-interface-extension-testing/releases/download/v0.2.0/usdr-airtable-interface-testing-fixtures-0.2.0.tgz ``` ## Usage diff --git a/packages/fixture-generator/package.json b/packages/fixture-generator/package.json index de7db02..877213a 100644 --- a/packages/fixture-generator/package.json +++ b/packages/fixture-generator/package.json @@ -1,6 +1,6 @@ { "name": "@usdr/airtable-interface-testing-fixtures", - "version": "0.1.0", + "version": "0.2.0", "description": "Generate test fixture data for Airtable interface extensions from a real base via the Airtable REST API", "type": "module", "bin": { diff --git a/packages/testing/README.md b/packages/testing/README.md index bdd0c9e..7dcd952 100644 --- a/packages/testing/README.md +++ b/packages/testing/README.md @@ -27,7 +27,7 @@ The [example extension's test suite](../../examples/todo-list/test/app.test.tsx) This package isn't published to npm. Install the tarball from a [GitHub Release](https://github.com/usdigitalresponse/airtable-interface-extension-testing/releases): ```bash -npm install --save-dev https://github.com/usdigitalresponse/airtable-interface-extension-testing/releases/download/v0.1.0/usdr-airtable-interface-testing-0.1.0.tgz @airtable/blocks@interface-alpha +npm install --save-dev https://github.com/usdigitalresponse/airtable-interface-extension-testing/releases/download/v0.2.0/usdr-airtable-interface-testing-0.2.0.tgz @airtable/blocks@interface-alpha ``` **Finding the URL ---** on the [releases page](https://github.com/usdigitalresponse/airtable-interface-extension-testing/releases), open a release and copy the link to `usdr-airtable-interface-testing-.tgz` under **Assets**. Every release follows the same URL shape, so bumping the version in the command above works too: diff --git a/scripts/check-versions.mjs b/scripts/check-versions.mjs new file mode 100644 index 0000000..b41cb2e --- /dev/null +++ b/scripts/check-versions.mjs @@ -0,0 +1,74 @@ +#!/usr/bin/env node +/** + * Fails if the workspaces disagree about the version. + * + * Releases are driven by the version in package.json, and a mismatch between + * packages produces tarballs whose names disagree with the tag — which is how + * a release fails halfway through. Catching it in CI keeps that off main. + * + * Workspaces are discovered from the root package.json, so a new package is + * covered without touching this script. + */ +import {existsSync, readFileSync, readdirSync} from 'node:fs'; +import * as path from 'node:path'; +import {fileURLToPath} from 'node:url'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +function readPackageJson(file) { + return JSON.parse(readFileSync(file, 'utf8')); +} + +/** Expand the `dir/*` and plain `dir` workspace patterns we use. */ +function resolveWorkspaceDirs(patterns) { + const dirs = []; + for (const pattern of patterns) { + if (pattern.endsWith('/*')) { + const parent = path.join(repoRoot, pattern.slice(0, -2)); + if (!existsSync(parent)) { + continue; + } + for (const entry of readdirSync(parent, {withFileTypes: true})) { + if (entry.isDirectory()) { + dirs.push(path.join(parent, entry.name)); + } + } + } else { + dirs.push(path.join(repoRoot, pattern)); + } + } + return dirs; +} + +const rootFile = path.join(repoRoot, 'package.json'); +const rootPackage = readPackageJson(rootFile); + +const packageFiles = [rootFile]; +for (const dir of resolveWorkspaceDirs(rootPackage.workspaces ?? [])) { + const file = path.join(dir, 'package.json'); + if (existsSync(file)) { + packageFiles.push(file); + } +} + +const found = packageFiles.map((file) => { + const {name, version} = readPackageJson(file); + return {file: path.relative(repoRoot, file), name, version}; +}); + +const versions = new Set(found.map((entry) => entry.version)); + +if (versions.size > 1) { + console.error('Package versions disagree:\n'); + for (const {file, name, version} of found) { + console.error(` ${version.padEnd(12)} ${name} (${file})`); + } + console.error( + '\nEvery package.json in this repo must declare the same version — the ' + + 'release is tagged from it. Bump them together, e.g.:\n' + + '\n npm version --workspaces --include-workspace-root --no-git-tag-version\n', + ); + process.exit(1); +} + +console.log(`All ${found.length} package.json files agree on version ${[...versions][0]}.`);