Skip to content
47 changes: 47 additions & 0 deletions .claude/skills/publish-new-npm-package/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
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 and its trusted publisher is configured.

## 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 <package-name> version` | `E404`. A version means the first publish already happened; switch to trusted publisher setup. |
| Version matches the fixed group | compare `version` in `packages/<dir>/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 build` at the repository root and `yarn workspace <package-name> pack`, `tar -xzOf packages/<dir>/package.tgz package/package.json` | No `workspace:` ranges, and `tar -tzf packages/<dir>/package.tgz` lists the files that `main` and `bin` point to |

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. 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 <package-name> 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

Report each check as `<command>: <result>`, the published version if any, and the steps still left for the maintainer.
16 changes: 16 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# 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.
- Before opening a PR, run the checks for the area you changed. They are listed in "Verifying a change" in `CONTRIBUTING.md`.

## 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.
193 changes: 175 additions & 18 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,188 @@ If you need to intentionally commit those files (for an explicit update), bypass

`SKIP_BROWNFIELD_NAVIGATION_CHECK=1 git commit -m "..."`

### Verifying a change

Run the checks for the area you touched before opening a PR. CI runs the same ones, so this catches most failures locally.

| If you changed | Run |
| --- | --- |
| TS/JS in `packages/` | `yarn build`, `yarn lint`, `yarn typecheck` and `yarn test:packages`. The pre-commit hook already runs lint and typecheck on staged JS/TS files. |
| `scripts/` | `yarn test:scripts` |
| JS in the example apps under `apps/` | `yarn test:apps` |
| The `BrownfieldConfig` type | `yarn generate:schema`. The pre-commit hook regenerates `packages/cli/schema.json` and stages it. |
| The Brownfield Gradle plugin (`gradle-plugins/react/brownfield`) | From that directory: `./gradlew detekt ktlintCheck test`, which is what CI runs. `yarn gradle-plugin:lint` also runs detekt, but its `ktlintFormat` rewrites files. |
| Native iOS or Android code in a package | Build the affected example with its `build:example:*` script (see [Workspace scripts](#workspace-scripts)), then run the matching E2E with the [Local CI scripts](#local-ci-scripts). |
| Anything in a published package | Add a changeset with `yarn changeset`. CI fails the PR without one (`changeset status`). |

## 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`). 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 <package-name>@<version> _npmUser` shows `GitHub Actions`.

### 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 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, 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.
3. Log in and check the account:

```sh
npm login
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`.
- 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 <package-name> 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/<dir>/package.tgz
tar -xzOf packages/<dir>/package.tgz package/package.json | grep workspace:
```

The second command should print nothing. Optionally, run `npm publish packages/<dir>/package.tgz --dry-run`.

8. Publish the tarball:

```sh
npm publish packages/<dir>/package.tgz
```

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 <package-name> 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 was first published by hand as `5.1.1`.

### Configuring the trusted publisher

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. Open the settings page of the new package: `https://www.npmjs.com/package/<package-name>/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.

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

- `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 <script>`. Scripts marked _[Turbo]_ call `turbo run`, which runs the task of the same name in every workspace that defines it.

### Build, lint and test

- `build` - runs `build` in all workspaces, building dependencies first _[Turbo]_
- `build:brownfield` - runs `build:brownfield` in the packages under `packages/` that define it _[Turbo]_
- `build:docs` - runs `build:docs` in the documentation site (`docs/`) _[Turbo]_
- `dev` - runs `dev` in all workspaces in parallel (`yarn workspaces foreach`)
- `lint` - runs `lint` in all workspaces _[Turbo]_
- `typecheck` - runs `typecheck` in all workspaces _[Turbo]_
- `test:packages` - runs `test` in the workspaces under `packages/` _[Turbo]_
- `test:apps` - runs `test` in the workspaces under `apps/`, including the Node test suite in `apps/brownfield-example-shared-tests` _[Turbo]_
- `test:scripts` - runs the Node test runner on `scripts/__tests__/**/*.test.ts`

### Brownfield Gradle plugin

- `brownfield:plugin:publish:local` - builds the plugin in `gradle-plugins/react/brownfield` and publishes a snapshot to your local Maven repository, without signing
- `brownfield:plugin:publish:local:signed` - same as above, but signed and without the snapshot flag
- `brownfield:plugin:version:check` - fails if the plugin version in `gradle-plugins/react/brownfield/gradle.properties` doesn't match the copies in the JS package and the example apps
- `brownfield:plugin:version:sync` - writes that version into those copies
- `brownfield:plugin:release-notes` - generates release notes for the plugin from git history. Requires `--version` and `--output`; `--ref` is optional.

### Code generation

- `generate:schema` - regenerates `packages/cli/schema.json` from the `BrownfieldConfig` type (runs `generate:schema` in `@callstack/brownfield-cli`)
- `generate:store` - points to `scripts/generate-store.ts`, which doesn't exist in the repository, so the script currently fails

### Release and CI

- `ci:version` - used by the release workflow. See [Publishing to npm](#publishing-to-npm).
- `ci:publish` - used by the release workflow. See [Publishing to npm](#publishing-to-npm).
- `ci:local:*` - reproduce the CI E2E jobs locally. See [Local CI scripts](#local-ci-scripts).

### Skill evaluations

- `skillgym:brownie` - runs the SkillGym suite in `skillgym/suites/brownie-suite.ts`
- `skillgym:navigation` - runs the SkillGym suite in `skillgym/suites/brownfield-navigation-suite.ts`

### Workspace scripts

These live in a workspace, not in the root `package.json`. Run them with `yarn workspace <name> <script>` from the root, or with `yarn <script>` from the workspace directory.

`apps/RNApp` (`@callstack/brownfield-example-rn-app`):

- `build:example:android-rn` - builds the Android app (`react-native build-android`)
- `build:example:ios-rn` - builds the iOS app (`react-native build-ios`)

`apps/AndroidApp` (`@callstack/brownfield-example-android-app`), one Gradle flavor per consumed RN app:

- `build:example:android-consumer:vanilla` - consumes `apps/RNApp` (`assembleVanillaRelease`)
- `build:example:android-consumer:expo58` - consumes `apps/ExpoApp58` (`assembleExpo58Release`)
- `build:example:android-consumer:expo57` - consumes `apps/ExpoApp57` (`assembleExpo57Release`)
- `build:example:android-consumer:expo` - alias for `build:example:android-consumer:expo57`
- `build:example:android-consumer:vanilla` - builds the example native Android consumer (`apps/AndroidApp`) app's flavor consuming the vanilla RN app (`apps/RNApp`) artifact
- `build:example:android-consumer:expopreview` - consumes the Expo preview app (`assembleExpopreviewRelease`)

`apps/AppleApp` (`@callstack/brownfield-example-ios-app`), each copies the XCFrameworks with `prepareXCFrameworks.js` and runs `xcodebuild`:

- `build:example:ios-consumer:vanilla` - consumes `RNApp`, scheme **Brownfield Apple App Vanilla** (`Release Vanilla`)
- `build:example:ios-consumer:expo58` - consumes `ExpoApp58`, scheme **Brownfield Apple App Expo 58** (`Release`)
- `build:example:ios-consumer:expo57` - consumes `ExpoApp57`, scheme **Brownfield Apple App Expo 57** (`Release`)
- `build:example:ios-consumer:expo` - alias for `build:example:ios-consumer:expo57`
- `build:example:ios-consumer:expo58` - builds the `Brownfield Apple App (ExpoApp58)` target via scheme **Brownfield Apple App Expo 58** (`Release`)
- `build:example:ios-consumer:expo57` - builds the `Brownfield Apple App (ExpoApp57)` target via scheme **Brownfield Apple App Expo 57** (`Release`)
- `build:example:ios-consumer:vanilla` - builds the `Brownfield Apple App (RNApp)` target via scheme **Brownfield Apple App Vanilla** (`Release Vanilla`)
- `build:example:ios-consumer:expopreview` - consumes `ExpoAppPreview`, scheme **Brownfield Apple App Expo Preview** (`Release`)

`gradle-plugins/react` (`@callstack/brownfield-gradle-plugin-react`):

- `gradle-plugin:lint` - runs detekt and `ktlintFormat` on the Brownfield Gradle plugin. `ktlintFormat` rewrites files in place.

## Running demo apps

Expand Down
Loading