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
2 changes: 2 additions & 0 deletions .changeset/changeset-authoring-guidance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
44 changes: 35 additions & 9 deletions .claude/skills/clerk-monorepo/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,33 @@ pnpm lint
pnpm format # workspace packages plus root files, docs/, integration/, scripts/
pnpm prettier --write '.claude/**/*.md' # pnpm format does not cover .claude/; format skill files this way

# Changesets
pnpm changeset # for package-affecting changes
pnpm changeset:empty # for repo/tooling-only changes (see rules)
# Changesets — write the file, don't run the CLI (both scripts are interactive prompts)
pnpm changeset status --since=origin/main # what CI checks; run this to verify
```

Write `.changeset/<descriptive-name>.md` directly, in the **repo root** `.changeset/` (never
`packages/<pkg>/.changeset/`). Package change:

```md
---
'@clerk/nextjs': patch
---

Remove the redundant `https://*.client.protect.clerk.com` source from CSP headers generated by `clerkMiddleware()`.
```

Repo/tooling-only or private-package-only change — the file is exactly two delimiters, nothing else:

```md
---
---
```

`@clerk/swingset`, `@clerk/msw`, and `@clerk/headless` are private and never appear in a changeset;
every other `packages/*` entry publishes, **including `@clerk/ui` and `@clerk/clerk-js`**. Bump
selection, which packages to list, body-writing rules, and the major-version gate are in
[`references/changesets.md`](references/changesets.md).

Test runner differs by package (`shared`, `clerk-js`, most adapters use vitest; `backend` runs a
multi-runtime suite), but the `pnpm --filter <name> test` invocation is uniform.

Expand All @@ -106,10 +128,11 @@ Each rule below restates `AGENTS.md`; the parenthetical is how it is enforced.

- **pnpm only, Node `>=24.15`, pnpm `>=10.33`.** (`preinstall` blocks npm/yarn; `engines` in
`package.json`.)
- **Every PR needs a changeset.** Use `pnpm changeset` for anything that affects a published package.
Use `pnpm changeset:empty` for repo/tooling-only changes; an empty changeset is **two `---`
delimiters with no body** (local `CLAUDE.local.md` convention). A changeset is a changelog entry
for users upgrading, not a summary of the diff. (CI fails PRs missing a changeset.)
- **Every PR needs a changeset.** Write the file directly; the `pnpm changeset` /
`pnpm changeset:empty` scripts are interactive prompts an agent cannot drive. A changeset is a
changelog entry for users upgrading, not a summary of the diff. See
[`references/changesets.md`](references/changesets.md). (CI runs
`pnpm changeset status --since=origin/main` and fails PRs missing one.)
- **Conventional commit `type(scope):`, scope is mandatory.** Enforced on the **PR title**
(`.github/workflows/pr-title-linter.yml`), not on individual commits. There is no local
`commit-msg` hook. Valid `scope` = any `packages/*` short name **and** its `clerk-`-stripped form
Expand All @@ -128,7 +151,9 @@ Each rule below restates `AGENTS.md`; the parenthetical is how it is enforced.

1. Branch off `main`.
2. Make the change in the right package(s); add/update unit tests next to the code.
3. `pnpm changeset` (or `pnpm changeset:empty`).
3. Write `.changeset/<name>.md` (see above, and
[`references/changesets.md`](references/changesets.md)), then `git add` it. An unstaged changeset
fails CI the same as a missing one.
4. Verify locally: `pnpm build`, `pnpm test` (or the filtered forms above), `pnpm lint`,
`pnpm format:check`.
5. Open the PR; the title must be a valid conventional commit (it becomes the squash commit). Fill in
Expand Down Expand Up @@ -162,7 +187,8 @@ Full decision matrix:
`@clerk/ui` appearance/theming system.
- Bundled: [`setup-and-footguns.md`](references/setup-and-footguns.md),
[`package-map.md`](references/package-map.md),
[`breaking-changes.md`](references/breaking-changes.md).
[`breaking-changes.md`](references/breaking-changes.md),
[`changesets.md`](references/changesets.md).

Analyzing or coordinating a **release PR** (the "Version packages" PR) is out of scope for this
skill; the release process lives in `docs/PUBLISH.md` and `docs/CICD.md`. Clerk employees may also
Expand Down
147 changes: 147 additions & 0 deletions .claude/skills/clerk-monorepo/references/changesets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Writing a changeset in clerk/javascript

Reference for the `clerk-monorepo` skill. The file format and the private-package list are inlined
in `SKILL.md`; this covers bump selection, which packages to list, body-writing, and verification.

Every PR needs a changeset. CI runs `pnpm changeset status --since=origin/main` and fails the PR
when a changed published package has no entry.

**Write the file directly with the Write tool.** Do not run `pnpm changeset` or `pnpm changeset:empty`.
Both are interactive prompts that cannot be driven non-interactively. Those scripts exist for
humans; the file they produce is the only thing that matters, and it is plain markdown.

## Where the file goes

`.changeset/<descriptive-kebab-name>.md`, in the **repo root** `.changeset/` directory.

Never `packages/<pkg>/.changeset/`. There is one changeset directory for the whole workspace, and a
file placed under a package is invisible to changesets and to CI.

The generated names from the CLI (`shiny-words-lay.md`) are fine but unhelpful. Prefer a name that
describes the change: `avoid-domain-loading-shift.md`, `drop-expo-sdk-53.md`.

## The two forms

### Package changeset

For any change to a **published** package:

```md
---
'@clerk/nextjs': patch
---

Remove the redundant `https://*.client.protect.clerk.com` source from CSP headers generated by `clerkMiddleware()`.
```

Multiple packages in one change get one entry each, under the same delimiters:

```md
---
'@clerk/expo': major
'@clerk/expo-passkeys': major
---

Drop support for Expo SDK 53. The minimum supported version is now Expo SDK 54.
```

Single quotes around the package name. A blank line after the closing `---`, then the body.

### Empty changeset

For changes that touch **no published package**: tooling, CI, repo config, docs, or a private
package. The whole file is exactly two delimiters and nothing else:

```md
---
---
```

Write it with the Write tool like any other file. If you must use the shell,
`printf -- '---\n---\n' > .changeset/<name>.md` produces the same bytes, but there is no reason to
verify it with `cat -A` or `od -c` afterwards. The format has no hidden characters.

## Which packages to list

List every published package whose **own** source you changed. Do not list dependents: changesets
bumps them automatically (`updateInternalDependencies: patch` in `.changeset/config.json`), so
listing `@clerk/nextjs` because you changed `@clerk/shared` is wrong and inflates its release notes.

Private packages never appear in a changeset. In this repo the private ones are:

| Package | Status |
| ----------------- | ---------------------------- |
| `@clerk/swingset` | private, component workbench |
| `@clerk/msw` | private, test mocks |
| `@clerk/headless` | private |

Everything else under `packages/*` is published, **including `@clerk/ui` and `@clerk/clerk-js`**.
You do not need to check the `private` field; if it is not one of the three above, it publishes.

If a change touches only private packages, that is an empty changeset.

## Choosing the bump

| Bump | When |
| ------- | ------------------------------------------------------------------------------------------------------- |
| `patch` | Bug fix, internal refactor, style/visual change, perf work. No API surface change. |
| `minor` | New export, new option, new component, added behavior. Existing callers keep working untouched. |
| `major` | Removing or renaming a public export, changing a signature, or any behavior existing callers depend on. |

`major` is not a solo decision. It requires the `!allow-major` label and passes
`.github/workflows/major-version-check.yml`. Before writing one for `clerk-js` or `ui`, run the
breaking-change check in the `clerk-monorepo` skill. A new `clerk-js` runtime loads into apps
pinned to older framework SDKs, so a removal breaks production for users who never upgraded.

APIs prefixed `__internal_` / `__experimental_`, or exported from an `/experimental` subpath, carry
no SemVer guarantee. Changing them is a `patch`.

## Writing the body

The body is a changelog entry read by someone upgrading the package. It is not a summary of your
diff. The reader has no idea which files you touched and does not care.

Write what changed for a consumer and what, if anything, they must do about it.

Good:

> Fix a crash on Android in Expo Go where rendering `<ClerkProvider>` failed with
> `Cannot find native module 'ClerkExpo'`, even for JavaScript-only flows that use no native components.

Bad:

> Refactor `ClerkExpoModule` resolution in `native-module.ts` to use optional requires and update
> the corresponding tests.

Rules that hold across the repo's history:

- Lead with the user-visible effect, in the present tense: "Fix…", "Add…", "Remove…", "Drop support for…".
- Name public API in backticks. Never name internal file paths.
- For `major`, the body **must** tell the reader how to migrate. Include a short before/after code
block when the migration is not obvious from prose alone.
- One or two sentences is normal. Length is earned by migration steps, not by detail about the work.

## Verify

```bash
# Exactly what CI runs. Reports the bumps your changesets produce.
pnpm changeset status --since=origin/main
```

Then confirm the file is staged. An unstaged changeset fails CI the same as a missing one:

```bash
git status --short .changeset/
```

## Amending an existing changeset

If the branch already has a changeset and you are adding to the same change, edit that file rather
than adding a second one. Two changesets for one package produce two bullet points in the changelog
for what the reader experiences as a single change.

Check first:

```bash
git diff --name-only origin/main...HEAD -- .changeset/
```
2 changes: 1 addition & 1 deletion .claude/skills/clerk-monorepo/references/package-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ backwards-compatibility contract (see `breaking-changes.md`).
| `@clerk/msw` | tooling | | MSW request handlers for mocking the Clerk API in tests. Private (not published). |
| `@clerk/swingset` | tooling | | Component explorer for `@clerk/ui`'s Mosaic design system. Private (not published). |
| `@clerk/upgrade` | tooling | | CLI codemod tool for upgrading consumers between SDK versions. |
| `@clerk/eslint-plugin` | tooling | | ESLint plugin enforcing Clerk patterns across JavaScript frameworks (lint rules shipped to apps). Published. |
| `@clerk/eslint-plugin` | tooling | | ESLint plugin enforcing Clerk patterns across JavaScript frameworks (lint rules shipped to apps). Published. |

Tests: `pnpm turbo test --filter=@clerk/<name>` (or `pnpm --filter @clerk/<name> test` after a
build). Most packages use vitest; `@clerk/backend` runs a multi-runtime suite (node + edge +
Expand Down
Loading