From 6f2f0bfdacc503f735c6ec17ee316769fa2336a7 Mon Sep 17 00:00:00 2001 From: Edu Date: Mon, 5 Oct 2026 14:43:40 +0200 Subject: [PATCH 1/9] docs: document npm publishing and first-time package publish --- .../skills/publish-new-npm-package/SKILL.md | 46 ++++++++++ CONTRIBUTING.md | 91 ++++++++++++++++++- 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/publish-new-npm-package/SKILL.md diff --git a/.claude/skills/publish-new-npm-package/SKILL.md b/.claude/skills/publish-new-npm-package/SKILL.md new file mode 100644 index 00000000..617c1ff5 --- /dev/null +++ b/.claude/skills/publish-new-npm-package/SKILL.md @@ -0,0 +1,46 @@ +--- +name: publish-new-npm-package +description: "Trigger: a maintainer needs to publish a package from this monorepo to npm for the first time, set up npm trusted publishing (OIDC) for a package, or a release fails because a package does not exist on npm yet. Guides the manual first publish and trusted publisher setup." +--- + +## Activation Contract + +Use when a workspace package under `packages/` has never been published to npm, when a maintainer asks how to bootstrap trusted publishing for a package, or when the release workflow fails to publish a package npm does not know. + +Do not use for regular releases. Those only need a changeset; CI publishes them. + +## Hard Rules + +- The "Publishing to npm" section of `CONTRIBUTING.md` is the source of truth. Read it in full before acting and follow its steps and order. Do not work from memory or from a copy of the steps. +- If this skill and `CONTRIBUTING.md` disagree, `CONTRIBUTING.md` wins. Tell the maintainer about the mismatch. +- Never run `npm publish` (without `--dry-run`) unless the maintainer has explicitly said to publish that exact package and version. Publishing is irreversible: npm never accepts the same version twice. +- The maintainer runs `npm login` and completes two-factor authentication. Never ask for, read, store, or type OTPs, passwords, or npm tokens. +- Never commit `package.tgz` or any other packed tarball. Delete it after publishing. +- Do not merge the PR that adds the package before the manual publish succeeded. + +## Pre-flight Checks + +Run these yourself before the maintainer publishes, and report the results: + +| Check | Command | Expected | +|-------|---------|----------| +| Logged in | `npm whoami` | The maintainer's npm username | +| Not published yet | `npm view version` | `E404`. A version means the first publish already happened; switch to trusted publisher setup. | +| Version matches the fixed group | compare `version` in `packages//package.json` with `packages/react-native-brownfield/package.json` | Same version | +| In the fixed group | `.changeset/config.json` | Package name listed under `fixed` | +| Public access | `publishConfig.access` in the package's `package.json` | `public` | +| Tarball clean | after `yarn workspace pack`, `tar -xzOf packages//package.tgz package/package.json` | No `workspace:` ranges | + +If a check fails, stop and tell the maintainer what to fix. Do not fix version numbers or the fixed group without asking. + +## Execution Steps + +1. Read the "Publishing to npm" section of `CONTRIBUTING.md`. +2. Run the pre-flight checks. +3. Walk the maintainer through the steps in `CONTRIBUTING.md`, running the build, pack and inspection commands when asked. +4. Before the publish step, show the exact command, package name and version, and wait for an explicit go-ahead. +5. After the publish, run `npm view version`, delete the tarball, and remind the maintainer to configure the trusted publisher once the PR is merged. + +## Output Contract + +Report each check as `: `, the published version if any, and the steps still left for the maintainer. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b756b72f..a9114faa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,96 @@ If you need to intentionally commit those files (for an explicit update), bypass ## Publishing to npm -We use [changesets](https://github.com/changesets/changesets) to make it easier to publish new versions. It handles common tasks like bumping version based on semver, creating tags and releases etc. +We use [changesets](https://github.com/changesets/changesets) to version and publish packages. Contributors only add a changeset to their PR. CI does the rest. + +The release workflow (`.github/workflows/release.yml`) runs on every push to `main`: + +1. A PR with a changeset is merged to `main`. +2. The workflow builds the packages, runs `yarn ci:version` (which bumps versions, updates changelogs and refreshes the lockfile), and opens or updates a PR titled `chore(release): version packages` with the result. +3. Merging the version PR runs the workflow again, and this time `yarn ci:publish` (`changeset publish`) publishes the new versions to npm. + +The packages listed in the `fixed` group in `.changeset/config.json` always share one version. A changeset for any of them bumps all of them. + +CI has no npm token. It publishes through [npm trusted publishing](https://docs.npmjs.com/trusted-publishers), which uses the workflow's OIDC identity (`id-token: write`). Provenance is generated automatically. Trusted publishing requires npm CLI 11.5.1 or later and Node 22.14.0 or later on the runner. + +### Publishing a new package for the first time + +A trusted publisher can only be configured for a package that already exists on npm. A brand-new package therefore needs one manual publish by a maintainer. After that, CI publishes it like every other package. + +> [!IMPORTANT] +> Publish the new package manually before merging the PR that adds it. If the PR is merged first, the release workflow will try to publish a package npm has never seen, with no trusted publisher configured for it, and the release fails. + +The order is: manual publish, merge the PR, configure the trusted publisher, then let the next version PR publish the package. + +1. Use an npm account (personal or a Callstack one) with two-factor authentication enabled. +2. Ask a Callstack npm org admin to add your npm username to the `@callstack` org with publish rights. +3. Log in and check the account: + + ```sh + npm login + npm whoami + ``` + +4. Prepare the package on the PR branch: + - Set `version` in its `package.json` to the version the rest of the fixed group uses (see `packages/react-native-brownfield/package.json`). + - Add the package name to the `fixed` group in `.changeset/config.json`. + - Make sure `publishConfig.access` is `public`. +5. Build all packages from the repository root: + + ```sh + yarn build + ``` + +6. Pack the package. Use `yarn` here, not `npm`: dependencies between workspace packages use `workspace:^`, and `npm publish` run inside the package folder would publish that range literally. `yarn pack` replaces it with real versions. + + ```sh + yarn workspace pack + ``` + + This writes `package.tgz` into the package folder. + +7. Inspect the tarball before publishing. Check the file list, and check that `package/package.json` inside it has no `workspace:` ranges: + + ```sh + tar -tzf packages//package.tgz + tar -xzOf packages//package.tgz package/package.json | grep workspace: + ``` + + The second command should print nothing. Optionally, run `npm publish packages//package.tgz --dry-run`. + +8. Publish the tarball: + + ```sh + npm publish packages//package.tgz + ``` + + npm asks you to complete two-factor authentication, either in the browser or with a one-time password. + + A published version can't be published again, even after unpublishing. Double-check the version before running this. + +9. Check that the version is live: + + ```sh + npm view version + ``` + +10. Delete `package.tgz`. Never commit it. + +Example: `@callstack/create-react-native-brownfield` lives in `packages/create-react-native-brownfield` and its first manual publish is `5.1.1`. + +### Configuring the trusted publisher + +Do this once per package, after its first manual publish: + +1. On npmjs.com, open the package and go to **Settings**, then **Trusted Publisher**. +2. Choose **GitHub Actions** and fill in: + - Organization or user: `callstack` + - Repository: `react-native-brownfield` + - Workflow filename: `release.yml` (the filename only, not the path) + - Environment: leave empty, the release workflow doesn't use one +3. Save. + +Once a CI release has published the package successfully, set its publishing access to "Require two-factor authentication and disallow tokens", as npm recommends. ## Scripts From 150d86250059d9087952d4684bd9f7351e724df1 Mon Sep 17 00:00:00 2001 From: Edu Date: Mon, 5 Oct 2026 15:21:38 +0200 Subject: [PATCH 2/9] docs: record observed behavior of the first manual npm publish --- .claude/skills/publish-new-npm-package/SKILL.md | 3 ++- CONTRIBUTING.md | 12 ++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.claude/skills/publish-new-npm-package/SKILL.md b/.claude/skills/publish-new-npm-package/SKILL.md index 617c1ff5..0ae12ac4 100644 --- a/.claude/skills/publish-new-npm-package/SKILL.md +++ b/.claude/skills/publish-new-npm-package/SKILL.md @@ -39,7 +39,8 @@ If a check fails, stop and tell the maintainer what to fix. Do not fix version n 2. Run the pre-flight checks. 3. Walk the maintainer through the steps in `CONTRIBUTING.md`, running the build, pack and inspection commands when asked. 4. Before the publish step, show the exact command, package name and version, and wait for an explicit go-ahead. -5. After the publish, run `npm view version`, delete the tarball, and remind the maintainer to configure the trusted publisher once the PR is merged. +5. The maintainer runs `npm publish` in their own interactive terminal. Your shell is non-interactive, so npm can't wait for the browser approval and fails with `EOTP` without publishing. Do not suggest `--otp`, because the code would end up in the conversation. +6. After the publish, poll `npm view versions` until the real version replaces the `0.0.0-stage` placeholder (it took about two minutes the first time). Then delete the tarball and remind the maintainer to configure the trusted publisher once the PR is merged. ## Output Contract diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9114faa..36410f45 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,7 +44,7 @@ A trusted publisher can only be configured for a package that already exists on The order is: manual publish, merge the PR, configure the trusted publisher, then let the next version PR publish the package. 1. Use an npm account (personal or a Callstack one) with two-factor authentication enabled. -2. Ask a Callstack npm org admin to add your npm username to the `@callstack` org with publish rights. +2. Ask a Callstack npm org admin to add your npm username to the `@callstack` org. npm sends the invitation by email; accept it. The default `developer` role is enough to publish a new package under the scope. 3. Log in and check the account: ```sh @@ -52,6 +52,8 @@ The order is: manual publish, merge the PR, configure the trusted publisher, the npm whoami ``` + `npm login` prints a URL instead of asking for a password. Finish the login, including two-factor authentication, in the browser. + 4. Prepare the package on the PR branch: - Set `version` in its `package.json` to the version the rest of the fixed group uses (see `packages/react-native-brownfield/package.json`). - Add the package name to the `fixed` group in `.changeset/config.json`. @@ -85,19 +87,21 @@ The order is: manual publish, merge the PR, configure the trusted publisher, the npm publish packages//package.tgz ``` - npm asks you to complete two-factor authentication, either in the browser or with a one-time password. + Run this in an interactive terminal. npm prints an authentication URL and waits with "Press ENTER to open in the browser". Approve the publish there and npm finishes on its own. In a non-interactive shell, such as a command run by an AI agent, npm can't wait and fails with `EOTP`. Nothing is published in that case, so run the command again in a regular terminal. A published version can't be published again, even after unpublishing. Double-check the version before running this. 9. Check that the version is live: ```sh - npm view version + npm view versions ``` + npm reports "Your package is being processed and may take a few minutes to become available". Until processing ends, the registry only lists a `0.0.0-stage` placeholder. The real version showed up after about two minutes the first time we did this. + 10. Delete `package.tgz`. Never commit it. -Example: `@callstack/create-react-native-brownfield` lives in `packages/create-react-native-brownfield` and its first manual publish is `5.1.1`. +Example: `@callstack/create-react-native-brownfield` lives in `packages/create-react-native-brownfield` and was first published by hand as `5.1.1`. ### Configuring the trusted publisher From 1af68592a9644ce80093a97578a26d3ca666e91a Mon Sep 17 00:00:00 2001 From: Edu Date: Mon, 5 Oct 2026 15:52:22 +0200 Subject: [PATCH 3/9] docs: document the trusted publisher form and configure it before merging --- .../skills/publish-new-npm-package/SKILL.md | 4 +-- CONTRIBUTING.md | 25 +++++++++++-------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/.claude/skills/publish-new-npm-package/SKILL.md b/.claude/skills/publish-new-npm-package/SKILL.md index 0ae12ac4..38b2d2f6 100644 --- a/.claude/skills/publish-new-npm-package/SKILL.md +++ b/.claude/skills/publish-new-npm-package/SKILL.md @@ -16,7 +16,7 @@ Do not use for regular releases. Those only need a changeset; CI publishes them. - Never run `npm publish` (without `--dry-run`) unless the maintainer has explicitly said to publish that exact package and version. Publishing is irreversible: npm never accepts the same version twice. - The maintainer runs `npm login` and completes two-factor authentication. Never ask for, read, store, or type OTPs, passwords, or npm tokens. - Never commit `package.tgz` or any other packed tarball. Delete it after publishing. -- Do not merge the PR that adds the package before the manual publish succeeded. +- Do not merge the PR that adds the package before the manual publish succeeded and its trusted publisher is configured. ## Pre-flight Checks @@ -40,7 +40,7 @@ If a check fails, stop and tell the maintainer what to fix. Do not fix version n 3. Walk the maintainer through the steps in `CONTRIBUTING.md`, running the build, pack and inspection commands when asked. 4. Before the publish step, show the exact command, package name and version, and wait for an explicit go-ahead. 5. The maintainer runs `npm publish` in their own interactive terminal. Your shell is non-interactive, so npm can't wait for the browser approval and fails with `EOTP` without publishing. Do not suggest `--otp`, because the code would end up in the conversation. -6. After the publish, poll `npm view versions` until the real version replaces the `0.0.0-stage` placeholder (it took about two minutes the first time). Then delete the tarball and remind the maintainer to configure the trusted publisher once the PR is merged. +6. After the publish, poll `npm view versions` until the real version replaces the `0.0.0-stage` placeholder (it took about two minutes the first time). Then delete the tarball and walk the maintainer through the trusted publisher setup in `CONTRIBUTING.md` before the PR is merged. The maintainer fills in the npmjs.com form. If they share screenshots of an existing package's card or of the form, compare each field with `CONTRIBUTING.md` before they click **Set up connection**, since the connection can't be edited afterwards. ## Output Contract diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 36410f45..3400fed7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,9 +39,9 @@ CI has no npm token. It publishes through [npm trusted publishing](https://docs. A trusted publisher can only be configured for a package that already exists on npm. A brand-new package therefore needs one manual publish by a maintainer. After that, CI publishes it like every other package. > [!IMPORTANT] -> Publish the new package manually before merging the PR that adds it. If the PR is merged first, the release workflow will try to publish a package npm has never seen, with no trusted publisher configured for it, and the release fails. +> Publish the new package manually and configure its trusted publisher before merging the PR that adds it. Otherwise the next release tries to publish a package that CI has no permission for, and the release fails. -The order is: manual publish, merge the PR, configure the trusted publisher, then let the next version PR publish the package. +The order is: manual publish, configure the trusted publisher, merge the PR, then let the next version PR publish the package. 1. Use an npm account (personal or a Callstack one) with two-factor authentication enabled. 2. Ask a Callstack npm org admin to add your npm username to the `@callstack` org. npm sends the invitation by email; accept it. The default `developer` role is enough to publish a new package under the scope. @@ -105,17 +105,20 @@ Example: `@callstack/create-react-native-brownfield` lives in `packages/create-r ### Configuring the trusted publisher -Do this once per package, after its first manual publish: +Do this once per package, right after its first manual publish. The other packages in the repo already have the same connection, so you can compare with any of them (for example `https://www.npmjs.com/package/@callstack/brownfield-navigation/access`). -1. On npmjs.com, open the package and go to **Settings**, then **Trusted Publisher**. -2. Choose **GitHub Actions** and fill in: - - Organization or user: `callstack` - - Repository: `react-native-brownfield` - - Workflow filename: `release.yml` (the filename only, not the path) - - Environment: leave empty, the release workflow doesn't use one -3. Save. +1. Open the settings page of the new package: `https://www.npmjs.com/package//access`. Make sure it is the new package and not an existing one. Adding the same connection to a package that already has it fails with "a trusted publisher configuration that a token could also match already exists for this package". +2. Under **Trusted Publisher**, select **GitHub Actions** and fill in the form: + - **Label**: leave empty. + - **Organization or user**: `callstack` + - **Repository**: `react-native-brownfield`. This is the GitHub repository that publishes, not the npm package name. + - **Workflow filename**: `release.yml` + - **Environment name**: leave empty. The release workflow doesn't use one. + - **Allowed actions**: check **Allow npm publish**, leave **Allow npm dist-tag** unchecked. `npm stage publish` is always allowed. -Once a CI release has published the package successfully, set its publishing access to "Require two-factor authentication and disallow tokens", as npm recommends. + The provider and required fields can't be edited after saving. To fix a typo, delete the connection and create a new one. +3. Click **Set up connection**. The card should show `callstack/react-native-brownfield`, `release.yml` and the permissions **npm publish** and **npm stage publish**. +4. Under **Publishing access**, select **Require two-factor authentication and disallow bypass 2fa tokens (recommended)** and click **Update Package Settings**. Trusted publishing keeps working with this option. ## Scripts From c864c0e959262d278131f4ccd00aa2ea938856fd Mon Sep 17 00:00:00 2001 From: Edu Date: Mon, 5 Oct 2026 19:09:07 +0200 Subject: [PATCH 4/9] docs: add a minimal AGENTS.md pointing agents to CONTRIBUTING and skills --- AGENTS.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..b3759486 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,15 @@ +# AGENTS.md + +Start with [`CONTRIBUTING.md`](CONTRIBUTING.md). It covers setup, scripts, tests, E2E and publishing for this monorepo, and it wins if anything here disagrees with it. + +## Repository basics + +- Yarn 4 workspaces with Turbo. Run `yarn` at the root to install. +- Packages live in `packages/`, example and host apps in `apps/`, and the Android Gradle plugin in `gradle-plugins/`. +- Every PR that changes a published package needs a changeset (`yarn changeset`). CI handles versioning and npm publishing. +- Commit messages follow Conventional Commits. A `commitlint` hook checks them. + +## Skills + +- `.claude/skills/` holds skills for working on this repo. For example, `publish-new-npm-package` walks through the first manual publish of a new package. Claude Code loads them on its own. Other agents should read the `SKILL.md` that matches the task. +- `skills/` holds skills for people using the libraries in their apps (`brownie`, `brownfield-navigation`). They are not about maintaining this repo. From 4c9361dd272f80685026b0e1548c477da9d5f83b Mon Sep 17 00:00:00 2001 From: Edu Date: Tue, 6 Oct 2026 09:51:58 +0200 Subject: [PATCH 5/9] docs: drop the provenance claim and add a check for CI publishes --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3400fed7..dcdeb886 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,7 +32,7 @@ The release workflow (`.github/workflows/release.yml`) runs on every push to `ma The packages listed in the `fixed` group in `.changeset/config.json` always share one version. A changeset for any of them bumps all of them. -CI has no npm token. It publishes through [npm trusted publishing](https://docs.npmjs.com/trusted-publishers), which uses the workflow's OIDC identity (`id-token: write`). Provenance is generated automatically. Trusted publishing requires npm CLI 11.5.1 or later and Node 22.14.0 or later on the runner. +CI has no npm token. It publishes through [npm trusted publishing](https://docs.npmjs.com/trusted-publishers), which uses the workflow's OIDC identity (`id-token: write`). Trusted publishing requires npm CLI 11.5.1 or later and Node 22.14.0 or later on the runner. To confirm a release went through CI, check that `npm view @ _npmUser` shows `GitHub Actions`. ### Publishing a new package for the first time From a60a4cf125b3b265154930f00241054aeb6c4fab Mon Sep 17 00:00:00 2001 From: Edu Date: Tue, 6 Oct 2026 10:01:20 +0200 Subject: [PATCH 6/9] docs: make the Scripts section list where each script actually lives --- CONTRIBUTING.md | 81 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 17 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dcdeb886..d3749526 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -122,25 +122,72 @@ Do this once per package, right after its first manual publish. The other packag ## Scripts -- `lint` - runs linting on all JS/TS source files in the monorepo _[Turbo]_ -- `gradle-plugin:lint` - runs linting on the Brownfield Gradle plugin source code -- `typecheck` - runs TypeScript type checking on all TS source files in the monorepo _[Turbo]_ -- `test:apps` - runs Jest for the React Native example apps under `apps/` (Expo 58, plain RN) _[Turbo]_ -- `build` - runs all `build*` tasks in the Turbo repo - see below for more details _[Turbo]_ -- `dev` - runs all `dev` tasks in all workspaces -- `brownfield:plugin:publish:local` - publishes the Brownfield Gradle plugin to your local Maven repository for testing purposes -- `build:brownfield` - builds the React Native Brownfield package (`packages/react-native-brownfield`) _[Turbo]_ -- `build:docs` - builds the documentation site (`docs/`) _[Turbo]_ -- `build:example:android-rn` - builds the example React Native app for Android (`apps/RNApp/android`) -- `build:example:ios-rn` - builds the example React Native app for iOS (`apps/RNApp/ios`) -- `build:example:android-consumer:expo58` - builds the example native Android consumer (`apps/AndroidApp`) app's flavor consuming the Expo 58 RN app (`apps/ExpoApp58`) artifact -- `build:example:android-consumer:expo57` - builds the example native Android consumer (`apps/AndroidApp`) app's flavor consuming the Expo 57 RN app (`apps/ExpoApp57`) artifact +Root scripts run from the repository root with `yarn