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
9 changes: 7 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

Guidance for Claude Code when working in this repository.

## Work Log
## Notes and sync

ALWAYS log meaningful changes as they are completed — not at the end of the session. Each branch gets its own file in `work-log/`. See [`work-log/CLAUDE.md`](work-log/CLAUDE.md) for format.
You must do the following every time you work on something:

1. ALWAYS log meaningful changes as they are completed. Each branch gets its own file in `work-log/`. See [`work-log/CLAUDE.md`](work-log/CLAUDE.md).
2. After editing any files in `latest/`, run `node scripts/sync-latest-to-next.js <file-or-dir>` to apply the same changes to `next/`.

## Products

Expand All @@ -26,6 +29,8 @@ Each versioned product directory has:
- `next/` — active development. Default working directory for new content.
- `v0.53/`, `v10.1.x/`, etc. — archived versions. Do not edit these.

Comment on lines 29 to 31
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Duplicate sync instruction

The instruction to run sync-latest-to-next.js after editing latest/ appears twice: once in the "Notes and sync" numbered list (item 2, lines 10-11) and again as a standalone paragraph here. Having it in two places risks the two versions drifting and makes CLAUDE.md longer without adding clarity.

After editing files in `latest/`, run `node scripts/sync-latest-to-next.js <file-or-dir>` to apply the same changes to `next/`.

## Writing Style

- No bold or italic text in documentation content
Expand Down
172 changes: 172 additions & 0 deletions scripts/sync-latest-to-next.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
#!/usr/bin/env node

/**
* sync-latest-to-next.js
*
* Copies one or more files from a product's latest/ directory to the
* equivalent path in next/, rewriting internal links along the way.
*
* Usage:
* node scripts/sync-latest-to-next.js <file|dir> [file2|dir2 ...]
*
* Examples:
* node scripts/sync-latest-to-next.js sdk/latest/learn/concepts/baseapp.mdx
* node scripts/sync-latest-to-next.js sdk/latest/
* node scripts/sync-latest-to-next.js sdk/latest/ hub/latest/overview.mdx
*
* The script rewrites /sdk/latest/ → /sdk/next/ in link text (preserving
* external https:// URLs). Paths must be relative to the repo root.
*/

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.join(__dirname, '..');

const PRODUCTS = ['evm', 'sdk', 'hub', 'cometbft', 'ibc', 'skip-go', 'enterprise'];

function usage() {
console.error('Usage: node scripts/sync-latest-to-next.js <file> [file2 ...]');
console.error(' Files must be paths relative to the repo root, e.g.:');
console.error(' sdk/latest/learn/concepts/baseapp.mdx');
process.exit(1);
}

function extractFrontMatter(content) {
const match = content.match(/^---\n[\s\S]*?\n---\n/);
return match ? match[0] : '';
}

function extractBody(content) {
const match = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
return match ? match[1] : content;
}

function rewriteLinks(content, product) {
// Only rewrite /<product>/latest/ → /<product>/next/ for the product being synced.
// Cross-product links (e.g. /cometbft/latest/) are left unchanged — next/ files
// intentionally reference other products' latest/ versions.
const escapedProduct = product.replace(/-/g, '\\-');
const re = new RegExp(`(https?:\\/\\/\\S+)|\\/${escapedProduct}\\/latest\\/`, 'g');
return content.replace(re, (match, externalUrl) => {
if (externalUrl) return externalUrl;
return `/${product}/next/`;
});
Comment on lines +47 to +56
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Comment says "https://" but regex also matches "http://"

The comment says the function preserves "external https:// URLs", but the regex https?:\/\/ matches both http:// and https://. The code behaviour is correct (preserving both is the right thing), but the comment is misleading.

}

function syncFile(relPath) {
// Normalise: strip leading ./
relPath = relPath.replace(/^\.\//, '');

// Validate the path contains /latest/
const latestMatch = relPath.match(/^([^/]+)\/latest\/(.+)$/);
if (!latestMatch) {
console.error(`✗ ${relPath}`);
console.error(' Path must be under a product\'s latest/ directory.');
return false;
}

const [, product, subPath] = latestMatch;

if (!PRODUCTS.includes(product)) {
console.error(`✗ ${relPath}`);
console.error(` Unknown product "${product}". Expected one of: ${PRODUCTS.join(', ')}`);
return false;
}

const srcPath = path.join(REPO_ROOT, relPath);
const destPath = path.join(REPO_ROOT, product, 'next', subPath);

if (!fs.existsSync(srcPath)) {
console.error(`✗ ${relPath}`);
console.error(` File not found: ${srcPath}`);
return false;
}

const srcContent = fs.readFileSync(srcPath, 'utf8');
const srcBody = extractBody(srcContent);
const rewrittenBody = rewriteLinks(srcBody, product);

let rewritten;
if (fs.existsSync(destPath)) {
// Keep the destination's front matter exactly as-is (preserves noindex, canonical,
// and their positions). Only the body content is synced from latest/.
const destContent = fs.readFileSync(destPath, 'utf8');
const destFrontMatter = extractFrontMatter(destContent);
rewritten = destFrontMatter + rewrittenBody;
} else {
// New file — use source front matter with links rewritten
rewritten = rewriteLinks(srcContent, product);
}

const destDir = path.dirname(destPath);
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
console.log(` Created directory: ${path.relative(REPO_ROOT, destDir)}`);
}

const destExists = fs.existsSync(destPath);
fs.writeFileSync(destPath, rewritten, 'utf8');

const destRelPath = path.relative(REPO_ROOT, destPath);
console.log(`✓ ${relPath} → ${destRelPath} ${destExists ? '(updated)' : '(created)'}`);
return true;
}

function collectFiles(argPath) {
const absPath = path.isAbsolute(argPath)
? argPath
: path.join(REPO_ROOT, argPath);

if (!fs.existsSync(absPath)) {
console.error(`✗ Not found: ${argPath}`);
return [];
}

const stat = fs.statSync(absPath);
if (stat.isFile()) {
return [argPath.replace(/^\.\//, '')];
}

if (stat.isDirectory()) {
const files = [];
function walk(dir) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(full);
} else if (entry.name.endsWith('.mdx')) {
files.push(path.relative(REPO_ROOT, full));
}
}
}
walk(absPath);
return files;
}

return [];
}

// --- main ---

const args = process.argv.slice(2);
if (args.length === 0) usage();

const allFiles = args.flatMap(collectFiles);

let ok = 0;
let fail = 0;

for (const f of allFiles) {
if (syncFile(f)) ok++; else fail++;
}

console.log('');
if (fail === 0) {
console.log(`Done. ${ok} file(s) synced to next/. Review the diff before committing.`);
} else {
console.log(`Done. ${ok} succeeded, ${fail} failed.`);
process.exit(1);
}
83 changes: 54 additions & 29 deletions sdk/latest/guides/upgrades/upgrade.mdx
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
---
title: Upgrading Modules
title: Upgrades and Store Migrations
---

<Warning>
Read and understand all of this page before running a migration on a live chain.
</Warning>

<Note>
**Synopsis**
In-place store migrations allow modules to upgrade to new versions that include breaking changes. This document covers both the module-side (writing migrations) and the app-side (running migrations during an upgrade).
</Note>

The Cosmos SDK supports two approaches to chain upgrades: exporting the entire application state to JSON and starting fresh with a modified genesis file, or performing in-place store migrations that update state directly. In-place migrations are significantly faster for chains with large state and are the standard approach for live networks.

This page covers how to write module migrations and how to run them inside an upgrade handler in your app.

## Consensus Version

Successful upgrades of existing modules require each `AppModule` to implement the function `ConsensusVersion() uint64`.
Expand Down Expand Up @@ -49,53 +57,36 @@ Since these migrations are functions that need access to a Keeper's store, use a
package keeper

import (

sdk "github.com/cosmos/cosmos-sdk/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/bank/exported"
v2 "github.com/cosmos/cosmos-sdk/x/bank/migrations/v2"
v3 "github.com/cosmos/cosmos-sdk/x/bank/migrations/v3"
v4 "github.com/cosmos/cosmos-sdk/x/bank/migrations/v4"
v2 "github.com/cosmos/cosmos-sdk/x/bank/migrations/v2"
v3 "github.com/cosmos/cosmos-sdk/x/bank/migrations/v3"
v4 "github.com/cosmos/cosmos-sdk/x/bank/migrations/v4"
)

// Migrator is a struct for handling in-place store migrations.
type Migrator struct {
keeper BaseKeeper
legacySubspace exported.Subspace
legacySubspace exported.Subspace
}

// NewMigrator returns a new Migrator.
func NewMigrator(keeper BaseKeeper, legacySubspace exported.Subspace)

Migrator {
return Migrator{
keeper: keeper, legacySubspace: legacySubspace
}
func NewMigrator(keeper BaseKeeper, legacySubspace exported.Subspace) Migrator {
return Migrator{keeper: keeper, legacySubspace: legacySubspace}
}

// Migrate1to2 migrates from version 1 to 2.
func (m Migrator)

Migrate1to2(ctx sdk.Context)

error {
func (m Migrator) Migrate1to2(ctx sdk.Context) error {
return v2.MigrateStore(ctx, m.keeper.storeService, m.keeper.cdc)
}

// Migrate2to3 migrates x/bank storage from version 2 to 3.
func (m Migrator)

Migrate2to3(ctx sdk.Context)

error {
func (m Migrator) Migrate2to3(ctx sdk.Context) error {
return v3.MigrateStore(ctx, m.keeper.storeService, m.keeper.cdc)
}

// Migrate3to4 migrates x/bank storage from version 3 to 4.
func (m Migrator)

Migrate3to4(ctx sdk.Context)

error {
func (m Migrator) Migrate3to4(ctx sdk.Context) error {
m.MigrateSendEnabledParams(ctx)
return v4.MigrateStore(ctx, m.keeper.storeService, m.legacySubspace, m.keeper.cdc)
}
Expand All @@ -116,7 +107,13 @@ To see example code of changes that were implemented in a migration of balance k

## Running Migrations in the App

Once modules have registered their migrations, the app runs them inside an `UpgradeHandler` using `RunMigrations`. The handler is registered in `app.go`:
Once modules have registered their migrations, the app runs them inside an `UpgradeHandler`. The upgrade handler type is:

```go
type UpgradeHandler func(ctx context.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error)
```

The handler receives the `VersionMap` stored by `x/upgrade` (reflecting the consensus versions from the previous binary), performs any additional upgrade logic, and must return the updated `VersionMap` from `RunMigrations`. Register the handler in `app.go`:

```go
app.UpgradeKeeper.SetUpgradeHandler("my-plan", func(ctx context.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) {
Expand Down Expand Up @@ -170,3 +167,31 @@ func (app *MyApp) InitChainer(ctx sdk.Context, req *abci.RequestInitChain) (*abc
```

This lets the Cosmos SDK detect when modules with newer consensus versions are introduced in a future upgrade.

### Overwriting genesis functions

The SDK provides modules that app developers can import, and those modules often already have an `InitGenesis` function. If you want to run a custom genesis function for one of those modules during an upgrade instead of the default one, you must both call your custom function in the handler AND manually set that module's consensus version in `fromVM`. Without the second step, `RunMigrations` will run the module's existing `InitGenesis` even though you already initialized it.

<Warning>
You must manually set the consensus version in `fromVM` for any module whose `InitGenesis` you are overriding. If you don't, the SDK will call the module's default `InitGenesis` in addition to your custom one.
</Warning>

```go
import foo "github.com/my/module/foo"

app.UpgradeKeeper.SetUpgradeHandler("my-plan", func(ctx context.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) {
// Prevent RunMigrations from calling foo's default InitGenesis.
fromVM["foo"] = foo.AppModule{}.ConsensusVersion()

// Run your custom genesis initialization for foo.
app.ModuleManager.Modules["foo"].(module.HasGenesis).InitGenesis(ctx, app.appCodec, myCustomGenesisState)

return app.ModuleManager.RunMigrations(ctx, app.Configurator(), fromVM)
})
```

## Syncing a Full Node to an Upgraded Blockchain

A full node joining an already-upgraded chain must start from the initial binary that the chain used at genesis and replay all historical upgrades. If all upgrade plans include binary download instructions, Cosmovisor's auto-download mode handles this automatically. Otherwise, you must provide each historical binary manually.

See the [Cosmovisor](/sdk/latest/guides/upgrades/cosmovisor) guide for setup and configuration.
Loading