From e6d9a6acd79c73de8cfee5fc59126b535b22e19c Mon Sep 17 00:00:00 2001 From: evanorti <87997759+evanorti@users.noreply.github.com> Date: Thu, 19 Mar 2026 16:31:42 -0400 Subject: [PATCH] Add docs-sync workflow and transform tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a docs sync system and update tutorial docs. Introduces .github/workflows/docs-sync.yml to transform docs/*.md → Mintlify .mdx and open/update PRs on the cosmos/docs repo. Adds scripts/docs-sync/transform.py and tests (scripts/docs-sync/test_transform.py) plus a compiled __pycache__ artifact. Renames and reorganizes tutorial files to numbered names (00-overview.md, 01-05 tutorials) and updates content/links for Mintlify compatibility. Also adds CLAUDE.md (repo/agent context, branch/docs policy) and CHANGELOG.md documenting the changes and sync setup. Overall enables automated bidirectional doc transforms and repository documentation updates. --- .github/workflows/docs-sync.yml | 142 ++++++++++ CHANGELOG.md | 68 +++++ CLAUDE.md | 209 ++++++++++++++ docs/00-overview.md | 34 +++ ...0-prerequisites.md => 01-prerequisites.md} | 33 ++- ...rial-01-quickstart.md => 02-quickstart.md} | 10 +- ...build-a-module.md => 03-build-a-module.md} | 28 +- ...lkthrough.md => 04-counter-walkthrough.md} | 55 ++-- ...-04-run-and-test.md => 05-run-and-test.md} | 55 ++-- .../__pycache__/transform.cpython-312.pyc | Bin 0 -> 9247 bytes scripts/docs-sync/test_transform.py | 266 ++++++++++++++++++ scripts/docs-sync/transform.py | 190 +++++++++++++ 12 files changed, 1022 insertions(+), 68 deletions(-) create mode 100644 .github/workflows/docs-sync.yml create mode 100644 CHANGELOG.md create mode 100644 CLAUDE.md create mode 100644 docs/00-overview.md rename docs/{tutorial-00-prerequisites.md => 01-prerequisites.md} (63%) rename docs/{tutorial-01-quickstart.md => 02-quickstart.md} (78%) rename docs/{tutorial-02-build-a-module.md => 03-build-a-module.md} (90%) rename docs/{tutorial-03-counter-walkthrough.md => 04-counter-walkthrough.md} (84%) rename docs/{tutorial-04-run-and-test.md => 05-run-and-test.md} (72%) create mode 100644 scripts/docs-sync/__pycache__/transform.cpython-312.pyc create mode 100644 scripts/docs-sync/test_transform.py create mode 100644 scripts/docs-sync/transform.py diff --git a/.github/workflows/docs-sync.yml b/.github/workflows/docs-sync.yml new file mode 100644 index 0000000..2a3fc31 --- /dev/null +++ b/.github/workflows/docs-sync.yml @@ -0,0 +1,142 @@ +name: Docs Sync → cosmos/docs + +# Runs when docs/ files change on main. +# Transforms .md → .mdx and opens a PR on the docs site repo. +# If a sync PR is already open, updates it instead of opening a new one. +# Skip if the commit was itself produced by the sync (loop guard). + +on: + push: + branches: + - main + paths: + - "docs/**" + +jobs: + sync: + name: Sync docs to cosmos/docs + runs-on: ubuntu-latest + # Loop guard: skip commits that the docs-sync bot created + if: "!contains(github.event.head_commit.message, '[docs-sync]')" + + steps: + - name: Checkout example repo + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Checkout cosmos/docs + uses: actions/checkout@v4 + with: + repository: cosmos/docs + # Fine-grained PAT with contents:write and pull-requests:write on cosmos/docs + token: ${{ secrets.DOCS_REPO_TOKEN }} + path: cosmos-docs + + - name: Transform docs → Mintlify format + run: | + python3 scripts/docs-sync/transform.py \ + --direction to-mintlify \ + --input docs/ \ + --output-dir cosmos-docs/sdk/next/tutorials/example/ + + - name: Check for changes + id: diff + run: | + cd cosmos-docs + git diff --quiet && echo "changed=false" >> "$GITHUB_OUTPUT" || echo "changed=true" >> "$GITHUB_OUTPUT" + + - name: Open or update PR on cosmos/docs + if: steps.diff.outputs.changed == 'true' + env: + GH_TOKEN: ${{ secrets.DOCS_REPO_TOKEN }} + run: | + cd cosmos-docs + + git config user.name "docs-sync[bot]" + git config user.email "docs-sync[bot]@users.noreply.github.com" + + # Check for an existing open sync PR + EXISTING=$(gh pr list \ + --repo cosmos/docs \ + --label "docs-sync" \ + --state open \ + --json number,headRefName \ + --jq '.[0]') + + if [ -n "$EXISTING" ]; then + PR_NUMBER=$(echo "$EXISTING" | jq -r '.number') + BRANCH=$(echo "$EXISTING" | jq -r '.headRefName') + + # Update the existing branch + git fetch origin "$BRANCH" + git checkout "$BRANCH" + git add sdk/next/tutorials/example/ + git commit -m "docs: sync example tutorials from cosmos/example [docs-sync] + +Auto-synced from cosmos/example@${{ github.sha }} +Source commit: ${{ github.event.head_commit.message }}" + git push origin "$BRANCH" + + gh pr comment "$PR_NUMBER" \ + --repo cosmos/docs \ + --body "$(cat <<'EOF' +## Sync updated + +A new commit was pushed to **cosmos/example** before this PR was merged. This PR's branch has been updated with the latest changes. + +**New commit:** ${{ github.sha }} +**Triggered by:** ${{ github.event.head_commit.message }} + +Please re-review the diff before merging. + +🤖 docs-sync bot +EOF +)" + echo "Updated existing PR #$PR_NUMBER" + + else + # No existing PR — create a new branch and open one + BRANCH="docs-sync/example-$(date +%Y%m%d-%H%M%S)" + git checkout -b "$BRANCH" + git add sdk/next/tutorials/example/ + git commit -m "docs: sync example tutorials from cosmos/example [docs-sync] + +Auto-synced from cosmos/example@${{ github.sha }} +Source commit: ${{ github.event.head_commit.message }}" + git push origin "$BRANCH" + + gh pr create \ + --repo cosmos/docs \ + --head "$BRANCH" \ + --base main \ + --title "docs: sync example tutorials from cosmos/example" \ + --label "docs-sync" \ + --body "$(cat <<'EOF' +## Automated docs sync + +This PR was auto-generated by the [docs-sync workflow](https://github.com/cosmos/example/blob/main/.github/workflows/docs-sync.yml) in **cosmos/example**. + +**Source commit:** ${{ github.sha }} +**Triggered by:** ${{ github.event.head_commit.message }} + +### What changed +Transformed \`docs/*.md\` → \`sdk/next/tutorials/example/*.mdx\`: +- Stripped \`# H1\` headings → YAML frontmatter +- Rewrote \`https://docs.cosmos.network/...\` links → relative paths +- Renamed \`.md\` extensions → \`.mdx\` + +### Review checklist +- [ ] Mintlify frontmatter looks correct +- [ ] Links render correctly in preview +- [ ] Navigation in \`docs.json\` is up to date + +> **Do not edit these files directly** — edit the source in cosmos/example and let the sync bot update them. Changes made here will be synced back automatically. + +🤖 Generated by docs-sync bot +EOF +)" + fi diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..05f51f1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,68 @@ +# Changelog + +All notable changes to this repository are tracked here for agent context. + +## [Unreleased] + +### Docs Changes + +- Renamed tutorial docs to `NN-name.md` format (`01-prerequisites.md` through `05-run-and-test.md`), added `00-overview.md` intro page +- `02-quickstart.md`: rewrote opening paragraph, added Mintlify Note callout linking to prerequisites +- `03-build-a-module.md`: replaced blockquote prerequisite notice with Mintlify Note callout; added links to SDK concept docs throughout (modules, transactions, encoding, keeper, app.go, etc.) +- `04-counter-walkthrough.md`: added anchor links in feature comparison table; added Gas section covering `minimum-gas-prices` in `app.toml`; linked repo in branch switch instruction; removed stale to-do comment +- `05-run-and-test.md`: added Node Configuration section covering `app.toml` and `config.toml` with key settings tables +- `01-prerequisites.md`: updated Go version output to show both Linux and macOS variants; updated Make install instructions to cover both platforms +- Verified `make install` builds successfully on Linux via Docker `golang:1.25` container + +### CLAUDE.md Changes + +- Added Changelog Policy section requiring changelog updates after every change + +--- + +## [2026-03-18] — Docs sync system + file renames + +Branch: `feat/docs-sync` (in `cosmos/example`), `feat/example-tutorial-sync` (in `cosmos/docs`) + +### example repo changes +- Renamed all 5 tutorial docs to drop the `tutorial-NN-` prefix: + - `tutorial-00-prerequisites.md` → `prerequisites.md` + - `tutorial-01-quickstart.md` → `quickstart.md` + - `tutorial-02-build-a-module.md` → `build-a-module.md` + - `tutorial-03-counter-walkthrough.md` → `counter-walkthrough.md` + - `tutorial-04-run-and-test.md` → `run-and-test.md` +- Updated all internal cross-links between tutorial files to use new names +- Added `scripts/docs-sync/transform.py` — bidirectional transform between `.md` and Mintlify `.mdx` format (H1 ↔ frontmatter, absolute ↔ relative links, `.md` ↔ `.mdx` extensions) +- Added `scripts/docs-sync/test_transform.py` — 25 unit + round-trip tests (all passing) +- Added `.github/workflows/docs-sync.yml` — GitHub Action that auto-opens PRs on `cosmos/docs` when `docs/**` changes on `main` + +### cosmos/docs repo changes +- Fixed typo: `prerequisits.mdx` → `prerequisites.mdx` +- Added 5 new `.mdx` files in `sdk/next/tutorials/example/` (transformed from `docs/*.md`) +- Added `Build a Chain` group to `docs.json` navigation under `sdk/next` How-to Guides +- Added `.github/workflows/docs-sync-to-example.yml` — reverse sync action that opens PRs on `cosmos/example` when tutorial `.mdx` files change + +### Setup required (one-time) +- Add secret `DOCS_REPO_TOKEN` to `cosmos/example` (PAT: `contents:write` + `pull-requests:write` on `cosmos/docs`) +- Add secret `EXAMPLE_REPO_TOKEN` to `cosmos/docs` (PAT: `contents:write` + `pull-requests:write` on `cosmos/example`) + +--- + +## [2026-03-18] — Initial setup + +- Added `CLAUDE.md` to document repo purpose, branch strategy, docs policy, and module architecture for future agents. +- Added `CHANGELOG.md` (this file) to track changes over time. + +--- + +## Format + +Each entry should follow: + +``` +## [YYYY-MM-DD] — Short description + +- What changed and why +- Docs site impact (if any) +- Branch(es) affected +``` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..af55400 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,209 @@ +# CLAUDE.md — Agent Context for `example` Repo + +## What This Repo Is + +This is the **Cosmos SDK example application** — a reference implementation showing developers how to build a Cosmos SDK module from scratch and wire it into a chain. It lives at `github.com/cosmos/example`. + +The primary audience is Cosmos SDK developers learning module development. The docs are tutorial-style and are published to the **Cosmos SDK documentation site**. + +--- + +## Branch Strategy + +| Branch | Purpose | +|---|---| +| `main` | Complete, fully-featured counter module with all bells and whistles | +| `tutorial/start` | Blank template — users follow tutorial docs to build the module from scratch | + +**Rule:** `main` always contains the finished module. `tutorial/start` contains the starting scaffolding users begin from. The tutorials in `docs/` walk users from `tutorial/start` → `main`. + +--- + +## Docs Policy + +**Critical:** All docs live in `/docs/` and are published to the Cosmos SDK docs site. **Any change to a doc file here must also be reflected on the Cosmos docs site.** Always flag this when making doc changes. + +### Doc Files + +| File | Content | +|---|---| +| `tutorial-00-prerequisites.md` | Prerequisites: Go, Make, Docker, Git; repo layout overview | +| `tutorial-01-quickstart.md` | Fast path: build, install, run chain, send a tx | +| `tutorial-02-build-a-module.md` | Step-by-step: build a minimal counter module from scratch | +| `tutorial-03-counter-walkthrough.md` | Walk through full module (main branch) — params, fees, auth, errors, telemetry, sim | +| `tutorial-04-run-and-test.md` | Running a local chain, localnet, CLI reference, all test layers | + +--- + +## Module Architecture + +The custom module is `x/counter`. The full build pattern is: + +``` +proto files → make proto-gen → generated types → keeper → msg/query servers → module.go → app.go wiring +``` + +### Key Layers + +- **`proto/example/counter/v1/`** — Protobuf definitions (tx, query, state, genesis) +- **`x/counter/types/`** — Generated + hand-written types (keys, codec, expected keepers) +- **`x/counter/keeper/`** — Keeper (state), MsgServer (txs), QueryServer (queries), telemetry, errors +- **`x/counter/module.go`** — AppModule implementation (genesis, services, simulation, block hooks) +- **`x/counter/autocli.go`** — Auto-generates CLI from proto +- **`app.go`** — Wires counter module into the chain + +### Full Module Features (main branch) + +- State: `count` (uint64) + `params` (MaxAddValue, AddCost) +- Messages: `MsgAdd`, `MsgUpdateParams` (governance-gated) +- Queries: `Count`, `Params` +- Validation: MaxAddValue limit, overflow protection +- Fees: AddCost charged via bank keeper +- Authority: governance module controls param updates +- Errors: named sentinel errors with codes +- Telemetry: OpenTelemetry Int64Counter metric +- Simulation: simsx weighted operations + randomized genesis +- Tests: unit (keeper, msg_server, query_server), E2E, simulation + +--- + +## Docs Sync System + +Docs in `docs/` are kept in sync with the Cosmos docs site repo (`cosmos/docs`) via GitHub Actions + a transform script. + +### File mapping + +| `example` repo | Docs site | +|---|---| +| `docs/prerequisites.md` | `sdk/next/tutorials/example/prerequisites.mdx` | +| `docs/quickstart.md` | `sdk/next/tutorials/example/quickstart.mdx` | +| `docs/build-a-module.md` | `sdk/next/tutorials/example/build-a-module.mdx` | +| `docs/counter-walkthrough.md` | `sdk/next/tutorials/example/counter-walkthrough.mdx` | +| `docs/run-and-test.md` | `sdk/next/tutorials/example/run-and-test.mdx` | + +### Format differences + +| | `example` repo | Docs site (Mintlify) | +|---|---|---| +| Title | `# H1 heading` | YAML frontmatter `title:` + `noindex: true` | +| Docs links | `https://docs.cosmos.network/sdk/next/...` | `sdk/next/...` (relative) | +| File extension | `.md` | `.mdx` | + +### Transform script + +`scripts/docs-sync/transform.py` handles all conversion in both directions: + +```bash +# example → docs site +python3 scripts/docs-sync/transform.py --direction to-mintlify --input docs/ --output-dir /path/to/docs-site/sdk/next/tutorials/example/ + +# docs site → example +python3 scripts/docs-sync/transform.py --direction to-example --input /path/to/docs-site/sdk/next/tutorials/example/ --output-dir docs/ + +# Run tests +python3 scripts/docs-sync/test_transform.py +``` + +### GitHub Actions + +- **`example` → docs site:** `.github/workflows/docs-sync.yml` — triggers on push to `main` when `docs/**` changes, opens a PR on `cosmos/docs`. Requires secret `DOCS_REPO_TOKEN`. +- **docs site → `example`:** `.github/workflows/docs-sync-to-example.yml` in the docs repo — triggers on push to `main` when `sdk/next/tutorials/example/**` changes, opens a PR on `cosmos/example`. Requires secret `EXAMPLE_REPO_TOKEN`. + +### Loop prevention + +Both actions check `!contains(github.event.head_commit.message, '[docs-sync]')` and tag sync commits with `[docs-sync]` to prevent infinite loops. + +### Required secrets to set up + +- In `cosmos/example`: secret `DOCS_REPO_TOKEN` — fine-grained PAT with `contents:write` + `pull-requests:write` on `cosmos/docs` +- In `cosmos/docs`: secret `EXAMPLE_REPO_TOKEN` — fine-grained PAT with `contents:write` + `pull-requests:write` on `cosmos/example` + +--- + +## Generating `tutorial/start` from `main` + +The `tutorial/start` branch is **not manually maintained** — it is generated from `main` using: + +```bash +bash scripts/create-tutorial-branch.sh +``` + +This script (lives on `tutorial/start` branch at `scripts/create-tutorial-branch.sh`): +1. Creates the `tutorial/start` branch from `main` +2. Removes all counter module source files (`x/counter/`, proto files, generated `.pb.go`, tests) +3. Adds `.gitkeep` placeholders so `x/counter/` and `proto/example/counter/v1/` exist on clone +4. Strips counter wiring from `app.go` (imports, keeper field, store key, module registration, etc.) +5. Verifies `go build ./...` still passes +6. Commits the result + +**Rule:** When `main` changes significantly (new features, restructured files), re-run this script to regenerate `tutorial/start`. Don't hand-edit `tutorial/start`. + +--- + +## App Wiring (app.go) + +Counter module integration points in `app.go`: + +- `maccPerms` — counter module account entry (for fee collection) +- `ExampleApp.CounterKeeper` field +- Counter store key in store keys +- `NewKeeper(...)` with bank keeper + authority +- `ModuleManager` registration +- Genesis, export, begin/end blocker order lists + +--- + +## Testing + +```bash +go test ./x/counter/... # Unit tests (fast, no chain) +go test -v -run TestE2ETestSuite ./tests/... # E2E (starts in-process network) +make test-sim-full # Simulation (randomized txs) +make lint # Lint +``` + +--- + +## Development Scripts + +```bash +make build # Compile binary +make install # Install exampled binary +make start # Build + start single-node chain (Chain ID: demo, accounts: alice/bob, denom: stake) +make proto-gen # Regenerate proto (requires Docker) +make localnet-init # Initialize multi-validator localnet +make localnet-start # Start localnet +``` + +--- + +## Changelog Policy + +**After every change to this repo, update `CHANGELOG.md`.** Add an entry under `## [Unreleased]` describing what changed and why. When changes are committed, move the unreleased items into a dated entry. + +--- + +## Writing Style + +- **No em dashes.** Use a period, comma, or colon instead. + +--- + +## Key Constants + +- Binary: `exampled` +- Chain ID (dev): `demo` +- Accounts: `alice`, `bob` +- Denom: `stake` +- Module name: `counter` +- Default params: `add_cost = 100stake`, `max_add_value = 100` + +--- + +## Go Dependencies + +- `github.com/cosmos/cosmos-sdk v0.54.0-rc.1` +- `cosmossdk.io/core v1.1.0` +- `cosmossdk.io/collections v1.4.0` +- `github.com/cometbft/cometbft v0.39.0-beta.2` +- Go 1.25.7 diff --git a/docs/00-overview.md b/docs/00-overview.md new file mode 100644 index 0000000..f04acea --- /dev/null +++ b/docs/00-overview.md @@ -0,0 +1,34 @@ +# Build a Chain + +The Cosmos SDK is a developer-first framework for building custom blockchains. This tutorial series shows you how to build a module from scratch, wire it into a chain, and run it locally, all in minutes. + +By the end, you will have: + +- A working Cosmos SDK chain running on your machine +- A custom module you built yourself, wired into the chain +- A clear mental model of how modules, keepers, messages, and queries fit together + +This series starts from zero; you don't need any prior Cosmos SDK experience to follow along. + +## The example repo + +All tutorials in this series are based on [cosmos/example](https://github.com/cosmos/example), a reference Cosmos SDK chain built around a custom `x/counter` module. + +The repo has two main branches: + +- `main`: the complete chain with the full `x/counter` module wired in. This is used in the [Quickstart guide](./02-quickstart.md). +- `tutorial/start`: the same chain without the counter module. The `x/counter` directory and its app wiring are stripped out so you can build the module from scratch by [following the tutorial](./03-build-a-module.md). + +If you want to follow along and build the module yourself, start from `tutorial/start`. If you want to browse the finished implementation first, use `main`. + +## What's in this series + +1. [Prerequisites](./01-prerequisites.md): Install Go, Make, Docker, and Git. Clone the repo and get familiar with the layout. + +2. [Quickstart](./02-quickstart.md): Build and run the chain in minutes. Submit a transaction, query the result, and see the counter module in action before you build it yourself. + +3. [Build a Module from Scratch](./03-build-a-module.md): Build a minimal counter module step by step: proto definitions, keeper, message server, query server, and app wiring. Start here if you want to understand how a module comes together. + +4. [Full Module Walkthrough](./04-counter-walkthrough.md): Walk through the complete `x/counter` implementation on `main` with additional features. See how to add params, fees, governance-gated updates, sentinel errors, telemetry, and simulation on top of the minimal module. + +5. [Run and Test](./05-run-and-test.md): Learn the full development workflow: running a local chain, using the CLI, and working with the three layers of testing: unit tests, end-to-end tests, and simulation. diff --git a/docs/tutorial-00-prerequisites.md b/docs/01-prerequisites.md similarity index 63% rename from docs/tutorial-00-prerequisites.md rename to docs/01-prerequisites.md index 91ea840..066a6df 100644 --- a/docs/tutorial-00-prerequisites.md +++ b/docs/01-prerequisites.md @@ -2,13 +2,18 @@ Before starting the tutorial, make sure you have the following tools installed. + +This tutorial is intended for macOS and Linux systems. Other systems may have additional requirements. + + ## Go -The example chain requires Go 1.22 or higher. +The example chain requires Go 1.25 or higher. ```bash go version -# go version go1.22.0 darwin/arm64 +# go version go1.25.0 linux/amd64 # Linux +# go version go1.25.0 darwin/arm64 # macOS ``` If Go is not installed, download it from [go.dev/dl](https://go.dev/dl). @@ -22,11 +27,10 @@ make --version # GNU Make 3.81 ``` -Make is pre-installed on most Linux and macOS systems. On macOS, if it is missing, install it with Xcode command line tools: +Make is pre-installed on most Linux and macOS systems. If it is missing: -```bash -xcode-select --install -``` +- **macOS:** `xcode-select --install` +- **Linux (Debian/Ubuntu):** `sudo apt install build-essential` ## Docker @@ -34,7 +38,7 @@ Docker is required to run `make proto-gen`, which generates Go code from the mod ```bash docker --version -# Docker version 24.0.0 +# Docker version 29.2.1 ``` Download Docker from [docs.docker.com/get-docker](https://docs.docker.com/get-docker). @@ -45,17 +49,22 @@ Docker must be running before you execute `make proto-gen`. ```bash git --version -# git version 2.39.0 +# git version 2.52.0 ``` ## Clone the repository +Clone [cosmos/example](https://github.com/cosmos/example) and navigate into it: + ```bash git clone https://github.com/cosmos/example cd example ``` ---- +The repo has two branches used in this tutorial series: + +- `main` — the complete chain with the full `x/counter` module wired in. +- `tutorial/start` — the same chain with the counter module stripped out. Start here if you want to build the module yourself from scratch. ## Repository Layout @@ -64,7 +73,7 @@ After cloning, the repository looks like this: ```text example/ ├── exampled/ # Binary entrypoint (main.go + CLI root command) -├── app.go # Chain application — module wiring lives here +├── app.go # Chain application, module wiring lives here ├── proto/ # Proto definitions for all modules ├── x/ # Module implementations │ └── counter/ # The example counter module @@ -74,6 +83,8 @@ example/ └── Makefile # Build, test, and dev commands ``` +## Where things live + The tutorials in this section will walk you through the most common kinds of chain changes and show you where they usually live in the repo: - Add or modify a module: `x//` and `proto/` @@ -83,4 +94,4 @@ The tutorials in this section will walk you through the most common kinds of cha --- -Next: [Quickstart →](./tutorial-01-quickstart.md) +Next: [Quickstart →](./02-quickstart.md) diff --git a/docs/tutorial-01-quickstart.md b/docs/02-quickstart.md similarity index 78% rename from docs/tutorial-01-quickstart.md rename to docs/02-quickstart.md index 1f2f552..dc884b5 100644 --- a/docs/tutorial-01-quickstart.md +++ b/docs/02-quickstart.md @@ -1,8 +1,12 @@ # Quickstart -`exampled` is a simple Cosmos SDK chain that shows the core pieces of a working app chain. It includes the basic building-block modules for accounts, bank, staking, distribution, slashing, governance, and more. +Building on Cosmos is simple: you can start a chain with a [single command](#start-the-chain). This quickstart gets you from zero to a running chain, a submitted transaction, and a queried result in minutes. -This quickstart gets you running `exampled`, submitting a transaction, and querying the result as quickly as possible. It also includes the `x/counter` module, which stores a single counter value, lets you query the current count, and lets you submit `Add` transactions to increment it. In the next tutorials, you'll build a simple version of this module yourself and then walk through the full implementation and its additional features. +`exampled` is a simple Cosmos SDK chain that shows the core pieces of a working app chain. It includes the basic building-block modules for accounts, bank, staking, distribution, slashing, governance, and more, plus a custom `x/counter` module. In the next tutorials, you'll build a simple version of that module yourself and then walk through the full implementation. + + +Before continuing, make sure you have completed the [Prerequisites](./01-prerequisites.md) to get your environment set up. + ## Install the binary @@ -96,4 +100,4 @@ In the following tutorials, you will: 3. See how modules are wired into a chain and how to run the full test suite -Next: [Build a Module from Scratch →](./tutorial-02-build-a-module.md) +Next: [Build a Module from Scratch →](./03-build-a-module.md) diff --git a/docs/tutorial-02-build-a-module.md b/docs/03-build-a-module.md similarity index 90% rename from docs/tutorial-02-build-a-module.md rename to docs/03-build-a-module.md index 7562706..cf9c727 100644 --- a/docs/tutorial-02-build-a-module.md +++ b/docs/03-build-a-module.md @@ -1,10 +1,12 @@ # Build a Module from Scratch -In [quickstart](./tutorial-01-quickstart.md), you started a chain and submitted a transaction to increase the counter. In this tutorial, you'll build a simple counter module from scratch. It follows the same overall structure as the full `x/counter`, but uses a stripped-down version so you can focus on the core steps of building and wiring a module yourself. +In [quickstart](./02-quickstart.md), you started a chain and submitted a transaction to increase the counter. In this tutorial, you'll build a simple counter module from scratch. It follows the same overall structure as the full `x/counter`, but uses a stripped-down version so you can focus on the core steps of building and wiring a module yourself. -By the end, you'll have built a working module and wired it into a running chain. +By the end, you'll have built a working module and wired it into a running chain. For a deeper dive into how modules work in the Cosmos SDK, see [Intro to Modules](https://docs.cosmos.network/sdk/next/learn/concepts/modules). -> **Install prerequisites:** Before continuing, follow the [Prerequisites guide](./tutorial-00-prerequisites.md) to make sure everything is installed. This tutorial will not work without them. + +Before continuing, you must follow the [Prerequisites guide](./01-prerequisites.md) to make sure everything is installed. + ## Making modules @@ -71,7 +73,7 @@ You should see empty placeholder directories at `x/counter/` and `proto/example/ ## Step 2: Proto files -Proto files are the source of truth for the module's public API. You define messages and services here. +Proto files are the source of truth for the module's public API. You define messages and services here. For a deeper look at how protobuf is used across modules, see [Encoding and Protobuf](https://docs.cosmos.network/sdk/next/learn/concepts/encoding#how-protobuf-is-used-in-modules). In this tutorial, the counter module stores one number, `Add` increases it by the amount the user submits, and the query returns the current value. @@ -87,7 +89,7 @@ Then add the following contents to each file. ### tx.proto -This is the first module file you define. It declares the transaction message shape for `Add`: what the user sends to increment the counter, and what the module returns after handling it. Add the following code to `tx.proto`. +This is the first module file you define. It declares the transaction message shape for `Add`: what the user sends to increment the counter, and what the module returns after handling it. To learn more about how messages are defined and routed, see [Messages](https://docs.cosmos.network/sdk/next/learn/concepts/transactions#messages). Add the following code to `tx.proto`. ```proto syntax = "proto3"; @@ -123,7 +125,7 @@ message MsgAddResponse { ### query.proto -This file defines the read-only gRPC query service and the response type for fetching the current count. Add the following code to `query.proto`. +This file defines the read-only gRPC query service and the response type for fetching the current count. To learn more about how queries differ from transactions, see [Queries](https://docs.cosmos.network/sdk/next/learn/concepts/transactions#queries). Add the following code to `query.proto`. ```proto syntax = "proto3"; @@ -215,7 +217,7 @@ Then add the following contents to each file. ### keys.go -This file defines the module's basic identifiers: the module name used throughout the SDK, and the store key used to claim the module's KV store namespace. +This file defines the module's basic identifiers: the module name used throughout the SDK, and the store key used to claim the module's KV store namespace. For more on how modules access state through store keys, see [How modules access state](https://docs.cosmos.network/sdk/next/learn/concepts/store#how-modules-access-state). ```go // x/counter/types/keys.go @@ -260,7 +262,7 @@ func RegisterInterfaces(registry codectypes.InterfaceRegistry) { ## Step 5: Keeper -In this step, you create the keeper, which is the part of the module that owns the counter state and provides the methods the rest of the module will call. +In this step, you create the keeper, which is the part of the module that owns the counter state and provides the methods the rest of the module will call. For a conceptual overview of the keeper's role, see [Keeper](https://docs.cosmos.network/sdk/next/learn/concepts/modules#keeper). Create the keeper file: @@ -348,7 +350,7 @@ func (k *Keeper) ExportGenesis(ctx context.Context) (*types.GenesisState, error) ## Step 6: MsgServer -In this step, you implement the transaction handler for the generated `MsgServer` interface. This is the code path that runs when a user submits `tx counter add`. +In this step, you implement the transaction handler for the generated `MsgServer` interface. This is the code path that runs when a user submits `tx counter add`. For a conceptual overview of message execution, see [Message execution](https://docs.cosmos.network/sdk/next/learn/concepts/modules#message-execution-msgserver). Create the message server file: @@ -394,7 +396,7 @@ func (m msgServer) Add(ctx context.Context, req *types.MsgAddRequest) (*types.Ms ## Step 7: QueryServer -In this step, you implement the read-only query handler for the generated `QueryServer` interface. This is the code path that runs when someone queries the current counter value. +In this step, you implement the read-only query handler for the generated `QueryServer` interface. This is the code path that runs when someone queries the current counter value. For more on how modules expose queries, see [Queries](https://docs.cosmos.network/sdk/next/learn/concepts/modules#queries). Create the query server file: @@ -598,7 +600,7 @@ func (a AppModule) AutoCLIOptions() *autocliv1.ModuleOptions { ## Step 10: Wire into app.go -In this step, you wire your new module into the application so the chain creates its store, constructs its keeper, and includes it in module startup and genesis handling. +In this step, you wire your new module into the application so the chain creates its store, constructs its keeper, and includes it in module startup and genesis handling. For a full explanation of what `app.go` does and why the wiring order matters, see [app.go Overview](https://docs.cosmos.network/sdk/next/learn/concepts/app-go). Open `app.go`. Each code block below starts with the exact marker comment to look for in the file. Paste your code directly below each marker. @@ -695,7 +697,7 @@ make install make start ``` -This builds and installs `exampled` and then runs [`scripts/local_node.sh`](../../scripts/local_node.sh), which: +This builds and installs `exampled` and then runs `scripts/local_node.sh`, which: - resets the local chain data - initializes genesis - creates and funds the `alice` and `bob` test accounts @@ -738,4 +740,4 @@ Congratulations, you've just created a Cosmos module from scratch and wired it i The simple counter module you built here follows the same structure as the full `x/counter` example in the `main` branch. Next, you'll see how the full module extends that foundation with features like params, fee collection, tests, and more. -Next: [Full Counter Module Walkthrough →](./tutorial-03-counter-walkthrough.md) +Next: [Full Counter Module Walkthrough →](./04-counter-walkthrough.md) diff --git a/docs/tutorial-03-counter-walkthrough.md b/docs/04-counter-walkthrough.md similarity index 84% rename from docs/tutorial-03-counter-walkthrough.md rename to docs/04-counter-walkthrough.md index 6cc037e..270388e 100644 --- a/docs/tutorial-03-counter-walkthrough.md +++ b/docs/04-counter-walkthrough.md @@ -1,6 +1,6 @@ # Full Counter Module Walkthrough -If you came here from the module building tutorial, switch back to the `main` branch first: +If you came here from the module building tutorial, switch back to the `main` branch of the [`cosmos/example` repo](https://github.com/cosmos/example) first: ```bash git checkout main @@ -16,24 +16,24 @@ The full counter in the `main` branch adds quite a bit of functionality to the m | Feature | minimal x/counter | full x/counter | |---|---|---| -| State | `count` | `count` + `params` | -| Messages | `Add` | `Add` + `UpdateParams` | -| Queries | `Count` | `Count` + `Params` | -| Validation | None | `MaxAddValue` limit, overflow check | -| Fees | None | `AddCost` charged via bank module | -| Authority | None | Governance-gated param updates | -| Errors | Generic | Named sentinel errors | -| Telemetry | None | OpenTelemetry counter metric | -| CLI | AutoCLI | AutoCLI + `EnhanceCustomCommand` | -| Simulation | None | `simsx` weighted operations | -| Block hooks | None | `BeginBlock` + `EndBlock` stubs | -| Unit tests | None | Full keeper/msg/query test suite | +| [State](#params-and-authority) | `count` | `count` + `params` | +| [Messages](#params-and-authority) | `Add` | `Add` + `UpdateParams` | +| [Queries](#params-and-authority) | `Count` | `Count` + `Params` | +| [Validation](#expected-keepers-and-fee-collection) | None | `MaxAddValue` limit, overflow check | +| [Fees](#expected-keepers-and-fee-collection) | None | `AddCost` charged via bank module | +| [Authority](#params-and-authority) | None | Governance-gated param updates | +| [Errors](#sentinel-errors) | Generic | Named sentinel errors | +| [Telemetry](#telemetry) | None | OpenTelemetry counter metric | +| [CLI](#autocli) | AutoCLI | AutoCLI + `EnhanceCustomCommand` | +| [Simulation](#simulation) | None | `simsx` weighted operations | +| [Block hooks](#beginblock-and-endblock) | None | `BeginBlock` + `EndBlock` | +| [Unit tests](#unit-tests) | None | Full keeper/msg/query test suite | The wiring code in [`msg_server.go`](https://github.com/cosmos/example/blob/main/x/counter/keeper/msg_server.go), [`query_server.go`](https://github.com/cosmos/example/blob/main/x/counter/keeper/query_server.go), [`module.go`](https://github.com/cosmos/example/blob/main/x/counter/module.go), and [`types/`](https://github.com/cosmos/example/tree/main/x/counter/types) is structurally similar between the two. Much of the new keeper logic lives in a single method: `AddCount` in [`keeper.go`](https://github.com/cosmos/example/blob/main/x/counter/keeper/keeper.go). ## Params and authority -A module param is on-chain configuration that controls how the module behaves without changing the code. +A [module param](https://docs.cosmos.network/sdk/next/learn/concepts/modules#params) is on-chain configuration that controls how the module behaves without changing the code. The full counter adds a `Params` type that lets the chain governance configure the module's behavior at runtime. In the full module, params control how large an `Add` can be and how much it costs. @@ -142,7 +142,7 @@ This pattern, storing authority in the keeper and checking it in `MsgServer`, is ## Expected keepers and fee collection -This section shows the standard Cosmos SDK pattern for module-to-module interaction. `x/counter` uses an expected keeper to call into the bank module and charge a fee for each add operation. +This section shows the standard Cosmos SDK pattern for [module-to-module interaction](https://docs.cosmos.network/sdk/next/learn/concepts/modules#inter-module-access). `x/counter` uses an expected keeper to call into the bank module and charge a fee for each add operation. ### Where the code lives @@ -300,7 +300,7 @@ This lives in the `maccPerms` map in [`app.go`](https://github.com/cosmos/exampl ## Sentinel errors -Rather than returning generic errors, `x/counter` defines named errors with registered codes. That makes failures easier to understand and easier for clients to match on programmatically. +Rather than returning generic errors, `x/counter` defines [named sentinel errors](https://docs.cosmos.network/sdk/next/build/building-modules/errors) with registered codes. That makes failures easier to understand and easier for clients to match on programmatically. ### Where the code lives @@ -320,7 +320,7 @@ Registered errors produce structured error responses on-chain that clients can m ## Telemetry -Telemetry records how often the counter is updated so you can observe module activity in an OpenTelemetry-compatible system. +[Telemetry](https://docs.cosmos.network/sdk/next/learn/advanced/telemetry) records how often the counter is updated so you can observe module activity in an OpenTelemetry-compatible system. ### Where the code lives @@ -348,7 +348,7 @@ func init() { ## AutoCLI -AutoCLI exposes the module's queries and transactions as CLI commands. The full module example keeps the same basic AutoCLI setup as the minimal module and adds the recommended setting for custom command integration. +[AutoCLI](https://docs.cosmos.network/sdk/next/learn/advanced/autocli) exposes the module's queries and transactions as CLI commands. The full module example keeps the same basic AutoCLI setup as the minimal module and adds the recommended setting for custom command integration. ### Where the code lives @@ -394,7 +394,7 @@ func (a AppModule) AutoCLIOptions() *autocliv1.ModuleOptions { ## Simulation -Simulation lets the SDK generate randomized transactions against the module during fuzz-style testing. +[Simulation](https://docs.cosmos.network/sdk/next/build/building-modules/simulator) lets the SDK generate randomized transactions against the module during fuzz-style testing. ### Where the code lives @@ -440,7 +440,7 @@ func (a AppModule) WeightedOperationsX(weights simsx.WeightSource, reg simsx.Reg ## BeginBlock and EndBlock -These hooks let a module run code automatically at the start or end of every block. In `x/counter`, they are purposefully empty to demonstrate where and how these features can be added. +These [hooks](https://docs.cosmos.network/sdk/next/learn/concepts/modules#block-hooks) let a module run code automatically at the start or end of every block. In `x/counter`, they are purposefully empty to demonstrate where and how these features can be added. ### Where the code lives @@ -490,7 +490,7 @@ app.ModuleManager.SetOrderEndBlockers( ## Unit tests -The full module example includes a real test suite for keeper logic, query behavior, message handling, and bank keeper interactions. +The full module example includes a real [test suite](https://docs.cosmos.network/sdk/next/learn/concepts/testing) for keeper logic, query behavior, message handling, and bank keeper interactions. ### Where the code lives @@ -548,7 +548,16 @@ s.bankKeeper.SendCoinsFromAccountToModuleFn = func(...) error { } ``` -Next: [Running and Testing →](./tutorial-04-run-and-test.md) +## Gas +`minimum-gas-prices` in `app.toml` sets the minimum fee a node requires before it will accept and relay a transaction. The local dev chain started by `make start` leaves this empty, so transactions are accepted with no fee beyond the `AddCost` module parameter. - +To require a minimum network fee, set it in `app.toml`: + +```toml +minimum-gas-prices = "0.025stake" +``` + +Transactions that don't meet the minimum will be rejected by the node before they reach your module. This is a per-node setting, not a chain-wide consensus rule, so validators on a live network each configure their own threshold. + +Next: [Running and Testing →](./05-run-and-test.md) diff --git a/docs/tutorial-04-run-and-test.md b/docs/05-run-and-test.md similarity index 72% rename from docs/tutorial-04-run-and-test.md rename to docs/05-run-and-test.md index ff52674..e5682f4 100644 --- a/docs/tutorial-04-run-and-test.md +++ b/docs/05-run-and-test.md @@ -1,8 +1,6 @@ # Running and Testing -Now that you've [built a module from scratch](./tutorial-02-build-a-module.md) and walked through the [full counter module](./tutorial-03-counter-walkthrough.md), the next step is learning the workflow for running and validating a production-ready chain. This page shows how to start the chain locally, interact with it through the CLI, and use the main layers of testing before shipping changes. - ---- +Now that you've [built a module from scratch](./03-build-a-module.md) and walked through the [full counter module](./04-counter-walkthrough.md), the next step is learning the workflow for running and validating a production-ready chain. This page shows how to start the chain locally, interact with it through the CLI, and use the main layers of testing before shipping changes. ## Single-node local chain @@ -33,8 +31,6 @@ make start Re-running `make start` resets state automatically. There is no separate reset command. ---- - ## Localnet (multi-validator) Use localnet when you want a setup that is closer to a real network. It runs multiple validators in Docker so you can test multi-node behavior locally. @@ -60,7 +56,7 @@ make localnet-clean ## CLI reference -Once the chain is running, these are the core CLI commands you'll use to inspect state and submit transactions. +Once the chain is running, these are the core [CLI](https://docs.cosmos.network/sdk/next/learn/concepts/cli-grpc-rest#cli) commands you'll use to inspect state and submit transactions. ### Query commands @@ -105,11 +101,42 @@ These flags are the ones you'll use most often while iterating locally. | `--node` | RPC endpoint (default: `tcp://localhost:26657`) | | `--output json` | Output response as JSON | ---- + +## Node Configuration + +When you run `make start`, the chain creates `~/.exampleapp/config/` automatically and initializes two config files inside it: + +| File | What it controls | +|---|---| +| `app.toml` | SDK application settings: gas prices, pruning, API/gRPC servers, telemetry | +| `config.toml` | CometBFT settings: peer networking, consensus timeouts, mempool, RPC | + +### app.toml + +The most common settings to change during development: + +| Setting | Default | Description | +|---|---|---| +| `minimum-gas-prices` | `"0stake"` | Minimum fee the node accepts before processing a transaction | +| `pruning` | `"default"` | How much historical state to keep (`default`, `nothing`, `everything`, `custom`) | +| `api.enable` | `true` | Enables the REST API on port 1317 | +| `grpc.enable` | `true` | Enables the gRPC server on port 9090 | + +### config.toml + +The settings most likely to change during development: + +| Setting | Default | Description | +|---|---|---| +| `moniker` | `"test"` | Human-readable name for the node | +| `log_level` | `"info"` | Log verbosity (`debug`, `info`, `error`) | +| `consensus.timeout_commit` | `"5s"` | How long to wait after a block is committed before starting the next one | +| `p2p.seeds` | `""` | Seed nodes to connect to on a live network | +| `p2p.persistent_peers` | `""` | Peers to maintain permanent connections to | ## Unit tests -Start here when you want fast feedback on module logic without running a chain. These tests isolate the keeper and gRPC servers from the rest of the app. +Start here when you want fast feedback on module logic without running a chain. These tests isolate the [keeper](https://docs.cosmos.network/sdk/next/learn/concepts/testing#keeper-unit-tests) and gRPC servers from the rest of the app. The unit test logic lives in the counter keeper package on `main`: the shared suite setup is in [x/counter/keeper/keeper_test.go](https://github.com/cosmos/example/blob/main/x/counter/keeper/keeper_test.go), message-path tests are in [x/counter/keeper/msg_server_test.go](https://github.com/cosmos/example/blob/main/x/counter/keeper/msg_server_test.go), and query-path tests are in [x/counter/keeper/query_server_test.go](https://github.com/cosmos/example/blob/main/x/counter/keeper/query_server_test.go). @@ -139,11 +166,9 @@ The test suite is structured around three files: | `keeper/msg_server_test.go` | `MsgAdd`, event emission, `MsgUpdateParams` | | `keeper/query_server_test.go` | `QueryCount`, `QueryParams` | ---- - ## E2E tests -Run E2E tests when you want to verify the full request path against a real node. They give you higher confidence than unit tests, but take longer to complete. +Run [E2E tests](https://docs.cosmos.network/sdk/next/learn/concepts/testing#integration-tests) when you want to verify the full request path against a real node. They give you higher confidence than unit tests, but take longer to complete. The E2E logic lives on `main` in [tests/counter_test.go](https://github.com/cosmos/example/blob/main/tests/counter_test.go), which starts an in-process network, builds signed transactions, and verifies query results. The shared network fixture it uses is defined in [tests/test_helpers.go](https://github.com/cosmos/example/blob/main/tests/test_helpers.go). @@ -155,11 +180,9 @@ go test -v -run TestE2ETestSuite ./tests/... E2E tests take longer than unit tests because they spin up a real node. Run them before merging significant changes. ---- - ## Simulation tests -Simulation tests stress the chain with randomized activity to catch edge cases that targeted tests can miss. In this repo, that simulation flow is built with `simsx`, the Cosmos SDK's higher-level simulation framework for defining random on-chain activity at the module level. +[Simulation tests](https://docs.cosmos.network/sdk/next/learn/concepts/testing#simulation-tests) stress the chain with randomized activity to catch edge cases that targeted tests can miss. In this repo, that simulation flow is built with `simsx`, the Cosmos SDK's higher-level simulation framework for defining random on-chain activity at the module level. The top-level simulation test commands on `main` run through [sim_test.go](https://github.com/cosmos/example/blob/main/sim_test.go). The counter module's `simsx` registration lives in [x/counter/module.go](https://github.com/cosmos/example/blob/main/x/counter/module.go), the random `MsgAdd` generation lives in [x/counter/simulation/msg_factory.go](https://github.com/cosmos/example/blob/main/x/counter/simulation/msg_factory.go), and randomized counter genesis lives in [x/counter/simulation/genesis.go](https://github.com/cosmos/example/blob/main/x/counter/simulation/genesis.go). @@ -182,8 +205,6 @@ make test-sim Simulation requires the `sims` build tag, which the Makefile targets handle automatically. ---- - ## Lint Linting is the quickest way to catch style problems and common code-quality issues before CI or code review does. @@ -200,8 +221,6 @@ This installs and runs `golangci-lint` across the repository. To auto-fix issues make lint-fix ``` ---- - ## Test summary Use this table as a quick reference for choosing the right validation command for the kind of change you made. diff --git a/scripts/docs-sync/__pycache__/transform.cpython-312.pyc b/scripts/docs-sync/__pycache__/transform.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3f507d1493623477783b4c53c09bbbe96a78016a GIT binary patch literal 9247 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 JbSswf{{!6^Xfpr+ literal 0 HcmV?d00001 diff --git a/scripts/docs-sync/test_transform.py b/scripts/docs-sync/test_transform.py new file mode 100644 index 0000000..22db504 --- /dev/null +++ b/scripts/docs-sync/test_transform.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +""" +Tests for the docs-sync transform script. + +Run from repo root: + python3 -m pytest scripts/docs-sync/test_transform.py -v + # or without pytest: + python3 scripts/docs-sync/test_transform.py +""" + +import sys +import os +import textwrap +import unittest + +# Add the scripts/docs-sync directory to path so we can import transform +sys.path.insert(0, os.path.dirname(__file__)) +import transform + + +class TestToMintlify(unittest.TestCase): + """Tests for to_mintlify direction (example .md → docs site .mdx).""" + + def test_h1_becomes_frontmatter_title(self): + md = "# Prerequisites\n\nSome content here.\n" + result = transform.to_mintlify(md) + self.assertIn("title: Prerequisites", result) + self.assertNotIn("# Prerequisites", result) + + def test_frontmatter_format(self): + md = "# Build a Module\n\nContent.\n" + result = transform.to_mintlify(md) + self.assertTrue(result.startswith("---\ntitle: Build a Module\n---\n")) + + def test_body_content_preserved(self): + md = "# Title\n\nThis is the body.\n\n## Section\n\nMore text.\n" + result = transform.to_mintlify(md) + self.assertIn("This is the body.", result) + self.assertIn("## Section", result) + self.assertIn("More text.", result) + + def test_absolute_docs_link_rewritten_to_mintlify_path(self): + md = "# Title\n\nSee [page](https://docs.cosmos.network/sdk/next/learn/concepts/modules).\n" + result = transform.to_mintlify(md) + self.assertIn("(/sdk/next/learn/concepts/modules)", result) + self.assertNotIn("https://docs.cosmos.network", result) + + def test_absolute_link_in_text_rewritten(self): + md = "# Title\n\nRead more at https://docs.cosmos.network/sdk/next/learn/foo.\n" + result = transform.to_mintlify(md) + self.assertIn("/sdk/next/learn/foo", result) + self.assertNotIn("https://docs.cosmos.network/sdk/next/learn/foo", result) + + def test_local_md_link_becomes_absolute_mintlify_path(self): + md = "# Title\n\nNext: [Quickstart](./quickstart.md).\n" + result = transform.to_mintlify(md) + self.assertIn("(/sdk/next/tutorials/example/quickstart)", result) + self.assertNotIn("./quickstart", result) + + def test_external_links_not_rewritten(self): + md = "# Title\n\nSee [Go](https://go.dev) and [Docker](https://docs.docker.com/get-docker).\n" + result = transform.to_mintlify(md) + self.assertIn("https://go.dev", result) + self.assertIn("https://docs.docker.com/get-docker", result) + + def test_strips_existing_frontmatter(self): + md = "---\nnoindex: true\ntitle: Old Title\n---\n\n# New Title\n\nContent.\n" + result = transform.to_mintlify(md) + self.assertIn("title: New Title", result) + self.assertNotIn("title: Old Title", result) + + def test_preserves_extra_frontmatter_from_existing_mdx(self): + md = "# Prerequisites\n\nContent.\n" + existing_mdx = "---\ntitle: Prerequisites\ndescription: Learn how to set up your environment.\n---\n\nOld content.\n" + result = transform.to_mintlify(md, existing_content=existing_mdx) + self.assertIn("title: Prerequisites", result) + self.assertIn("description: Learn how to set up your environment.", result) + + def test_title_always_overwritten_not_preserved(self): + md = "# New Title\n\nContent.\n" + existing_mdx = "---\ntitle: Old Title\ndescription: Some description.\n---\n\nOld content.\n" + result = transform.to_mintlify(md, existing_content=existing_mdx) + self.assertIn("title: New Title", result) + self.assertNotIn("title: Old Title", result) + self.assertIn("description: Some description.", result) + + def test_no_existing_mdx_produces_title_only_frontmatter(self): + md = "# Quickstart\n\nContent.\n" + result = transform.to_mintlify(md) + self.assertEqual(result.count("---"), 2) + self.assertIn("title: Quickstart", result) + self.assertNotIn("description", result) + + def test_no_h1_uses_untitled(self): + md = "No heading here.\n\nJust content.\n" + result = transform.to_mintlify(md) + self.assertIn("title: Untitled", result) + + def test_code_blocks_preserved(self): + md = textwrap.dedent("""\ + # Title + + ```bash + exampled query counter count + ``` + """) + result = transform.to_mintlify(md) + self.assertIn("```bash", result) + self.assertIn("exampled query counter count", result) + + def test_multiple_links_all_rewritten(self): + md = textwrap.dedent("""\ + # Title + + See [prereqs](https://docs.cosmos.network/sdk/next/tutorials/example/prerequisites) + and [quickstart](https://docs.cosmos.network/sdk/next/tutorials/example/quickstart). + """) + result = transform.to_mintlify(md) + self.assertNotIn("https://docs.cosmos.network", result) + self.assertIn("sdk/next/tutorials/example/prerequisites", result) + self.assertIn("sdk/next/tutorials/example/quickstart", result) + + +class TestToExample(unittest.TestCase): + """Tests for to_example direction (docs site .mdx → example .md).""" + + def test_frontmatter_title_becomes_h1(self): + mdx = "---\nnoindex: true\ntitle: Prerequisites\n---\n\nContent here.\n" + result = transform.to_example(mdx) + self.assertIn("# Prerequisites", result) + self.assertNotIn("title:", result) + self.assertNotIn("---", result) + + def test_frontmatter_stripped(self): + mdx = "---\nnoindex: true\ntitle: Quickstart\n---\n\nBody text.\n" + result = transform.to_example(mdx) + self.assertNotIn("noindex", result) + self.assertNotIn("---", result) + + def test_h1_at_top(self): + mdx = "---\ntitle: Run and Test\n---\n\nContent.\n" + result = transform.to_example(mdx) + self.assertTrue(result.startswith("# Run and Test")) + + def test_non_tutorial_mintlify_path_becomes_absolute_url(self): + mdx = "---\ntitle: Title\n---\n\nSee [modules](/sdk/next/learn/concepts/modules).\n" + result = transform.to_example(mdx) + self.assertIn("https://docs.cosmos.network/sdk/next/learn/concepts/modules", result) + self.assertNotIn("(/sdk/", result) + + def test_tutorial_link_not_expanded_to_full_url(self): + mdx = "---\ntitle: Title\n---\n\nSee [quickstart](/sdk/next/tutorials/example/quickstart).\n" + result = transform.to_example(mdx) + self.assertIn("./quickstart.md", result) + self.assertNotIn("https://docs.cosmos.network", result) + + def test_mintlify_absolute_link_becomes_relative_md(self): + mdx = "---\ntitle: Title\n---\n\nNext: [Quickstart](/sdk/next/tutorials/example/quickstart).\n" + result = transform.to_example(mdx) + self.assertIn("./quickstart.md", result) + self.assertNotIn("/sdk/next/tutorials/example/quickstart", result) + + def test_external_links_not_rewritten(self): + mdx = "---\ntitle: Title\n---\n\nSee [Go](https://go.dev).\n" + result = transform.to_example(mdx) + self.assertIn("https://go.dev", result) + + def test_body_content_preserved(self): + mdx = "---\ntitle: Title\n---\n\nThis is the body.\n\n## Section\n\nMore text.\n" + result = transform.to_example(mdx) + self.assertIn("This is the body.", result) + self.assertIn("## Section", result) + + def test_code_blocks_preserved(self): + mdx = textwrap.dedent("""\ + --- + title: Title + --- + + ```bash + make start + ``` + """) + result = transform.to_example(mdx) + self.assertIn("```bash", result) + self.assertIn("make start", result) + + def test_no_frontmatter_returns_content_unchanged(self): + mdx = "Just plain content with no frontmatter.\n" + result = transform.to_example(mdx) + self.assertIn("Just plain content with no frontmatter.", result) + + def test_multiple_relative_links(self): + mdx = textwrap.dedent("""\ + --- + title: Title + --- + + See [prereqs](sdk/next/tutorials/example/prerequisites) + and [quickstart](sdk/next/tutorials/example/quickstart). + """) + result = transform.to_example(mdx) + self.assertIn("https://docs.cosmos.network/sdk/next/tutorials/example/prerequisites", result) + self.assertIn("https://docs.cosmos.network/sdk/next/tutorials/example/quickstart", result) + + +class TestRoundTrip(unittest.TestCase): + """Round-trip tests: md → mdx → md and mdx → md → mdx should be stable.""" + + def test_roundtrip_example_to_mintlify_to_example(self): + original = textwrap.dedent("""\ + # Prerequisites + + Before starting, make sure you have Go installed. + + See [quickstart](./quickstart.md) to continue. + + External: [Go](https://go.dev). + Docs link: [learn](https://docs.cosmos.network/sdk/next/learn/foo). + """) + mdx = transform.to_mintlify(original) + recovered = transform.to_example(mdx) + + # Title round-trips + self.assertIn("# Prerequisites", recovered) + # External link preserved + self.assertIn("https://go.dev", recovered) + # Local link round-trips back to ./file.md + self.assertIn("./quickstart.md", recovered) + # Docs link recovered + self.assertIn("https://docs.cosmos.network/sdk/next/learn/foo", recovered) + + def test_roundtrip_mintlify_to_example_to_mintlify(self): + original = textwrap.dedent("""\ + --- + title: Quickstart + --- + + Run the chain with `make start`. + + See [prerequisites](/sdk/next/tutorials/example/prerequisites) first. + + Non-tutorial docs link: [learn](sdk/next/learn/foo). + """) + md = transform.to_example(original) + recovered = transform.to_mintlify(md) + + # Frontmatter round-trips + self.assertIn("title: Quickstart", recovered) + # Tutorial link round-trips back to absolute Mintlify path + self.assertIn("/sdk/next/tutorials/example/prerequisites", recovered) + # Non-tutorial docs link round-trips + self.assertIn("sdk/next/learn/foo", recovered) + + def test_roundtrip_is_stable_on_second_pass(self): + """Running to_mintlify twice on the same content should produce the same result.""" + md = "# Title\n\nSome content with [a link](https://docs.cosmos.network/sdk/next/foo).\n" + first = transform.to_mintlify(md) + # Simulate editing the mdx and re-converting to md then back + md2 = transform.to_example(first) + second = transform.to_mintlify(md2) + self.assertEqual(first, second) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/scripts/docs-sync/transform.py b/scripts/docs-sync/transform.py new file mode 100644 index 0000000..36e940b --- /dev/null +++ b/scripts/docs-sync/transform.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +""" +docs-sync transform script + +Converts docs between the example repo format and the Mintlify docs site format. + +Directions: + to-mintlify — example repo .md → docs site .mdx + - Strips # H1, injects YAML frontmatter (title, noindex) + - Rewrites https://docs.cosmos.network/sdk/next/... → sdk/next/... + - Rewrites local .md links → .mdx links + + to-example — docs site .mdx → example repo .md + - Strips YAML frontmatter, injects # H1 + - Rewrites sdk/next/... → https://docs.cosmos.network/sdk/next/... + - Rewrites .mdx links → .md links + +Usage: + python3 transform.py --direction to-mintlify --input docs/prerequisites.md --output out.mdx + python3 transform.py --direction to-example --input prerequisites.mdx --output out.md + + # Batch: transform all docs/ files to a target directory + python3 transform.py --direction to-mintlify --input docs/ --output-dir /path/to/docs-site/sdk/next/tutorials/example/ + python3 transform.py --direction to-example --input /path/to/docs-site/sdk/next/tutorials/example/ --output-dir docs/ +""" + +import argparse +import os +import re +import sys + +DOCS_BASE_URL = "https://docs.cosmos.network" +# The base path for example tutorials on the docs site (no leading slash) +TUTORIAL_BASE_PATH = "sdk/next/tutorials/example" + + +def strip_frontmatter(content: str) -> tuple[dict, str]: + """Remove YAML frontmatter from content. Returns (fields dict, remaining content).""" + fields = {} + if not content.startswith("---"): + return fields, content + end = content.find("\n---", 3) + if end == -1: + return fields, content + front = content[3:end].strip() + for line in front.splitlines(): + if ":" in line: + k, _, v = line.partition(":") + fields[k.strip()] = v.strip() + remaining = content[end + 4:].lstrip("\n") + return fields, remaining + + +def extract_h1(content: str) -> tuple[str | None, str]: + """Remove the first # H1 line. Returns (title, remaining content).""" + lines = content.split("\n") + for i, line in enumerate(lines): + if line.startswith("# "): + title = line[2:].strip() + remaining = "\n".join(lines[i + 1:]).lstrip("\n") + return title, remaining + return None, content + + +def to_mintlify(content: str, existing_content: str = "") -> str: + # If the destination file already exists, preserve any extra frontmatter fields + # (e.g. description, sidebarTitle) — title is always overwritten from the H1. + existing_fields: dict = {} + if existing_content: + existing_fields, _ = strip_frontmatter(existing_content) + existing_fields.pop("title", None) # title is always sourced from the H1 + + # Strip any existing frontmatter from source + _, content = strip_frontmatter(content) + # Extract H1 title + title, content = extract_h1(content) + + # Rewrite absolute docs.cosmos.network links to Mintlify paths (leading slash, no domain) + # e.g. https://docs.cosmos.network/sdk/next/learn/foo → /sdk/next/learn/foo + content = re.sub( + r'https://docs\.cosmos\.network/([^\s)"\']+)', + r'/\1', + content, + ) + + # Rewrite local .md links to full Mintlify paths (no extension, leading slash) + # e.g. [text](./prerequisites.md) → [text](/sdk/next/tutorials/example/prerequisites) + content = re.sub( + r'\(\./([^)]+)\.md\)', + lambda m: f'(/{TUTORIAL_BASE_PATH}/{m.group(1)})', + content, + ) + + # Build frontmatter: title first, then any preserved docs-site-only fields + fm_title = title if title else "Untitled" + fm_lines = [f"title: {fm_title}"] + for k, v in existing_fields.items(): + fm_lines.append(f"{k}: {v}") + frontmatter = "---\n" + "\n".join(fm_lines) + "\n---\n\n" + + return frontmatter + content.lstrip("\n") + + +def to_example(content: str) -> str: + # Extract frontmatter + fields, content = strip_frontmatter(content) + title = fields.get("title", "") + + # Rewrite tutorial-local Mintlify links back to relative .md links + # e.g. /sdk/next/tutorials/example/quickstart → ./quickstart.md + content = re.sub( + rf'\(/{re.escape(TUTORIAL_BASE_PATH)}/([^)]+)\)', + r'(./\1.md)', + content, + ) + + # Rewrite other /sdk/... Mintlify paths (non-tutorial) to full URLs + # e.g. (/sdk/next/learn/foo) → (https://docs.cosmos.network/sdk/next/learn/foo) + content = re.sub( + rf'\(/(?!{re.escape(TUTORIAL_BASE_PATH)})([^\s)"\']+)\)', + lambda m: f'({DOCS_BASE_URL}/{m.group(1)})', + content, + ) + + # Rewrite bare sdk/... paths (no leading slash) to full URLs for backwards compat + # e.g. (sdk/next/learn/foo) → (https://docs.cosmos.network/sdk/next/learn/foo) + content = re.sub( + r'\((?!http)(?!/)(sdk/[^\s)"\']+)\)', + lambda m: f'({DOCS_BASE_URL}/{m.group(1)})', + content, + ) + + # Inject H1 + header = f"# {title}\n\n" if title else "" + return header + content.lstrip("\n") + + +def transform_file(direction: str, input_path: str, output_path: str) -> None: + with open(input_path, "r", encoding="utf-8") as f: + content = f.read() + + if direction == "to-mintlify": + existing_content = "" + if os.path.exists(output_path): + with open(output_path, "r", encoding="utf-8") as f: + existing_content = f.read() + result = to_mintlify(content, existing_content) + elif direction == "to-example": + result = to_example(content) + else: + raise ValueError(f"Unknown direction: {direction}") + + os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True) + with open(output_path, "w", encoding="utf-8") as f: + f.write(result) + + print(f" {input_path} → {output_path}") + + +def main(): + parser = argparse.ArgumentParser(description="Transform docs between example repo and Mintlify format") + parser.add_argument("--direction", required=True, choices=["to-mintlify", "to-example"]) + parser.add_argument("--input", required=True, help="Input file or directory") + parser.add_argument("--output", help="Output file (single-file mode)") + parser.add_argument("--output-dir", help="Output directory (batch mode)") + args = parser.parse_args() + + if os.path.isdir(args.input): + # Batch mode + if not args.output_dir: + print("ERROR: --output-dir required when --input is a directory", file=sys.stderr) + sys.exit(1) + ext_in = ".md" if args.direction == "to-mintlify" else ".mdx" + ext_out = ".mdx" if args.direction == "to-mintlify" else ".md" + for fname in sorted(os.listdir(args.input)): + if fname.endswith(ext_in): + in_path = os.path.join(args.input, fname) + out_name = fname[: -len(ext_in)] + ext_out + out_path = os.path.join(args.output_dir, out_name) + transform_file(args.direction, in_path, out_path) + else: + # Single file mode + if not args.output: + print("ERROR: --output required for single-file mode", file=sys.stderr) + sys.exit(1) + transform_file(args.direction, args.input, args.output) + + +if __name__ == "__main__": + main()