Skip to content

Latest commit

 

History

History
285 lines (220 loc) · 13.3 KB

CONTRIBUTING.md

File metadata and controls

285 lines (220 loc) · 13.3 KB

Contributing to Milepost

Welcome, and thank you for your interest in contributing to Milepost! This guide covers everything you need to go from a clean checkout to an open pull request: local setup, running checks, documentation standards, and the issue workflow.

Table of Contents

  1. Prerequisites
  2. Local Setup
  3. Frontend
  4. Running Contract Checks
  5. Documentation Standards
  6. Issue and PR Workflow
  7. Error Code Stability Policy
  8. Release Process

1. Prerequisites

Tool Minimum version Notes
Node.js 18+ 22 is recommended for CI alignment
npm 8+ Do not use pnpm or yarn — it creates lockfile conflicts
Rust + Cargo stable (1.74+) Install via rustup.rs
wasm32v1-none — rustup target add wasm32v1-none
Stellar CLI 21+ Installation guide
Freighter wallet latest freighter.app — browser extension for UI testing

2. Local Setup

Clone

git clone https://github.com/milepost-labs/milepost.git
cd milepost

The web app is in frontend/, with its own setup guide in frontend/CONTRIBUTING.md.

Pre-commit hooks (opt-in) To automatically check formatting (rustfmt) on changed files before each commit:

./scripts/install-hooks.sh

The hook runs only on staged files for speed. If you need to bypass it in an emergency, use git commit --no-verify.

Development Container If you use VS Code or GitHub Codespaces, you can open the repository in a pre-configured Dev Container that includes all pinned toolchains (Rust stable with wasm32v1-none, Stellar CLI 27.1.0, and Node.js 24):

  1. Open the project in VS Code.
  2. When prompted, select Reopen in Container (or run Dev Containers: Reopen in Container from the Command Palette). Tool versions in .devcontainer/Dockerfile match .github/workflows/ci.yml. When updating toolchain versions, update both.

Task Runner (just or make) Common commands and workflows are available via just (or standard make):

  • just build / make build — Build contract WASM artifacts
  • just test / make test — Run workspace tests (encodes WASM build order dependency)
  • just lint / make lint — Run cargo fmt check and cargo clippy (encodes WASM build order dependency)
  • just frontend-local-bindings / make frontend-local-bindings — Build and test the app in frontend/ against this checkout's bindings after changing a contract
  • just deploy / make deploy — Deploy contracts using ./scripts/deploy.sh
  • just seed / make seed — Seed protocol scenario data using ./scripts/seed.sh
  • just / make help — List all available tasks

3. Frontend

The web app lives in frontend/, and the indexer that builds the lists it reads in milepost-indexer. Both have their own contributing guides and checks, and both install published @milepost/* versions, so neither sees a contract change until it is released.

If you change a contract's interface, or any event it emits, check them against your change before opening the PR, with milepost-indexer cloned next to this repository:

./scripts/frontend-with-local-bindings.sh frontend ../milepost-indexer
npm run build --prefix frontend && npm test --prefix frontend
npm run typecheck --prefix ../milepost-indexer && npm test --prefix ../milepost-indexer

An event rename is the case to watch: the app may not touch the event at all, while the indexer stops decoding it and quietly publishes shorter lists. Its tests decode events through the bindings' own specs, so they fail on it.

CI does the same on every PR, in the bindings job.


4. Running Contract Checks

Milepost's Soroban contracts are structured as a workspace in the root directory (e.g., contracts/attest, contracts/program, contracts/registry, etc.).

Run these checks from the root of the repository:

# Format check
cargo fmt --all --check

# Lint
cargo clippy --workspace -- -D warnings

# Unit tests
cargo test --workspace

To build the WASM artifacts for deployment:

stellar contract build

The compiled outputs will land in target/wasm32-unknown-unknown/release/.

The wasm build must come first. registry's tests contractimport! the built programme, so running clippy or tests before building produces failures that look like unrelated logic errors — and a stale artifact silently tests the wrong contract. See the testing guide for the ordering rule, bindings regeneration, and the generated-file and fixture traps that have cost review rounds.


5. Documentation Standards

  • Code comments: Only add a comment when the why is non-obvious — a hidden constraint, a subtle invariant, or a workaround for a specific bug. Do not comment on what the code does; well-named identifiers already do that.
  • Contract interface changes: Any change that touches the contract state or upgrade flow must be explicitly documented. Breaking changes require an explicit version bump and testnet verification before the PR can be merged.

6. Issue and PR Workflow

Picking up an issue

  1. Comment on the issue to let others know you are working on it.
  2. Fork the repository and clone your fork.
  3. Create a branch from main using the convention below.

Branch naming convention: <type>/<short-description> Examples:

  • feat/verifier-dashboard
  • fix/tranche-unlock-logic
  • docs/contributing-guide

Commit messages Write imperative-mood subject lines under 72 characters. Put context in the body when needed.

feat: add full-screen layout to the landing page Replaces the centered hero section with a split-screen design.

Pull request checklist Before marking a PR ready for review:

  • Contract checks pass if Rust files were touched (cargo fmt, cargo clippy, cargo test)
  • If a contract interface changed, the app in frontend/ still builds and tests against it (see Frontend)
  • PR description references the issue number(s) with Closes #<number>

7. Error Code Stability Policy

If you are modifying the Soroban smart contracts, error codes are part of the contract's public API and are matched on by SDK consumers and off-chain monitoring tools.

Rules

  1. Never reassign a published discriminant. Once an error code has been included in a release, that numeric value is permanently reserved — even if the corresponding variant is removed.
  2. Mark removed variants as reserved. Replace a deleted variant with an underscored placeholder and annotate it with a // (reserved — removed variant) comment. This makes the gap self-documenting and prevents accidental reuse.
  3. Always assign new variants the next available integer. Do not insert variants mid-sequence; append them at the end of the enum.

Why this matters: Stellar contract error codes propagate as u32 values in the transaction result. If we reused a previously published discriminant, an indexer or bot that matches on that numeric value could silently misinterpret a new error as an old, unrelated one.


8. Release Process

A release is a git tag. Pushing a tag matching vX.Y.Z (or a pre-release like v0.1.0-rc.1) triggers .github/workflows/release.yml, which:

  1. Builds every contract to wasm32v1-none release wasm — the same build CI already verified on the PR that merged.
  2. Hashes each .wasm with SHA-256 (checksums.txt). A programme is instantiated from a wasm hash (see the root README), so the hash is what downstream teams pin to — not a crate version.
  3. Generates a changelog from the commits since the previous tag (git-cliff, configured in cliff.toml) and publishes a GitHub Release with the wasm files, checksums.txt, and the changelog as the release body. v*-* tags (e.g. -rc.1, -beta.2) are published as pre-releases.
  4. Builds the five TypeScript binding packages at the tag's version, checks that each tarball actually contains dist/, and publishes them to npm as @milepost/* through trusted publishing, which also attaches provenance. This runs before the GitHub Release is created, so a broken release — a package that would ship without dist/, or a trusted publisher that is missing or misconfigured — stops cleanly instead of leaving a GitHub Release with no packages. A pre-release tag publishes under the next dist-tag, so npm install @milepost/program never resolves to one, and packages already published at that version are skipped, so a failed release can be re-run.

Nothing in the workflow edits Cargo.toml or commits back to the repo — tagging is a manual, deliberate act, not an automated bump.

Tagging a release

Publishing needs no npm token, and token publishing is disallowed on all five packages. Each of them on npmjs.com has a trusted publisher for GitHub Actions — organisation milepost-labs, repository milepost, workflow release.yml, no environment, direct publishing allowed — and the release job authenticates with its own short-lived OIDC credential. Those values are case-sensitive and npm does not check them when they are saved. After adding one, reopen the package's settings and confirm the connection is listed: the form can be left without saving, and nothing warns you.

A missing or mismatched trusted publisher does not fail as an authentication error. npm falls back to an unauthenticated upload, and the publish step fails with E404 Not Found on the PUT for that package ("could not be found or you do not have permission to access it"). Packages later in the loop are never attempted, nothing is published and the GitHub Release is not created, so fixing the configuration and re-running the failed job is enough — no new tag.

A package has to exist on npm before a trusted publisher can be added to it, so a new binding package must be published once by hand before a release can take it over.

git tag v0.2.0
git push origin v0.2.0

Pick the version by ordinary semver judgement (see below); nothing derives it for you from commit history.

Marking interface-breaking changes

Commit subjects already follow <type>(<scope>): <description>. To flag a commit as breaking a contract's public interface — its client-facing functions, its error codes (section 7), or its storage/upgrade layout — do either of:

  • Append ! after the type/scope: feat(program)!: change award() signature
  • Add a BREAKING CHANGE: <explanation> footer to the commit body.

Either form makes git-cliff place that commit in its own "⚠ Breaking changes" section at the top of the release notes, separate from ordinary Features/ Fixes entries, so a downstream integrator scanning a release can see at a glance whether upgrading is safe. A deployed Soroban contract can't be patched in place — a breaking release means existing callers need to re-integrate against a new deployment, so treat this marker as required, not decorative.

Versioning crates vs. tagging releases

The workspace's crates all share version = "0.1.0" in the root Cargo.toml and none are bumped automatically by this workflow — the git tag, not the crate version field, is the source of truth for what a release is. Bump the workspace version by hand when a tag represents a meaningful jump (e.g. the first 1.0.0), but a mismatch between the tag and the crate field is expected and not a bug: contracts here are consumed as compiled wasm pinned by hash and as the generated TypeScript bindings in packages/ (see the README), not as a Rust library dependency.

Why these crates are not published to crates.io

This release process produces GitHub Release artifacts only; nothing here publishes to crates.io. That's a deliberate choice, not a gap:

  • milepost-policy-spend depends on smart-wallet-interface pinned to a git revision (see its Cargo.toml) because that crate isn't published either — crates.io rejects git dependencies, so this crate can't be published as-is.
  • The actual integration surface for another Stellar team is a deployed wasm hash plus the generated TS bindings, not cargo add milepost-attest. None of the five contracts expose a Rust API meant to be depended on externally.
  • All crates are pre-1.0 and workspace-versioned together; publishing now would mean maintaining crates.io semver guarantees before the interfaces (and this release process itself) have stabilized.

If a real Rust-dependency use case shows up later, revisit this — starting with milepost-types, the one crate with no unpublished dependency in its way — but raise it as its own issue rather than folding it into this workflow.

Verifying the workflow

Push a pre-release tag (vX.Y.Z-rc.N) on your own fork before trusting a change to this workflow, and check the Actions run produces a pre-release with wasm files, checksums.txt, and a non-empty changelog attached.