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
15 changes: 15 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,18 @@ jobs:
run: pnpm lint
- name: Vitest
run: pnpm run -C packages/sdk test

changesets:
name: Changeset quality
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup Node.js 24.x
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.x
# Pending changesets only — released ones are consumed and deleted, and the check
# needs no install, so this stays fast enough to be a required check.
- name: Lint pending changesets
run: node scripts/lint-changesets.mjs
48 changes: 48 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,54 @@ haptic('light') // fire haptic
- Data fetching: TanStack Query v5 via hooks package (`useQuote`, `useTokenList`, `useRelayChains`)
- localStorage key: `relay-ui-kit` (starred chains, accepted unverified tokens)

## Writing changesets

Every changeset becomes a public changelog entry at
[docs.relay.link/changelog](https://docs.relay.link/changelog). Write it for someone
*using* the package, not for someone reading the diff.

**Required shape** — lead with the outcome, then the effect, then any action:

```md
---
'@relayprotocol/relay-sdk': minor
---

Add TON support: new `@relayprotocol/relay-ton-wallet-adapter` package exporting
`adaptTonWallet`, plus `tonvm` support across the SDK and UI kit.
```

### Rules

1. **State what changed for the reader**, not the mechanics. "Sync SDK types" and "Refactor
the token selector" describe the diff; "Suggested tokens now use the API's `logoURI`"
describes the change.
2. **No commit-style prefixes** — `feat:`, `fix:`, `chore:` belong in the commit, not here.
3. **Name the surface exactly** — endpoint, hook, prop, error code, parameter. Preserve exact
identifiers and versions.
4. **Breaking changes state the migration.** What was removed or renamed, what to use
instead, in the same entry. A breaking change without migration guidance is incomplete.
5. **One changeset per customer outcome.** A change spanning SDK, UI kit, and hooks is one
changeset naming all three packages — not three changesets.
6. **Nothing customer-visible?** Start the body with `[internal]`. That keeps it out of the
public changelog and skips the prose checks. Use it for refactors, test-only changes, and
dependency bumps — not as a way past the linter.
Comment on lines +127 to +129

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 [internal] entries remain in public changelogs

The [internal] marker only bypasses the prose linter; the configured standard Changesets generator does not interpret it as a changelog-exclusion directive. A valid patch changeset beginning with [internal] still bumped the package version and was emitted verbatim into CHANGELOG.md. Contributors following this guidance can therefore publish refactor, test-only, and dependency-maintenance notes that the documentation promises to suppress. Either configure a generator that filters these entries or correct the guidance to match the release behavior.

T-Rex Ran code and verified through T-Rex

7. **Don't guess at impact.** If you cannot state the user-visible effect, say what changed
and let review fill in the rest, or mark it `[internal]`.

### Anti-patterns

| Don't | Do |
|---|---|
| `Sync api types` | `[internal] Sync generated API types` |
| `fix: dead address` | `Fix the Bitcoin dead-address preview error on same-chain quotes` |
| `Refactor EOA detection` | `Detect EOAs before quoting so smart-account routes are not offered to EOAs` |
| `Add mappings` | `Add TRANSACTION_SUBMISSION_FAILED and TRANSACTION_NOT_INCLUDED to the failure reasons surfaced on the transaction page` |

`pnpm lint:changesets` enforces the mechanical minimums — prefixes, weak openers, and a
length floor. It cannot tell whether the writing is good, only whether it is obviously not
prose, so passing it is the floor rather than the goal.

## Build

```bash
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"sdk": "pnpm run -C packages/sdk dev",
"typecheck": "tsc --noEmit",
"prepare": "husky",
"lint": "pnpm run -C packages/ui lint && pnpm run -C packages/hooks lint"
"lint": "pnpm run -C packages/ui lint && pnpm run -C packages/hooks lint",
"lint:changesets": "node scripts/lint-changesets.mjs"
},
"devDependencies": {
"@changesets/cli": "^2.27.1",
Expand Down
83 changes: 83 additions & 0 deletions scripts/lint-changesets.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env node
// Checks pending changesets for the mechanical minimums a public changelog entry needs.
// It cannot judge whether prose is good, only that it is not obviously not-prose: no
// conventional-commit prefixes, no "Sync"/"Bump" openers, enough words to describe an
// effect. See the "Writing changesets" section of AGENTS.md for what good looks like.
//
// A change with genuinely nothing customer-visible to say starts its body with [internal];
// that skips the prose rules and keeps the entry off the public changelog.
//
// Usage: node scripts/lint-changesets.mjs [file...] (defaults to .changeset/*.md)

import { readdirSync, readFileSync } from 'node:fs'
import { basename, join } from 'node:path'

const MIN_CHARS = 20
const MIN_WORDS = 4

const COMMIT_PREFIX = /^(feat|fix|chore|refactor|docs|test|tests|ci|build|perf|style|revert)(\([^)]*\))?!?:/i
// Only openers that never carry an effect. "Refactor X to improve Y" and "Tweak the Z ui"
// do describe one, so they are left alone — a change with no effect uses [internal].
const WEAK_OPENER =
/^(sync|syncs|synced|bump|bumps|bumped|upgrade deps|update deps|update dependencies|cleanup|clean up|wip|misc|various|minor (fixes|changes)|small (fixes|changes)|update types)\b/i

const files =
process.argv.slice(2).length > 0
? process.argv.slice(2)
: readdirSync('.changeset')
.filter((file) => file.endsWith('.md') && basename(file).toLowerCase() !== 'readme.md')
.map((file) => join('.changeset', file))

const problems = []
const fail = (file, rule, detail) => problems.push({ file, rule, detail })

for (const file of files) {
const raw = readFileSync(file, 'utf8')
const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/)

if (!match) {
fail(file, 'frontmatter', 'no --- frontmatter block naming the packages and bump types')
continue
}
if (!/^\s*['"]?@[^'"\s]+['"]?\s*:\s*(patch|minor|major)\s*$/m.test(match[1])) {
fail(file, 'frontmatter', 'no `"@scope/package": patch|minor|major` line')
}

const body = match[2].trim()
if (!body) {
fail(file, 'empty', 'the body is empty — describe the change for someone using the package')
continue
}

if (/^\[internal\]/i.test(body)) continue

const summary = body.split('\n')[0].trim()

if (COMMIT_PREFIX.test(summary)) {
fail(file, 'commit-prefix', `drop the commit-style prefix: ${JSON.stringify(summary.slice(0, 60))}`)
}
if (WEAK_OPENER.test(summary)) {
fail(file, 'weak-opener', `say what changed for the reader, not the mechanics: ${JSON.stringify(summary.slice(0, 60))}`)
}
if (body.length < MIN_CHARS) {
fail(file, 'too-short', `${body.length} characters, minimum ${MIN_CHARS}`)
}
if (body.split(/\s+/).filter(Boolean).length < MIN_WORDS) {
fail(file, 'too-short', `${body.split(/\s+/).filter(Boolean).length} words, minimum ${MIN_WORDS}`)
}
}

if (problems.length === 0) {
console.log(`changesets ok (${files.length} checked)`)
process.exit(0)
}

console.error(`${problems.length} changeset problem(s):\n`)
for (const { file, rule, detail } of problems) console.error(` ${file} [${rule}] ${detail}`)
console.error(`
These are mechanical minimums, not a judgement of the writing. A changeset becomes a public
changelog entry at docs.relay.link/changelog, so write it for someone using the package:
what changed, what it means for them, and what they need to do.

If the change genuinely has no customer-visible effect, start the body with [internal].`)
process.exit(1)