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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,9 @@ MODE=production pnpm run build:web

## Releases

The command `pnpm run release` helps to automate the release process.
The command `pnpm run release` automates the release process. Run it and follow the prompts.

Run the command and follow the prompts.
See [RELEASE.md](RELEASE.md) for the full flow, including which merge method to use for each PR type.

## MixPanel

Expand Down
73 changes: 31 additions & 42 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,59 +1,48 @@
# Release process

Single command, idempotent. Run `pnpm release` at any point - it figures out where you are and does the next step.
> [!WARNING]
> **Merge release and hotfix PRs into `main` with "Create a merge commit". Never squash.**
> A squashed release breaks the script's tracking of what has shipped. If it happens, stop and ask in #engineering before running anything else.

## Regular release
Everything goes through `pnpm release`. Run it, pick the release type, and follow the prompts. It works out which step you're on, so re-running it is always safe.

1. Run `pnpm release`, select Regular, confirm -> creates **prerelease PR** (develop -> release)
2. **Merge prerelease PR on GitHub**
3. Run `pnpm release` again -> generates AI release notes, creates **release PR** (release -> main)
4. **Merge release PR on GitHub** when CI passes
5. Run `pnpm release` again -> tags main with version, creates **private sync PR** (main -> private)
6. **Merge private sync PR on GitHub**
7. Run `pnpm release` again -> "done, nothing to do"
Requires `gh` (authenticated). The `claude` CLI is optional and only used to write the release notes.

## Hotfix release
## Regular release

1. Run `pnpm release`, select Hotfix, pick commits -> creates **hotfix PR** (hotfix/vX.Y.Z -> main)
2. **Merge hotfix PR on GitHub**
3. Run `pnpm release` again -> tags, creates **private sync PR** + **backmerge PR** (main -> develop)
4. **Merge both PRs on GitHub**
1. Run `pnpm release` and choose **Regular**. Confirm the commit list. The script opens the release PR (`release` -> `main`).
2. Test the `release` deployment.
3. Merge the release PR with **"Create a merge commit"**.
4. Run `pnpm release` and choose **Regular** again. The script tags the version and opens the private sync PR, which merges itself once checks pass.

## How it works
## Release fix

The script derives its state from observable git/GitHub state (branch SHAs, tags, open PRs) rather than tracking state in a file. This makes it idempotent - you can run it as many times as you want without creating duplicates or re-tagging.
Adds a fix to the release PR that's already open.

### Regular release states
1. Merge the fix into `develop` as usual.
2. Run `pnpm release` and choose **Release fix**. Pick the commits to add.

```
idle (no prerelease) -> create develop -> release PR
prerelease_pr_open -> waiting for merge on GitHub
idle (prerelease merged) -> create release -> main PR with AI notes
release_pr_open -> waiting for merge on GitHub
merged_untagged -> tag main, create main -> private PR
tagged_private_stale -> waiting for private sync merge on GitHub
done -> nothing to do
```
The next release's notes may list these commits again. Delete those lines from the PR body.

### Hotfix states
## Hotfix

```
idle -> cherry-pick commits, create hotfix -> main PR
hotfix_pr_open -> waiting for merge on GitHub
merged_untagged -> tag main, create private sync + backmerge PRs
tagged_private_stale -> waiting for PR merges on GitHub
done -> nothing to do
```
Ships specific commits from `develop` straight to production.

## Branch protection
1. Run `pnpm release` and choose **Hotfix**. Pick the commits. The script opens the hotfix PR (`hotfix/vX.Y.Z` -> `main`).
2. Merge the hotfix PR with **"Create a merge commit"**.
3. Run `pnpm release` and choose **Hotfix** again. The script tags the version and opens the private sync PR, which merges itself once checks pass.

All four branches are protected - no direct pushes:
## Merge methods

- **main**: production
- **develop**: development
- **release**: staging between develop and main
- **private**: tracks main with different env vars (Cloudflare deployment)
| PR | Merge with |
| --- | --- |
| Feature/fix -> `develop` | Squash |
| Release -> `main` | **Merge commit** |
| Hotfix -> `main` | **Merge commit** |
| Private sync -> `private` | Squash (automatic) |

## AI release notes
## Rules

The release PR body is AI-generated using Claude CLI. It groups commits by feature domain, separates production changes from dev-only (feature-flagged) changes, and includes testing notes. Falls back to a raw commit list if Claude is unavailable.
- Don't push to `release`, `main`, or `private` yourself. Let the script do it.
- Don't edit the release PR title (`chore: release vX.Y.Z`).
- Don't create tags by hand. The script tags after each merge.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"postinstall": "[ -d .git ] && git config --local include.path '.gitconfig' || true",
"release": "pnpm exec tsx ./scripts/release.ts",
"scrape-walletconnect-dapps": "pnpm exec tsx ./scripts/scrapeWalletConnectRegistry.ts",
"test": "pnpm exec vitest run --project packages --project web --project swap-widget",
"test": "pnpm exec vitest run --project packages --project web --project swap-widget --project scripts",
"test:web": "pnpm exec vitest run --project web",
"test:packages": "pnpm exec vitest run --project packages",
"translations:count-words-invalidate-since": "pnpm exec tsx ./scripts/translations/countWordsInvalidateSince.ts",
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"eip155:10/erc20:0x94b008aa00579c1307b0ef2c499ad98a8ce58e58":"tether","eip155:10/erc20:0x0b2c639c533813f4aa9d7837caf62653d097ff85":"usd-coin","eip155:10/erc20:0x1f32b1c2345538c0c6f582fcb022739c4a194ebb":"lido-finance-wsteth","eip155:10/erc20:0x68f180fcce6836688e9084f035309e29bf0a2095":"wrapped-bitcoin","eip155:10/erc20:0x350a791bfc2c21f9ed5d10980dad2e2638ffa7f6":"chainlink","eip155:10/erc20:0x4200000000000000000000000000000000000006":"weth","eip155:10/erc20:0x5a7facb970d094b6c7ff1df0ea68d99e6e73cbff":"wrapped-eeth","eip155:10/erc20:0x6fd9d7ad17242c41f7131d257212c54a0e816691":"uniswap","eip155:10/erc20:0xdc6ff44d5d932cbd77b52e5612ba0529dc6226f1":"worldcoin-org","eip155:10/erc20:0x9bcef72be871e61ed4fbbc7630889bee758eb81d":"rocket-pool-eth","eip155:10/erc20:0x0994206dfe8de6ec6920ff4d779b0d950605fb53":"curve-dao-token","eip155:10/erc20:0x6985884c4392d348587b19cb9eaaf157f13271cd":"layerzero","eip155:10/erc20:0x4200000000000000000000000000000000000042":"optimism-ethereum","eip155:10/erc20:0x33800de7e817a70a694f31476313a7c572bba100":"derive","eip155:10/erc20:0x2e3d870790dc77a83dd1d18184acc7439a53f475":"frax","eip155:10/erc20:0xc6bdfc4f2e90196738873e824a9efa03f7c64176":"vicicoin","eip155:10/erc20:0x80eede496655fb9047dd39d9f418d5483ed600df":"frax-usd","eip155:10/erc20:0xb0ffa8000886e57f86dd5264b9582b2ad87b2b91":"wormhole","eip155:10/erc20:0x3eaeb77b03dbc0f6321ae1b72b2e9adb0f60112b":"sushiswap","eip155:10/erc20:0x8700daec35af8ff88c16bdf0418774cb3d7599b4":"synthetix-network-token","eip155:10/erc20:0x5a5fff6f753d7c11a56a52fe47a177a87e431655":"synapse-2","eip155:10/erc20:0xaeaeed23478c3a4b798e4ed40d8b7f41366ae861":"ankr","eip155:10/erc20:0x4f8a84c442f9675610c680990eddb2ccddb8ab6f":"frankencoin","eip155:10/erc20:0x03569cc076654f82679c4ba2124d64774781b01d":"liquity-v2","eip155:10/erc20:0x9923db8d7fbacc2e69e87fad19b886c81cd74979":"hyperlane","eip155:10/erc20:0xa00e3a3511aac35ca78530c85007afcd31753819":"kyber-network","eip155:10/erc20:0xec6adef5e1006bb305bb1975333e8fc4071295bf":"cartesi","eip155:10/erc20:0x14778860e937f509e651192a90589de711fb88a9":"cyberconnect","eip155:10/erc20:0xe05a08226c49b636acf99c40da8dc6af83ce5bb3":"ankreth","eip155:10/erc20:0xd6909e9e702024eb93312b989ee46794c0fb1c9d":"biconomy","eip155:10/erc20:0x8c6f28f2f1a3c87f0f938b96d27520d9751ec8d9":"susd","eip155:10/erc20:0xaf9fe3b5ccdae78188b1f8b9a49da7ae9510f151":"dhedge-dao","eip155:10/erc20:0xef4461891dfb3ac8572ccf7c794664a8dd927945":"walletconnect-token","eip155:10/erc20:0x4e200fe2f3efb977d5fd9c430a41531fb04d97b8":"orderly-network","eip155:10/erc20:0xbfd5206962267c7b4b4a8b3d76ac2e1b2a5c4d5e":"osaka-protocol","eip155:10/erc20:0x528cdc92eab044e1e39fe43b9514bfdab4412b98":"giveth","eip155:10/erc20:0xfc2e6e6bcbd49ccf3a5f029c79984372dcbfe527":"autonolas","eip155:10/erc20:0x6a661312938d22a2a0e27f585073e4406903990a":"mahadao","eip155:10/erc20:0x5f8d72676e37197b5011afc64d8641af2fe41fdb":"measurable-data-token","eip155:10/erc20:0xc7edf7b7b3667a06992508e7b156eff794a9e1c8":"persistence","eip155:10/erc20:0xe453d6649643f1f460c371dc3d1da98f7922fe51":"fuse-network","eip155:10/erc20:0x1da650c3b2daa8aa9ff6f661d4156ce24d08a062":"dentacoin"}
{"eip155:10/erc20:0x94b008aa00579c1307b0ef2c499ad98a8ce58e58":"tether","eip155:10/erc20:0x0b2c639c533813f4aa9d7837caf62653d097ff85":"usd-coin","eip155:10/erc20:0x1f32b1c2345538c0c6f582fcb022739c4a194ebb":"lido-finance-wsteth","eip155:10/erc20:0x350a791bfc2c21f9ed5d10980dad2e2638ffa7f6":"chainlink","eip155:10/erc20:0x68f180fcce6836688e9084f035309e29bf0a2095":"wrapped-bitcoin","eip155:10/erc20:0x4200000000000000000000000000000000000006":"weth","eip155:10/erc20:0x5a7facb970d094b6c7ff1df0ea68d99e6e73cbff":"wrapped-eeth","eip155:10/erc20:0x6fd9d7ad17242c41f7131d257212c54a0e816691":"uniswap","eip155:10/erc20:0xdc6ff44d5d932cbd77b52e5612ba0529dc6226f1":"worldcoin-org","eip155:10/erc20:0x9bcef72be871e61ed4fbbc7630889bee758eb81d":"rocket-pool-eth","eip155:10/erc20:0x6985884c4392d348587b19cb9eaaf157f13271cd":"layerzero","eip155:10/erc20:0x0994206dfe8de6ec6920ff4d779b0d950605fb53":"curve-dao-token","eip155:10/erc20:0x4200000000000000000000000000000000000042":"optimism-ethereum","eip155:10/erc20:0x33800de7e817a70a694f31476313a7c572bba100":"derive","eip155:10/erc20:0x2e3d870790dc77a83dd1d18184acc7439a53f475":"frax","eip155:10/erc20:0xc6bdfc4f2e90196738873e824a9efa03f7c64176":"vicicoin","eip155:10/erc20:0x80eede496655fb9047dd39d9f418d5483ed600df":"frax-usd","eip155:10/erc20:0xb0ffa8000886e57f86dd5264b9582b2ad87b2b91":"wormhole","eip155:10/erc20:0x3eaeb77b03dbc0f6321ae1b72b2e9adb0f60112b":"sushiswap","eip155:10/erc20:0x8700daec35af8ff88c16bdf0418774cb3d7599b4":"synthetix-network-token","eip155:10/erc20:0xaeaeed23478c3a4b798e4ed40d8b7f41366ae861":"ankr","eip155:10/erc20:0x4f8a84c442f9675610c680990eddb2ccddb8ab6f":"frankencoin","eip155:10/erc20:0x5a5fff6f753d7c11a56a52fe47a177a87e431655":"synapse-2","eip155:10/erc20:0x03569cc076654f82679c4ba2124d64774781b01d":"liquity-v2","eip155:10/erc20:0x9923db8d7fbacc2e69e87fad19b886c81cd74979":"hyperlane","eip155:10/erc20:0xa00e3a3511aac35ca78530c85007afcd31753819":"kyber-network","eip155:10/erc20:0xec6adef5e1006bb305bb1975333e8fc4071295bf":"cartesi","eip155:10/erc20:0x14778860e937f509e651192a90589de711fb88a9":"cyberconnect","eip155:10/erc20:0xe05a08226c49b636acf99c40da8dc6af83ce5bb3":"ankreth","eip155:10/erc20:0x8c6f28f2f1a3c87f0f938b96d27520d9751ec8d9":"susd","eip155:10/erc20:0xaf9fe3b5ccdae78188b1f8b9a49da7ae9510f151":"dhedge-dao","eip155:10/erc20:0xd6909e9e702024eb93312b989ee46794c0fb1c9d":"biconomy","eip155:10/erc20:0xef4461891dfb3ac8572ccf7c794664a8dd927945":"walletconnect-token","eip155:10/erc20:0x4e200fe2f3efb977d5fd9c430a41531fb04d97b8":"orderly-network","eip155:10/erc20:0xfc2e6e6bcbd49ccf3a5f029c79984372dcbfe527":"autonolas","eip155:10/erc20:0xbfd5206962267c7b4b4a8b3d76ac2e1b2a5c4d5e":"osaka-protocol","eip155:10/erc20:0x528cdc92eab044e1e39fe43b9514bfdab4412b98":"giveth","eip155:10/erc20:0x6a661312938d22a2a0e27f585073e4406903990a":"mahadao","eip155:10/erc20:0x5f8d72676e37197b5011afc64d8641af2fe41fdb":"measurable-data-token","eip155:10/erc20:0xc7edf7b7b3667a06992508e7b156eff794a9e1c8":"persistence","eip155:10/erc20:0xe453d6649643f1f460c371dc3d1da98f7922fe51":"fuse-network","eip155:10/erc20:0x1da650c3b2daa8aa9ff6f661d4156ce24d08a062":"dentacoin"}

Large diffs are not rendered by default.

Loading
Loading