Skip to content

Commit 9daf092

Browse files
authored
Add sync-latest-to-next script and update docs (#289)
Add a new executable script scripts/sync-latest-to-next.js to copy files from a product's latest/ tree to the matching next/ tree, rewriting internal latest→next links and preserving destination front matter. Update CLAUDE.md to require logging and to instruct running the sync script after editing latest/. Revise SDK guides (sdk/latest and sdk/next) for upgrades: add warnings, clarify upgrade handler type and InitGenesis override behavior, and expand notes on syncing full nodes; clean up Go example formatting in migration code. Also trim stray blank lines in ADR/RFC templates and make small copy edits in example tutorial pages and the v0.54 upgrade guide (clarify changelog references and fix anchors).
1 parent 05998a3 commit 9daf092

9 files changed

Lines changed: 299 additions & 70 deletions

File tree

CLAUDE.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
Guidance for Claude Code when working in this repository.
44

5-
## Work Log
5+
## Notes and sync
66

7-
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.
7+
You must do the following every time you work on something:
8+
9+
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).
10+
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/`.
811

912
## Products
1013

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

32+
After editing files in `latest/`, run `node scripts/sync-latest-to-next.js <file-or-dir>` to apply the same changes to `next/`.
33+
2934
## Writing Style
3035

3136
- No bold or italic text in documentation content

scripts/sync-latest-to-next.js

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* sync-latest-to-next.js
5+
*
6+
* Copies one or more files from a product's latest/ directory to the
7+
* equivalent path in next/, rewriting internal links along the way.
8+
*
9+
* Usage:
10+
* node scripts/sync-latest-to-next.js <file|dir> [file2|dir2 ...]
11+
*
12+
* Examples:
13+
* node scripts/sync-latest-to-next.js sdk/latest/learn/concepts/baseapp.mdx
14+
* node scripts/sync-latest-to-next.js sdk/latest/
15+
* node scripts/sync-latest-to-next.js sdk/latest/ hub/latest/overview.mdx
16+
*
17+
* The script rewrites /sdk/latest/ → /sdk/next/ in link text (preserving
18+
* external https:// URLs). Paths must be relative to the repo root.
19+
*/
20+
21+
import fs from 'fs';
22+
import path from 'path';
23+
import { fileURLToPath } from 'url';
24+
25+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
26+
const REPO_ROOT = path.join(__dirname, '..');
27+
28+
const PRODUCTS = ['evm', 'sdk', 'hub', 'cometbft', 'ibc', 'skip-go', 'enterprise'];
29+
30+
function usage() {
31+
console.error('Usage: node scripts/sync-latest-to-next.js <file> [file2 ...]');
32+
console.error(' Files must be paths relative to the repo root, e.g.:');
33+
console.error(' sdk/latest/learn/concepts/baseapp.mdx');
34+
process.exit(1);
35+
}
36+
37+
function extractFrontMatter(content) {
38+
const match = content.match(/^---\n[\s\S]*?\n---\n/);
39+
return match ? match[0] : '';
40+
}
41+
42+
function extractBody(content) {
43+
const match = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
44+
return match ? match[1] : content;
45+
}
46+
47+
function rewriteLinks(content, product) {
48+
// Only rewrite /<product>/latest/ → /<product>/next/ for the product being synced.
49+
// Cross-product links (e.g. /cometbft/latest/) are left unchanged — next/ files
50+
// intentionally reference other products' latest/ versions.
51+
const escapedProduct = product.replace(/-/g, '\\-');
52+
const re = new RegExp(`(https?:\\/\\/\\S+)|\\/${escapedProduct}\\/latest\\/`, 'g');
53+
return content.replace(re, (match, externalUrl) => {
54+
if (externalUrl) return externalUrl;
55+
return `/${product}/next/`;
56+
});
57+
}
58+
59+
function syncFile(relPath) {
60+
// Normalise: strip leading ./
61+
relPath = relPath.replace(/^\.\//, '');
62+
63+
// Validate the path contains /latest/
64+
const latestMatch = relPath.match(/^([^/]+)\/latest\/(.+)$/);
65+
if (!latestMatch) {
66+
console.error(`✗ ${relPath}`);
67+
console.error(' Path must be under a product\'s latest/ directory.');
68+
return false;
69+
}
70+
71+
const [, product, subPath] = latestMatch;
72+
73+
if (!PRODUCTS.includes(product)) {
74+
console.error(`✗ ${relPath}`);
75+
console.error(` Unknown product "${product}". Expected one of: ${PRODUCTS.join(', ')}`);
76+
return false;
77+
}
78+
79+
const srcPath = path.join(REPO_ROOT, relPath);
80+
const destPath = path.join(REPO_ROOT, product, 'next', subPath);
81+
82+
if (!fs.existsSync(srcPath)) {
83+
console.error(`✗ ${relPath}`);
84+
console.error(` File not found: ${srcPath}`);
85+
return false;
86+
}
87+
88+
const srcContent = fs.readFileSync(srcPath, 'utf8');
89+
const srcBody = extractBody(srcContent);
90+
const rewrittenBody = rewriteLinks(srcBody, product);
91+
92+
let rewritten;
93+
if (fs.existsSync(destPath)) {
94+
// Keep the destination's front matter exactly as-is (preserves noindex, canonical,
95+
// and their positions). Only the body content is synced from latest/.
96+
const destContent = fs.readFileSync(destPath, 'utf8');
97+
const destFrontMatter = extractFrontMatter(destContent);
98+
rewritten = destFrontMatter + rewrittenBody;
99+
} else {
100+
// New file — use source front matter with links rewritten
101+
rewritten = rewriteLinks(srcContent, product);
102+
}
103+
104+
const destDir = path.dirname(destPath);
105+
if (!fs.existsSync(destDir)) {
106+
fs.mkdirSync(destDir, { recursive: true });
107+
console.log(` Created directory: ${path.relative(REPO_ROOT, destDir)}`);
108+
}
109+
110+
const destExists = fs.existsSync(destPath);
111+
fs.writeFileSync(destPath, rewritten, 'utf8');
112+
113+
const destRelPath = path.relative(REPO_ROOT, destPath);
114+
console.log(`✓ ${relPath}${destRelPath} ${destExists ? '(updated)' : '(created)'}`);
115+
return true;
116+
}
117+
118+
function collectFiles(argPath) {
119+
const absPath = path.isAbsolute(argPath)
120+
? argPath
121+
: path.join(REPO_ROOT, argPath);
122+
123+
if (!fs.existsSync(absPath)) {
124+
console.error(`✗ Not found: ${argPath}`);
125+
return [];
126+
}
127+
128+
const stat = fs.statSync(absPath);
129+
if (stat.isFile()) {
130+
return [argPath.replace(/^\.\//, '')];
131+
}
132+
133+
if (stat.isDirectory()) {
134+
const files = [];
135+
function walk(dir) {
136+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
137+
const full = path.join(dir, entry.name);
138+
if (entry.isDirectory()) {
139+
walk(full);
140+
} else if (entry.name.endsWith('.mdx')) {
141+
files.push(path.relative(REPO_ROOT, full));
142+
}
143+
}
144+
}
145+
walk(absPath);
146+
return files;
147+
}
148+
149+
return [];
150+
}
151+
152+
// --- main ---
153+
154+
const args = process.argv.slice(2);
155+
if (args.length === 0) usage();
156+
157+
const allFiles = args.flatMap(collectFiles);
158+
159+
let ok = 0;
160+
let fail = 0;
161+
162+
for (const f of allFiles) {
163+
if (syncFile(f)) ok++; else fail++;
164+
}
165+
166+
console.log('');
167+
if (fail === 0) {
168+
console.log(`Done. ${ok} file(s) synced to next/. Review the diff before committing.`);
169+
} else {
170+
console.log(`Done. ${ok} succeeded, ${fail} failed.`);
171+
process.exit(1);
172+
}

sdk/latest/guides/upgrades/upgrade.mdx

Lines changed: 54 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
---
2-
title: Upgrading Modules
2+
title: Upgrades and Store Migrations
33
---
44

5+
<Warning>
6+
Read and understand all of this page before running a migration on a live chain.
7+
</Warning>
8+
59
<Note>
610
**Synopsis**
711
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).
812
</Note>
913

14+
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.
15+
16+
This page covers how to write module migrations and how to run them inside an upgrade handler in your app.
17+
1018
## Consensus Version
1119

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

5159
import (
52-
53-
sdk "github.com/cosmos/cosmos-sdk/types"
60+
sdk "github.com/cosmos/cosmos-sdk/types"
5461
"github.com/cosmos/cosmos-sdk/x/bank/exported"
55-
v2 "github.com/cosmos/cosmos-sdk/x/bank/migrations/v2"
56-
v3 "github.com/cosmos/cosmos-sdk/x/bank/migrations/v3"
57-
v4 "github.com/cosmos/cosmos-sdk/x/bank/migrations/v4"
62+
v2 "github.com/cosmos/cosmos-sdk/x/bank/migrations/v2"
63+
v3 "github.com/cosmos/cosmos-sdk/x/bank/migrations/v3"
64+
v4 "github.com/cosmos/cosmos-sdk/x/bank/migrations/v4"
5865
)
5966

6067
// Migrator is a struct for handling in-place store migrations.
6168
type Migrator struct {
6269
keeper BaseKeeper
63-
legacySubspace exported.Subspace
70+
legacySubspace exported.Subspace
6471
}
6572

6673
// NewMigrator returns a new Migrator.
67-
func NewMigrator(keeper BaseKeeper, legacySubspace exported.Subspace)
68-
69-
Migrator {
70-
return Migrator{
71-
keeper: keeper, legacySubspace: legacySubspace
72-
}
74+
func NewMigrator(keeper BaseKeeper, legacySubspace exported.Subspace) Migrator {
75+
return Migrator{keeper: keeper, legacySubspace: legacySubspace}
7376
}
7477

7578
// Migrate1to2 migrates from version 1 to 2.
76-
func (m Migrator)
77-
78-
Migrate1to2(ctx sdk.Context)
79-
80-
error {
79+
func (m Migrator) Migrate1to2(ctx sdk.Context) error {
8180
return v2.MigrateStore(ctx, m.keeper.storeService, m.keeper.cdc)
8281
}
8382

8483
// Migrate2to3 migrates x/bank storage from version 2 to 3.
85-
func (m Migrator)
86-
87-
Migrate2to3(ctx sdk.Context)
88-
89-
error {
84+
func (m Migrator) Migrate2to3(ctx sdk.Context) error {
9085
return v3.MigrateStore(ctx, m.keeper.storeService, m.keeper.cdc)
9186
}
9287

9388
// Migrate3to4 migrates x/bank storage from version 3 to 4.
94-
func (m Migrator)
95-
96-
Migrate3to4(ctx sdk.Context)
97-
98-
error {
89+
func (m Migrator) Migrate3to4(ctx sdk.Context) error {
9990
m.MigrateSendEnabledParams(ctx)
10091
return v4.MigrateStore(ctx, m.keeper.storeService, m.legacySubspace, m.keeper.cdc)
10192
}
@@ -116,7 +107,13 @@ To see example code of changes that were implemented in a migration of balance k
116107

117108
## Running Migrations in the App
118109

119-
Once modules have registered their migrations, the app runs them inside an `UpgradeHandler` using `RunMigrations`. The handler is registered in `app.go`:
110+
Once modules have registered their migrations, the app runs them inside an `UpgradeHandler`. The upgrade handler type is:
111+
112+
```go
113+
type UpgradeHandler func(ctx context.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error)
114+
```
115+
116+
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`:
120117

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

172169
This lets the Cosmos SDK detect when modules with newer consensus versions are introduced in a future upgrade.
170+
171+
### Overwriting genesis functions
172+
173+
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.
174+
175+
<Warning>
176+
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.
177+
</Warning>
178+
179+
```go
180+
import foo "github.com/my/module/foo"
181+
182+
app.UpgradeKeeper.SetUpgradeHandler("my-plan", func(ctx context.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) {
183+
// Prevent RunMigrations from calling foo's default InitGenesis.
184+
fromVM["foo"] = foo.AppModule{}.ConsensusVersion()
185+
186+
// Run your custom genesis initialization for foo.
187+
app.ModuleManager.Modules["foo"].(module.HasGenesis).InitGenesis(ctx, app.appCodec, myCustomGenesisState)
188+
189+
return app.ModuleManager.RunMigrations(ctx, app.Configurator(), fromVM)
190+
})
191+
```
192+
193+
## Syncing a Full Node to an Upgraded Blockchain
194+
195+
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.
196+
197+
See the [Cosmovisor](/sdk/latest/guides/upgrades/cosmovisor) guide for setup and configuration.

0 commit comments

Comments
 (0)