From 0e4afa499a459caa2113f6eb6cf4e5c74b5cf8d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 4 Jun 2026 15:50:05 +0000 Subject: [PATCH 1/7] ci: run identical checks on PRs and main to keep them in lockstep PRs were validated by CircleCI (lint + ci:build + test) while main was validated by the GitHub Actions release workflow (ci:build only, then changesets). Because the changeset version/release step only ever ran on main, a PR could go green and then break on merge -- e.g. #247 left a changeset pointing at the deleted react-native-mlkit-docs package, which passed PR CI but failed `changeset version` on main. - Add .github/workflows/ci.yml: a single `verify` job (install, lint, changeset status, ci:build, test) that runs on every pull_request and is exposed via workflow_call. - release.yml now calls that workflow as a `verify` job and gates the version/release job behind it (`needs: verify`), so main runs the exact same checks as PRs before anything is versioned or published. - `changeset status` catches changesets that reference missing/renamed packages at PR time (verified: it fails on a docs-package reference). - Drop the now-duplicate test_and_build job from CircleCI; CircleCI keeps only docs publishing, which depends on its orb + ir-docs SSH access. Closes #250 https://claude.ai/code/session_01HWaRWqpMHzzeTSJeqTeV75 --- .circleci/config.yml | 43 +++--------------------------- .github/workflows/ci.yml | 49 +++++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 8 ++++++ 3 files changed, 61 insertions(+), 39 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.circleci/config.yml b/.circleci/config.yml index c1e6cfff..1b21a61f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,44 +10,10 @@ parameters: orbs: publish-docs: infinitered/publish-docs@0.4 -# Docker defaults -defaults: &defaults - docker: - - image: cimg/node:18.16.1 - working_directory: /mnt/ramdisk/repo - -# Jobs -jobs: - test_and_build: - <<: *defaults - steps: - - checkout - # Restore Yarn cache (Yarn 3 uses .yarn/cache directory) - - restore_cache: - name: Restore Yarn Cache - keys: - - yarn-cache-{{ checksum "yarn.lock" }} - - run: - name: Use local Yarn version - command: echo "export PATH=$(pwd)/.yarn/releases:$PATH" >> $BASH_ENV - - run: - name: Install Dependencies - command: yarn install --immutable - # Save Yarn cache - - save_cache: - name: Save Yarn Cache - key: yarn-cache-{{ checksum "yarn.lock" }} - paths: - - .yarn/cache - - run: - name: Lint code - command: yarn lint - - run: - name: Build modules and packages - command: yarn ci:build - - run: - name: Run tests - command: yarn test +# Lint/build/test now live in GitHub Actions (.github/workflows/ci.yml) so that +# pull requests and `main` run the exact same checks. CircleCI is kept solely +# for docs publishing, which relies on the Infinite Red publish-docs orb and +# CircleCI-managed SSH access to the ir-docs repo. # Publishing docs details publish-details: &publish-details @@ -67,7 +33,6 @@ workflows: version: 2 build-and-test: jobs: - - test_and_build - publish-docs/build_docs: <<: *publish-details filters: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..a1ef325f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,49 @@ +name: CI + +# Single source of truth for the checks that gate the repo. +# +# This workflow runs on every pull request AND is called by the release +# workflow (via `workflow_call`) before anything is versioned or published. +# Running the exact same `verify` job in both places keeps PR and `main` +# checks in lockstep, so a PR can't go green and then break on merge. + +on: + pull_request: + workflow_call: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + verify: + name: Lint, validate changesets, build, and test + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + with: + # Full history so `changeset status` can diff against the base branch. + fetch-depth: 0 + + - name: Setup Node.js 18.x + uses: actions/setup-node@v4 + with: + node-version: 18.x + + - name: Install Dependencies + run: yarn install --immutable + + - name: Lint code + run: yarn lint + + # Catches changesets that reference packages that no longer exist (or were + # renamed) before they reach `main`, where `changeset version` would fail. + - name: Validate changesets + run: yarn changeset status + + - name: Build modules and packages + run: yarn ci:build + + - name: Run tests + run: yarn test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 088060d4..b0ffafe5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,6 +10,13 @@ on: concurrency: ${{ github.workflow }}-${{ github.ref }} jobs: + # Run the same lint/changeset/build/test checks that gate every pull request + # (see ci.yml) before doing anything irreversible like versioning or + # publishing. This keeps `main` in lockstep with PRs. + verify: + name: "CI checks" + uses: ./.github/workflows/ci.yml + version_or_release: # This job checks whether any changesets are present in './changesets' # @@ -21,6 +28,7 @@ jobs: # etc. # name: "Changesets version or release" + needs: verify runs-on: ubuntu-latest steps: - name: Checkout Repo From 2e019074741cbcdf0e47c3b10143b489e96f49fe Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 4 Jun 2026 16:17:46 +0000 Subject: [PATCH 2/7] ci: fix changeset status base ref and set least-privilege permissions - `changeset status` failed on PRs with "Failed to find where HEAD diverged from main" because a fresh Actions checkout has no local `main` branch. Pass `--since=origin/main` so it diffs against the remote-tracking ref (fetch-depth: 0 already provides the history). Verified it still fails on a changeset referencing a missing package. - Add an explicit `permissions: contents: read` block, resolving the CodeQL "Workflow does not contain permissions" finding. The verify job only reads the repo; publishing stays in release.yml. https://claude.ai/code/session_01HWaRWqpMHzzeTSJeqTeV75 --- .github/workflows/ci.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1ef325f..4d6e4013 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,11 @@ on: pull_request: workflow_call: +# Least privilege: this job only reads the repo to lint, build, and test. +# No publishing or writes happen here (that stays in release.yml). +permissions: + contents: read + concurrency: group: ci-${{ github.ref }} cancel-in-progress: true @@ -39,8 +44,11 @@ jobs: # Catches changesets that reference packages that no longer exist (or were # renamed) before they reach `main`, where `changeset version` would fail. + # `--since=origin/main` is required because a fresh Actions checkout has no + # local `main` branch for changesets' default merge-base lookup; it also + # scopes the check to changesets the PR actually adds. - name: Validate changesets - run: yarn changeset status + run: yarn changeset status --since=origin/main - name: Build modules and packages run: yarn ci:build From 517270a08f539087702f71a883e71ee2bd030699 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 4 Jun 2026 16:21:47 +0000 Subject: [PATCH 3/7] chore: add empty changeset for CI-only changes This PR only touches CI configuration (no published package changes), so it carries an intentionally empty changeset. This satisfies the new `changeset status` gate, which requires every PR with changes to either declare a release or explicitly opt out via an empty changeset. https://claude.ai/code/session_01HWaRWqpMHzzeTSJeqTeV75 --- .changeset/tidy-hotels-grin.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changeset/tidy-hotels-grin.md diff --git a/.changeset/tidy-hotels-grin.md b/.changeset/tidy-hotels-grin.md new file mode 100644 index 00000000..a845151c --- /dev/null +++ b/.changeset/tidy-hotels-grin.md @@ -0,0 +1,2 @@ +--- +--- From 9ca037714560ab98c8d45ae36ee1fb2b0294ecf1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 4 Jun 2026 17:33:29 +0000 Subject: [PATCH 4/7] ci: validate changeset package refs without requiring a changeset `changeset status` couples two behaviors: validating that changesets reference real packages, and requiring every PR with changes to include a changeset. Only the former is needed for #250, and the latter forced empty changesets on CI/docs/chore PRs (and behaved inconsistently between local and the PR merge-ref checkout). changesets has no config flag to disable the "changed but no changeset" error, so replace the step with scripts/validate-changesets.mjs, which uses @changesets/get-release-plan (already a devDependency) to assemble the release plan. That throws on a changeset referencing a missing package (catching #247) but does not require a changeset to exist and needs no git history -- so fetch-depth: 0 and the empty changeset are both dropped. https://claude.ai/code/session_01HWaRWqpMHzzeTSJeqTeV75 --- .changeset/tidy-hotels-grin.md | 2 -- .github/workflows/ci.yml | 15 ++++++--------- scripts/validate-changesets.mjs | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 11 deletions(-) delete mode 100644 .changeset/tidy-hotels-grin.md create mode 100644 scripts/validate-changesets.mjs diff --git a/.changeset/tidy-hotels-grin.md b/.changeset/tidy-hotels-grin.md deleted file mode 100644 index a845151c..00000000 --- a/.changeset/tidy-hotels-grin.md +++ /dev/null @@ -1,2 +0,0 @@ ---- ---- diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d6e4013..1692af5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,9 +27,6 @@ jobs: steps: - name: Checkout Repo uses: actions/checkout@v4 - with: - # Full history so `changeset status` can diff against the base branch. - fetch-depth: 0 - name: Setup Node.js 18.x uses: actions/setup-node@v4 @@ -42,13 +39,13 @@ jobs: - name: Lint code run: yarn lint - # Catches changesets that reference packages that no longer exist (or were - # renamed) before they reach `main`, where `changeset version` would fail. - # `--since=origin/main` is required because a fresh Actions checkout has no - # local `main` branch for changesets' default merge-base lookup; it also - # scopes the check to changesets the PR actually adds. + # Fails if any changeset references a package that no longer exists (e.g. + # a deleted/renamed package, as in #247) before it reaches `main`, where + # `changeset version` would otherwise fail. Unlike `changeset status`, + # this does not require PRs to include a changeset, so CI/docs/chore PRs + # don't need an empty one. - name: Validate changesets - run: yarn changeset status --since=origin/main + run: node scripts/validate-changesets.mjs - name: Build modules and packages run: yarn ci:build diff --git a/scripts/validate-changesets.mjs b/scripts/validate-changesets.mjs new file mode 100644 index 00000000..75612bbb --- /dev/null +++ b/scripts/validate-changesets.mjs @@ -0,0 +1,17 @@ +// Validates that every changeset references a real workspace package. +// +// This catches the failure mode from #250/#247 -- a changeset pointing at a +// deleted or renamed package -- at PR time, before it reaches `main` where +// `changeset version` would fail. Unlike `changeset status`, it does NOT +// require a changeset to be present (so CI-only/docs PRs don't need an empty +// changeset) and needs no git history. +import getReleasePlan from "@changesets/get-release-plan"; + +try { + await getReleasePlan.default(process.cwd()); + console.log("✓ All changesets reference valid packages"); +} catch (err) { + console.error("✗ Invalid changeset detected:"); + console.error(err.message); + process.exit(1); +} From 39c07dd69bdc77409788f141a4c44f3ed0af6e5c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 4 Jun 2026 17:42:12 +0000 Subject: [PATCH 5/7] ci: require changesets, exempt private packages, run check before lint Revert the custom validation script in favor of `changeset status`, which requires a changeset when a versionable package changes (the behavior we want) and still fails on changesets referencing a missing package (#247). Two adjustments make it robust: - Set `privatePackages.version: false` in .changeset/config.json. By default changesets treats private packages as versionable, so changes to the private demo app (example-app) and config packages demanded a changeset. Those are never published, so they are now exempt; the six published @infinitered/* modules still require one. - Run the changeset check before `yarn lint`. Lint runs `prettier --write`, which reformats drifted files in place and made changesets think example-app changed. Checking the pristine, just-checked-out tree avoids that phantom change. Note: root-level files (.github, .circleci, etc.) are not part of any workspace package, so CI/config-only PRs never require a changeset. https://claude.ai/code/session_01HWaRWqpMHzzeTSJeqTeV75 --- .changeset/config.json | 6 +++++- .github/workflows/ci.yml | 21 +++++++++++++-------- scripts/validate-changesets.mjs | 17 ----------------- 3 files changed, 18 insertions(+), 26 deletions(-) delete mode 100644 scripts/validate-changesets.mjs diff --git a/.changeset/config.json b/.changeset/config.json index 5d7c6032..3a592252 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -12,5 +12,9 @@ "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": [] + "ignore": [], + "privatePackages": { + "version": false, + "tag": false + } } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1692af5e..38bb5c3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,10 @@ jobs: steps: - name: Checkout Repo uses: actions/checkout@v4 + with: + # Full history so `changeset status` can find where the branch + # diverged from origin/main. + fetch-depth: 0 - name: Setup Node.js 18.x uses: actions/setup-node@v4 @@ -36,17 +40,18 @@ jobs: - name: Install Dependencies run: yarn install --immutable + # Runs before `lint` on purpose: `yarn lint` reformats files in place + # (prettier --write), which would otherwise make changesets think a + # package changed. This requires a changeset for changed published + # packages and fails on changesets that reference a missing package + # (#247). Private packages (example-app, configs) are exempt via + # `privatePackages.version: false` in .changeset/config.json. + - name: Validate changesets + run: yarn changeset status --since=origin/main + - name: Lint code run: yarn lint - # Fails if any changeset references a package that no longer exists (e.g. - # a deleted/renamed package, as in #247) before it reaches `main`, where - # `changeset version` would otherwise fail. Unlike `changeset status`, - # this does not require PRs to include a changeset, so CI/docs/chore PRs - # don't need an empty one. - - name: Validate changesets - run: node scripts/validate-changesets.mjs - - name: Build modules and packages run: yarn ci:build diff --git a/scripts/validate-changesets.mjs b/scripts/validate-changesets.mjs deleted file mode 100644 index 75612bbb..00000000 --- a/scripts/validate-changesets.mjs +++ /dev/null @@ -1,17 +0,0 @@ -// Validates that every changeset references a real workspace package. -// -// This catches the failure mode from #250/#247 -- a changeset pointing at a -// deleted or renamed package -- at PR time, before it reaches `main` where -// `changeset version` would fail. Unlike `changeset status`, it does NOT -// require a changeset to be present (so CI-only/docs PRs don't need an empty -// changeset) and needs no git history. -import getReleasePlan from "@changesets/get-release-plan"; - -try { - await getReleasePlan.default(process.cwd()); - console.log("✓ All changesets reference valid packages"); -} catch (err) { - console.error("✗ Invalid changeset detected:"); - console.error(err.message); - process.exit(1); -} From eff4ceefc4e7b39ebb5298bd98acec9d25100eb1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 4 Jun 2026 17:59:33 +0000 Subject: [PATCH 6/7] ci: check formatting/lint without mutating; fix example-app drift CI ran `yarn lint`, whose example-app script does `eslint --fix` and `prettier --write`, silently reformatting files in the runner instead of failing on drift. Make CI verify instead of fix: - Add `lint:ci` to example-app (eslint without --fix, prettier --check) and a matching `format:check` script. - Add root `ci:lint` that lints the modules via turbo (their `expo-module lint` is already non-mutating) and runs example-app's check. No --no-cache: the example-app check runs outside turbo and module lint is non-mutating and input-keyed, so turbo's cache stays correct. - Point the CI workflow at `yarn ci:lint`. `yarn lint` still auto-fixes locally. - Format the two drifted example-app files so the check passes. https://claude.ai/code/session_01HWaRWqpMHzzeTSJeqTeV75 --- .github/workflows/ci.yml | 14 +++++++------- .../app/screens/TextRecognitionScreen.tsx | 5 ++++- apps/ExampleApp/app/screens/index.ts | 2 +- apps/ExampleApp/package.json | 2 ++ package.json | 1 + 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38bb5c3e..0492a050 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,17 +40,17 @@ jobs: - name: Install Dependencies run: yarn install --immutable - # Runs before `lint` on purpose: `yarn lint` reformats files in place - # (prettier --write), which would otherwise make changesets think a - # package changed. This requires a changeset for changed published - # packages and fails on changesets that reference a missing package - # (#247). Private packages (example-app, configs) are exempt via - # `privatePackages.version: false` in .changeset/config.json. + # Requires a changeset for changed published packages and fails on + # changesets that reference a missing package (#247). Private packages + # (example-app, configs) are exempt via `privatePackages.version: false` + # in .changeset/config.json. - name: Validate changesets run: yarn changeset status --since=origin/main + # CI lints without mutating (eslint without --fix, prettier --check), so + # formatting drift fails the build. Locally `yarn lint` still auto-fixes. - name: Lint code - run: yarn lint + run: yarn ci:lint - name: Build modules and packages run: yarn ci:build diff --git a/apps/ExampleApp/app/screens/TextRecognitionScreen.tsx b/apps/ExampleApp/app/screens/TextRecognitionScreen.tsx index df574a3e..ee07d9e0 100644 --- a/apps/ExampleApp/app/screens/TextRecognitionScreen.tsx +++ b/apps/ExampleApp/app/screens/TextRecognitionScreen.tsx @@ -6,7 +6,10 @@ import { AppStackScreenProps } from "../navigators" import { Text, Icon, ImageSelector, Screen } from "../components" import { useTypedNavigation } from "../navigators/useTypedNavigation" -import { recognizeText, Text as RecognizedText } from "@infinitered/react-native-mlkit-text-recognition" +import { + recognizeText, + Text as RecognizedText, +} from "@infinitered/react-native-mlkit-text-recognition" import { UseExampleImageStatus, SelectedImage } from "../utils/useExampleImage" type TextRecognitionScreenProps = NativeStackScreenProps> diff --git a/apps/ExampleApp/app/screens/index.ts b/apps/ExampleApp/app/screens/index.ts index 52f2fb39..52018d68 100644 --- a/apps/ExampleApp/app/screens/index.ts +++ b/apps/ExampleApp/app/screens/index.ts @@ -7,4 +7,4 @@ export * from "./ImageLabelingScreen" export * from "./DocumentScannerScreen" export { BOX_COLORS } from "./FaceDetectionScreen" export * from "./ObjectDetectionScreen" -export * from "./TextRecognitionScreen" \ No newline at end of file +export * from "./TextRecognitionScreen" diff --git a/apps/ExampleApp/package.json b/apps/ExampleApp/package.json index 0783ef09..00b3224d 100644 --- a/apps/ExampleApp/package.json +++ b/apps/ExampleApp/package.json @@ -6,7 +6,9 @@ "scripts": { "compile": "tsc --noEmit -p . --pretty", "format": "prettier --write \"app/**/*.{js,jsx,json,md,ts,tsx}\"", + "format:check": "prettier --check \"app/**/*.{js,jsx,json,md,ts,tsx}\"", "lint": "eslint App.tsx app test --fix --ext .js,.ts,.tsx && npm run format", + "lint:ci": "eslint App.tsx app test --ext .js,.ts,.tsx && npm run format:check", "patch": "patch-package", "test": "jest", "test:watch": "jest --watch", diff --git a/package.json b/package.json index 8bbaa656..96c56072 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "test": "turbo run test -- --run-in-band", "dev": "turbo run dev", "lint": "turbo run lint", + "ci:lint": "turbo run lint --filter=!example-app && yarn workspace example-app lint:ci", "clean": "turbo run clean --force", "format": "prettier --write \"**/*.{ts,tsx,md}\"", "setup": "./scripts/setup.sh", From ed18685e570f317a5d1730305c9c6e0383b83cd9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 4 Jun 2026 18:20:49 +0000 Subject: [PATCH 7/7] ci: give the CI job a stable name (verify) for branch protection The job name is what branch protection's required-status-check rule matches on. Use a short, stable 'verify' instead of a descriptive sentence so the rule doesn't need re-updating if the description changes. https://claude.ai/code/session_01HWaRWqpMHzzeTSJeqTeV75 --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0492a050..6d8b2a2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,9 @@ concurrency: jobs: verify: - name: Lint, validate changesets, build, and test + # Keep this name stable: it's referenced by the branch protection + # "required status checks" rule on main. + name: verify runs-on: ubuntu-latest steps: - name: Checkout Repo