diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b13f335b..b9c8874b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,13 +1,35 @@ -name: Release & Deploy +name: Release -# Triggers: -# - Tag push v*.*.* → full release: build binary + WASM, publish image, GitHub Release -# The on-chain runtime upgrade is runtime-upgrade.yml. +# Version = code, network = build flavour. A release is `vX.Y.Z` — no `-rc`: the +# binary reports `-` to telemetry and `--version`, so the tag IS +# what operators read there. The same tag is built once per network, since the +# hyperbridge coprocessor is a compile-time feature: +# +# - Tag push vX.Y.Z → testnet flavour: image `X.Y.Z-testnet` + `testnet-latest` +# (what testnet Watchtower follows), GitHub pre-release. +# - workflow_dispatch environment=mainnet, version=X.Y.Z → mainnet flavour of that +# same tag: image `X.Y.Z` + `latest`, the GitHub Release gains the mainnet assets +# and stops being a pre-release. environment=testnet rebuilds the testnet flavour. +# +# Nothing here touches the chain: the runtime upgrade is runtime-upgrade.yml. on: push: tags: - "v[0-9]+.[0-9]+.[0-9]+" - - "v[0-9]+.[0-9]+.[0-9]+-*" + workflow_dispatch: + inputs: + environment: + description: "Network flavour to build and publish" + required: true + default: "mainnet" + type: choice + options: + - testnet + - mainnet + version: + description: "Release version, e.g. 0.2.0 (git tag v0.2.0)" + required: true + type: string concurrency: group: release-${{ github.ref }} @@ -23,9 +45,16 @@ jobs: metadata: name: Release Metadata runs-on: ubuntu-latest + permissions: + contents: read + packages: read # the mainnet promotion looks up the testnet image outputs: version: ${{ steps.meta.outputs.version }} environment: ${{ steps.meta.outputs.environment }} + # `X.Y.Z-testnet` or `X.Y.Z` (mainnet): the two flavours are different bytes and + # must never share a registry tag. + image_tag: ${{ steps.meta.outputs.image_tag }} + # Testnet builds are pre-releases; a release becomes final when mainnet ships it. is_prerelease: ${{ steps.meta.outputs.is_prerelease }} steps: - name: Checkout @@ -33,30 +62,75 @@ jobs: with: fetch-depth: 0 # required by git verify-tag + - name: Compute metadata + id: meta + env: + INPUT_VERSION: ${{ inputs.version }} + INPUT_ENVIRONMENT: ${{ inputs.environment }} + run: | + if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then + VERSION="$INPUT_VERSION" + ENV="$INPUT_ENVIRONMENT" + else + VERSION="${GITHUB_REF#refs/tags/v}" + ENV="testnet" # every tag ships to testnet first; mainnet is a promotion + fi + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::version must be X.Y.Z, got '$VERSION'"; exit 1 + fi + IMAGE_TAG="$VERSION"; [[ "$ENV" == "testnet" ]] && IMAGE_TAG="$VERSION-testnet" + IS_PRE=$([[ "$ENV" == "testnet" ]] && echo true || echo false) + { + echo "version=$VERSION" + echo "environment=$ENV" + echo "image_tag=$IMAGE_TAG" + echo "is_prerelease=$IS_PRE" + } >> "$GITHUB_OUTPUT" + + # Every run checks the tag it builds, the tag push and a promotion alike: a tag + # whose testnet run failed these checks still exists and could otherwise be + # promoted. - name: Verify GPG tag signature - if: github.event_name == 'push' env: RELEASE_GPG_PUBLIC_KEY: ${{ secrets.RELEASE_GPG_PUBLIC_KEY }} + TAG: v${{ steps.meta.outputs.version }} run: | echo "$RELEASE_GPG_PUBLIC_KEY" | gpg --import - git fetch origin "+refs/tags/${{ github.ref_name }}:refs/tags/${{ github.ref_name }}" --force - git verify-tag "refs/tags/${{ github.ref_name }}" + git fetch origin "+refs/tags/$TAG:refs/tags/$TAG" --force + git verify-tag "refs/tags/$TAG" - - name: Compute metadata - id: meta + # The binary reports `-` (SUBSTRATE_CLI_IMPL_VERSION), + # which is what telemetry and `--version` show. A tag that does not match that + # crate version ships a binary nobody can trace back to a release. + - name: Tag must match the orbinum-node crate version + env: + VERSION: ${{ steps.meta.outputs.version }} run: | - VERSION="${GITHUB_REF#refs/tags/}" - # rc / alpha / beta tags go to testnet - if [[ "$VERSION" =~ -(rc|alpha|beta|pre) ]]; then - IS_PRE="true" - ENV="testnet" - else - IS_PRE="false" - ENV="testnet" # mainnet requires manual approval — see deployment protection + # Read at the tag, not the checkout: a promotion runs on main. + CRATE=$(git show "v$VERSION:template/node/Cargo.toml" \ + | grep -m1 -E '^version *= *"' | sed -E 's/.*"(.*)".*/\1/') + if [[ "$VERSION" != "$CRATE" ]]; then + echo "::error::tag v$VERSION does not match template/node/Cargo.toml version $CRATE — bump the crate (and Cargo.lock) before tagging" + exit 1 fi - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "environment=$ENV" >> "$GITHUB_OUTPUT" - echo "is_prerelease=$IS_PRE" >> "$GITHUB_OUTPUT" + + - name: Log in to GHCR + if: steps.meta.outputs.environment == 'mainnet' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Mainnet only gets what testnet ran first: a version whose testnet image was never + # published never had its dress rehearsal. + - name: Mainnet promotion requires the testnet release + if: steps.meta.outputs.environment == 'mainnet' + env: + TESTNET_IMAGE: ghcr.io/${{ github.repository_owner }}/node:${{ steps.meta.outputs.version }}-testnet + run: | + docker buildx imagetools inspect "$TESTNET_IMAGE" >/dev/null \ + || { echo "::error::$TESTNET_IMAGE not found — ship v${{ steps.meta.outputs.version }} to testnet before promoting it"; exit 1; } # ── 2. Build binary + runtime WASM (single compile, inside the Dockerfile) ── # The Dockerfile is the only build definition. Compiling here and exporting the @@ -66,7 +140,6 @@ jobs: name: Build Release Artifacts runs-on: ubuntu-latest needs: metadata - if: github.event_name == 'push' steps: # A substrate release build needs ~20 GB; a stock runner has ~14 GB free. - name: Free disk space @@ -82,6 +155,9 @@ jobs: - name: Checkout uses: actions/checkout@v5 + with: + # A mainnet promotion builds the released tag, not whatever main is now. + ref: v${{ needs.metadata.outputs.version }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -131,7 +207,7 @@ jobs: - name: Upload binary artifact uses: actions/upload-artifact@v5 with: - name: orbinum-node-${{ needs.metadata.outputs.version }} + name: orbinum-node-${{ needs.metadata.outputs.image_tag }} path: | ${{ env.BINARY }} ${{ env.BINARY }}.sha256 @@ -140,32 +216,39 @@ jobs: - name: Upload WASM artifact uses: actions/upload-artifact@v5 with: - name: orbinum-runtime-wasm-${{ needs.metadata.outputs.version }} + name: orbinum-runtime-wasm-${{ needs.metadata.outputs.image_tag }} path: | ${{ env.WASM }} ${{ env.WASM }}.sha256 retention-days: 30 # ── 3. Publish Docker image to GHCR ────────────────────────────────────── - # Node binaries are updated via Watchtower following `testnet-latest`. + # Node binaries are updated via Watchtower following `testnet-latest` / `latest`. # No compilation: `build-contexts` replaces the Dockerfile's `artifacts` stage with # the files already verified in `build`, so the image carries exactly those bytes. docker-publish: name: Publish Docker Image runs-on: ubuntu-latest needs: [metadata, build] - if: github.event_name == 'push' + # Pushing `latest` IS mainnet's binary rollout — Watchtower takes it from there — so it + # waits for the mainnet reviewer. Testnet stays ungated: its tag push is already + # GPG-checked, and the testnet environment only admits `main`, which a tag run is not. + environment: ${{ needs.metadata.outputs.environment == 'mainnet' && 'mainnet' || '' }} permissions: contents: read packages: write steps: - name: Checkout uses: actions/checkout@v5 + with: + # The final stage (base image, user, /data, --chmod) comes from this Dockerfile: + # a promotion runs on main, and must wrap the tag's binary in the tag's image. + ref: v${{ needs.metadata.outputs.version }} - name: Download build artifacts uses: actions/download-artifact@v5 with: - pattern: orbinum-*-${{ needs.metadata.outputs.version }} + pattern: orbinum-*-${{ needs.metadata.outputs.image_tag }} path: dist merge-multiple: true @@ -187,11 +270,12 @@ jobs: uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository_owner }}/node + # Raw, not `type=semver`: a mainnet promotion runs from workflow_dispatch, where + # github.ref is a branch and semver derivation would yield nothing. tags: | - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,value=latest,enable=${{ needs.metadata.outputs.is_prerelease == 'false' }} - type=raw,value=testnet-latest,enable=${{ needs.metadata.outputs.is_prerelease == 'true' }} + type=raw,value=${{ needs.metadata.outputs.image_tag }} + type=raw,value=latest,enable=${{ needs.metadata.outputs.environment == 'mainnet' }} + type=raw,value=testnet-latest,enable=${{ needs.metadata.outputs.environment == 'testnet' }} - name: Build and push Docker image id: build @@ -245,35 +329,43 @@ jobs: github-release: name: GitHub Release runs-on: ubuntu-latest - needs: [metadata, build] - # Real tags only (not workflow_dispatch) - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + # After the image: a promotion the mainnet reviewer rejects must not leave a final + # release behind for an image that was never pushed. + needs: [metadata, build, docker-publish] + # Created by the tag push (testnet assets, pre-release); the mainnet promotion + # updates the same release with its own assets and drops the pre-release flag. permissions: contents: write steps: - name: Checkout uses: actions/checkout@v5 + with: + ref: v${{ needs.metadata.outputs.version }} + fetch-depth: 0 # release notes diff against the previous tag - name: Download release artifacts uses: actions/download-artifact@v5 with: - pattern: orbinum-*-${{ needs.metadata.outputs.version }} + pattern: orbinum-*-${{ needs.metadata.outputs.image_tag }} path: ./release-artifacts/ - name: Package artifacts run: | - VERSION="${{ needs.metadata.outputs.version }}" + # Asset names carry the flavour (`0.2.0-testnet` / `0.2.0`) so both builds of + # one release sit side by side without overwriting each other. + TAG="${{ needs.metadata.outputs.image_tag }}" find release-artifacts/ -name "orbinum-node" -not -name "*.sha256" \ - | xargs -I{} cp {} orbinum-node-linux-x86_64-${VERSION} + | xargs -I{} cp {} orbinum-node-linux-x86_64-${TAG} find release-artifacts/ -name "*.wasm" \ - | xargs -I{} cp {} orbinum-runtime-${VERSION}.wasm - sha256sum orbinum-node-linux-x86_64-${VERSION} >> checksums.txt - sha256sum orbinum-runtime-${VERSION}.wasm >> checksums.txt + | xargs -I{} cp {} orbinum-runtime-${TAG}.wasm + sha256sum orbinum-node-linux-x86_64-${TAG} >> checksums-${TAG}.txt + sha256sum orbinum-runtime-${TAG}.wasm >> checksums-${TAG}.txt - name: Generate release notes id: notes run: | VERSION="${{ needs.metadata.outputs.version }}" + TAG="${{ needs.metadata.outputs.image_tag }}" PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") echo "## Orbinum Node ${VERSION}" > release_notes.md echo "" >> release_notes.md @@ -282,18 +374,20 @@ jobs: git log --oneline "${PREV_TAG}..HEAD" --no-merges >> release_notes.md fi echo "" >> release_notes.md - echo "### Checksums" >> release_notes.md + echo "### Checksums (${TAG})" >> release_notes.md echo "\`\`\`" >> release_notes.md - cat checksums.txt >> release_notes.md + cat "checksums-${TAG}.txt" >> release_notes.md echo "\`\`\`" >> release_notes.md - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: + tag_name: v${{ needs.metadata.outputs.version }} name: "Orbinum ${{ needs.metadata.outputs.version }}" + # The mainnet run replaces the testnet body; the testnet assets stay attached. body_path: release_notes.md prerelease: ${{ needs.metadata.outputs.is_prerelease == 'true' }} files: | - orbinum-node-linux-x86_64-${{ needs.metadata.outputs.version }} - orbinum-runtime-${{ needs.metadata.outputs.version }}.wasm - checksums.txt + orbinum-node-linux-x86_64-${{ needs.metadata.outputs.image_tag }} + orbinum-runtime-${{ needs.metadata.outputs.image_tag }}.wasm + checksums-${{ needs.metadata.outputs.image_tag }}.txt diff --git a/docs/DEPLOYMENT_FLOW.md b/docs/DEPLOYMENT_FLOW.md index bbd14b86..b5bb4890 100644 --- a/docs/DEPLOYMENT_FLOW.md +++ b/docs/DEPLOYMENT_FLOW.md @@ -75,40 +75,63 @@ ordering below). ## How the pipeline maps to the axes -`release.yml` has two triggers. Know what each does. +Two workflows. `release.yml` builds and publishes; `runtime-upgrade.yml` touches the +chain. Know what each does. -### Tag push (`v*.*.*` / `v*.*.*-rc.N`) — **builds artifacts, deploys nothing** +### `release.yml` — tag push (`vX.Y.Z`): **builds the testnet flavour, deploys nothing** ``` -build → docker-publish → github-release +metadata → build → docker-publish → github-release ``` -- Builds the binary and the runtime WASM. -- Publishes the Docker image. A prerelease tag (`-rc.N`) tags it `testnet-latest`; - a stable tag (`vX.Y.Z`) tags it `latest`. -- Creates the GitHub Release with the WASM + binary attached. -- **Does NOT touch the chain.** No setCode, no node restart. Pushing a tag is safe - — it only produces artifacts. - -### Manual dispatch (Actions → "Release & Deploy" → Run workflow) — **deploys** - -The input gates the deploy job: - -| Input | Job | Effect | -|-------|-----|--------| -| `deploy_runtime: true` | `deploy-runtime` | `sudo.setCode(wasm)` — **runtime upgrade** on-chain | -| `image_tag` | — | Which published image to take the WASM from (e.g. `0.1.0-rc.24`) | -| `environment` | — | Selects the GitHub Environment, and so the RPC + sudo seed. `mainnet` requires a reviewer | - -A dispatch **does not compile**: `build` only runs on a tag push. The runtime WASM -comes out of the already-published image, and four preflight guards (registry -resolution, build provenance, `spec_version` strictly rising, and the RPC node -already running the matching binary) run before any chain state is touched. +- Refuses the tag unless it is GPG-signed and equals `version` in + `template/node/Cargo.toml`. The binary reports `-` to telemetry + and `--version`, so this is what makes "which release runs on that node" readable. +- Builds the binary and the runtime WASM with the `hyperbridge-testnet` feature. +- Publishes the Docker image as `X.Y.Z-testnet` and `testnet-latest`. +- Creates the GitHub Release (pre-release) with the WASM + binary attached. +- **Does NOT touch the chain.** No setCode. Pushing a tag is safe — it only produces + artifacts (and Watchtower picks up the new image). + +There is no `-rc` tag. Every release is a plain version; testnet gets all of them +and mainnet gets the ones promoted to it (below). Version = code, network = build +flavour. + +### `release.yml` — manual dispatch: **mainnet promotion** + +| Input | Effect | +|-------|--------| +| `environment` | Build flavour. `mainnet` = promotion; `testnet` = rebuild the testnet flavour of an existing tag | +| `version` | The release, e.g. `0.2.0` (git tag `v0.2.0`). Must be `X.Y.Z` | + +A mainnet dispatch builds `v` without the testnet feature, publishes `X.Y.Z` + +`latest`, attaches the mainnet assets to the existing GitHub Release and clears its +pre-release flag. It fails unless the tag is GPG-signed, matches the crate version, +and `X.Y.Z-testnet` was published. `docker-publish` waits for the `mainnet` +environment reviewer, since pushing `latest` is the fleet rollout. The build, the +image's final stage and the release notes all come from the tag, not from `main`. + +### `runtime-upgrade.yml` — manual dispatch: **testnet setCode** + +Actions → "Runtime Upgrade (testnet)" → Run workflow, input `image_tag`: a release +image (`0.1.0-rc.28`, `0.3.0-testnet`) or `testnet-latest`. + +- `sudo.setCode` with the WASM extracted from that published image. **Does not + compile.** +- **Testnet only.** The job is pinned to the `testnet` environment (reviewer + required); there is no mainnet option, and a mainnet-flavour image is refused + (`verify-coprocessor.sh testnet` on the image's binary). Mainnet runtime upgrades + go through a multisig, never a sudo key held by CI. +- Four preflight guards run before any chain state is touched: the image resolves, + it was built from the commit its git tag names (image tag minus `-testnet`, with a + `v` prefix), `spec_version` strictly rises, and the RPC node already runs that + release's binary. A floating `testnet-latest` names no git tag, so it skips the + provenance and binary checks — prefer the release tag. +- One run at a time (`concurrency: runtime-upgrade-testnet`). The **binary update is not a workflow job** — nodes run Watchtower, which pulls the -new `testnet-latest` image and restarts them automatically once the tag's -`docker-publish` job publishes it. So the manual dispatch only ever does the -runtime upgrade; the binary follows on its own. +new `testnet-latest` (or `latest` on mainnet) image and restarts them automatically +once `docker-publish` publishes it. > Ordering note: because Watchtower is autonomous, the binary may swap *before* the > setCode. Harmless — a new binary is backward-compatible with the old runtime, and @@ -126,15 +149,16 @@ runtime upgrade; the binary follows on its own. > **The binary always updates via Watchtower.** In every runbook below, pushing the > tag publishes `testnet-latest` and Watchtower restarts the nodes onto it — no -> manual step. The manual dispatch is only ever for the runtime setCode. +> manual step. `runtime-upgrade.yml` is only ever for the runtime setCode. ### A. Image-only update (no runtime change) `spec_version` unchanged. Example: an RPC fix, a base-image patch. 1. Merge to `main`, CI green. -2. Tag `vX.Y.Z-rc.N` → builds + publishes `testnet-latest`. Watchtower restarts the - nodes onto the new image. **No workflow dispatch needed.** +2. Bump `version` in `template/node/Cargo.toml` (+ `Cargo.lock`), merge, tag + `vX.Y.Z` → builds + publishes `testnet-latest`. Watchtower restarts the nodes + onto the new image. **No workflow dispatch needed.** 3. Verify the client version bumped (allow a few minutes for Watchtower): ```bash bash scripts/check-deployment.sh --expect-impl \ @@ -154,9 +178,10 @@ ships with the tag is fine). ```bash git show main:template/runtime/src/lib.rs | grep spec_version ``` -4. Tag `vX.Y.Z-rc.N` → builds the WASM (and image; Watchtower will pull it). +4. Tag `vX.Y.Z` (crate version bumped to match) → builds the WASM (and image; + Watchtower will pull it). 5. **Wait for the release run to finish `success`** (WASM built, image published). -6. Actions → Run workflow: `deploy_runtime: true`. +6. Actions → "Runtime Upgrade (testnet)" → Run workflow: `image_tag: `. 7. Verify: ```bash bash scripts/check-deployment.sh --expect-spec \ @@ -174,9 +199,9 @@ ships with the tag is fine). https://rpc-1.testnet.orbinum.io https://rpc-2.testnet.orbinum.io # note the current spec_version and client — this is your rollback reference ``` -5. Tag `vX.Y.Z-rc.N` → builds WASM + image. Watchtower restarts nodes onto the new - binary. **Wait for the release run to finish `success`.** -6. Actions → Run workflow: `deploy_runtime: true`. (Binary already updated via +5. Tag `vX.Y.Z` (crate version bumped to match) → builds WASM + image. Watchtower + restarts nodes onto the new binary. **Wait for the release run to finish `success`.** +6. Actions → "Runtime Upgrade (testnet)" → Run workflow: `image_tag: `. (Binary already updated via Watchtower — only the runtime setCode is left.) 7. Verify both axes moved: ```bash @@ -185,6 +210,30 @@ ships with the tag is fine). # spec_version == N and client shows the new commit hash ``` +### D. spec 16 — memo-bound circuits (v1 → v2) + +No migration rotates the keys: Root does it right after `setCode`, inside a +maintenance window with every service stopped, since a live v1 lets a pending +spend be resubmitted with swapped memos or an aliased recipient. Every client +able to prove v2 must be published **before** the window. Order: + +| # | Step | Why first | +|---|---|---| +| 1 | `@orbinum/circuits` 0.15.0, with v1 **and** v2 in the manifest (`ROTATE_CIRCUIT=transfer,unshield`) | clients fetch v2 artifacts from it | +| 2 | `@orbinum/proof-generator` 8.0.0 | 8 public signals for v2; proves the version the provider serves | +| 3 | `@orbinum/protocol` 0.6.0, then `@orbinum/wallet-sdk` 0.5.0 (`CIRCUITS_PACKAGE_VERSION` 0.15.0, `proof-generator ^8.0.0`) | proves v2; keeps proving v1 until the chain moves | +| 4 | app, indexer, explorer on those versions | the old app's fee claim is gone in spec 16 | +| 5 | relay operators: `register_relayer` | the new relay RPC refuses unregistered addresses | +| 6 | node image (runbook C, step 5) | binary before runtime | +| 7 | stop every service (app, relays, indexer) | nothing may submit a v1 spend during the rotation | +| 8 | `setCode` (runbook C, step 6) | v1 is still the active version | +| 9 | sudo `zkVerifier.batchRegisterVerificationKeys` — both v2 keys, `version: 2`, `set_active: true` | v2 becomes the active version | +| 10 | sudo `zkVerifier.retireVersion(1, 1)` and `retireVersion(2, 1)` | closes the v1 window | +| 11 | resume the services | | + +Before step 11, check `zkVerifier_getCircuitVersionInfo(1|2)`: `active_version = 2`, +`supported_versions = [2]`, and the v2 `vk_hash` equal to the manifest's. + --- ## Creating and pushing tags @@ -195,23 +244,41 @@ Cut a tag when you have a **merged, CI-green `main`** that you want to release never before the merge lands. A tag is the release trigger; tagging an unmerged branch ships a release off a commit that is not on `main`. -- **Image-only or runtime change → testnet:** `vX.Y.Z-rc.N` (prerelease). -- **Promoting a vetted RC → stable/mainnet:** `vX.Y.Z` (no suffix). +- **Any release → testnet:** `vX.Y.Z`. Patch bump for client-only changes, minor + bump when the runtime changes (`spec_version` moved). +- **Promoting a vetted release → mainnet:** no new tag. Dispatch the workflow on the + same version with `environment: mainnet`. This only ships + the binary; the mainnet runtime upgrade goes through a multisig, never this workflow. - Don't tag for every commit. Tag when you intend to deploy. Between deploys, work merges to `main` without tags. ### Version format -The trigger matches `v[0-9]+.[0-9]+.[0-9]+` and `v[0-9]+.[0-9]+.[0-9]+-*`. +The trigger matches `v[0-9]+.[0-9]+.[0-9]+` only. A suffixed tag (`-rc.N`) does not +release anything. The tag must equal `version` in `template/node/Cargo.toml` or the +`metadata` job fails. + +| Version | Runtime changed? | Example | +|---------|:----------------:|---------| +| Patch `X.Y.Z+1` | no | RPC fix, base-image patch, telemetry | +| Minor `X.Y+1.0` | yes (`spec_version` bumped) | Pallet logic, weights, new host function | + +| Run | Flavour | Docker tags | GitHub Release | +|-----|---------|-------------|----------------| +| Tag push `v0.2.0` | testnet | `0.2.0-testnet`, `testnet-latest` | `Orbinum 0.2.0`, pre-release, testnet assets | +| Dispatch `mainnet` on `0.2.0` | mainnet | `0.2.0`, `latest` | same release, pre-release cleared, mainnet assets added | + +Three versions live in the repo; only two matter: -| Tag | `is_prerelease` | Docker tag | Meaning | -|-----|:---------------:|-----------|---------| -| `v0.1.0-rc.5` | true | `testnet-latest` | Testnet release candidate | -| `v0.1.0-alpha.1` / `-beta.1` / `-pre.1` | true | `testnet-latest` | Also prerelease → testnet | -| `v0.1.0` | false | `latest` | Stable release | +- `template/node/Cargo.toml` `version` — **the release**. Shown by telemetry and + `--version` as `0.2.0-`. Bump every release. +- `spec_version` in `template/runtime/src/lib.rs` — the on-chain runtime. Bump only + when the WASM changes; setCode rejects an equal or lower value. +- `template/runtime/Cargo.toml` `version` — cosmetic. Nothing reads it; keep it in + step with the node crate if you like. -The `-rc` / `-alpha` / `-beta` / `-pre` suffix is what classifies a tag as a -prerelease (`release.yml` metadata step). Increment `N` per RC (`rc.1`, `rc.2`, …). +Historic testnet tags (`v0.1.0-rc.1` … `v0.1.0-rc.27`) stay as they are; the first +tag under this scheme is `v0.2.0`. ### Prerequisite: GPG-signed tags @@ -234,26 +301,29 @@ git checkout main && git pull origin main # 2. Sanity-check what you're about to release git log -1 --oneline # the merge commit +grep -m1 '^version' template/node/Cargo.toml # == the tag you are about to cut git show main:template/runtime/src/lib.rs | grep spec_version # bumped if runtime change -# 3. Pick the next free tag — never reuse one +# 3. Confirm the tag is free — never reuse one git fetch origin --tags -git tag -l "v0.1.0-rc.*" | sort -V # last used → pick next N +git tag -l "v*" | sort -V | tail -3 # 4. Create the signed tag and push it -git tag -s v0.1.0-rc.N -m "" -git push origin v0.1.0-rc.N +git tag -s vX.Y.Z -m "" +git push origin vX.Y.Z ``` Pushing the tag triggers `release.yml` (build + publish). It does **not** deploy — -that's the separate manual dispatch (see runbooks above). +that's `runtime-upgrade.yml` (see runbooks above). ### Tag hygiene -- Tags are immutable release points. **Never reuse a tag** — each `-rc.N` cuts one - release from one commit. If a tag already exists, bump to the next `N`. -- If a release fails mid-run, fix the cause, merge to `main`, and cut the **next** - `N`. Do not delete and re-push the same tag. +- Tags are immutable release points. **Never reuse a tag** — each `vX.Y.Z` cuts one + release from one commit. +- If a release fails mid-run, fix the cause, bump the crate version, merge to + `main`, and cut the **next** version. Do not delete and re-push the same tag. +- A version that never reached mainnet is still a version. Gaps in mainnet's + sequence are expected; gaps in the tag sequence are not. --- @@ -263,21 +333,22 @@ The pipeline is the same; the caution level is not. | | Testnet | Mainnet | |-|---------|---------| -| Tag | `vX.Y.Z-rc.N` (prerelease → `testnet-latest`) | `vX.Y.Z` (stable → `latest`) | -| Runtime upgrade | Deploy directly via workflow | Behind on-chain governance / multisig, not a raw sudo setCode | +| Release | Tag push `vX.Y.Z` → `X.Y.Z-testnet`, `testnet-latest` | Dispatch `release.yml` with `mainnet` on a testnet-proven version → `X.Y.Z`, `latest` | +| Runtime upgrade | Deploy directly via workflow | Multisig, not a raw sudo setCode — separate flow, not `runtime-upgrade.yml` | | Weights hardware | CCX33 reference (see BENCHMARKING_STEPS.md); shared-vCPU tolerated for RC | Dedicated CCX only — shared-vCPU noise is not acceptable | | Rollout | Both nodes at once is fine | Canary one node, watch finality, then the rest | | Pre-deploy | Baseline check | Baseline check + a tested rollback runtime staged | **Mainnet rule:** never `sudo.setCode` a runtime that has not run on testnet first. -The testnet RC is the dress rehearsal for the mainnet release. +Every version ships to testnet before it can be promoted; that run is the dress +rehearsal for the mainnet release. --- ## Scripts - [`scripts/deploy-runtime.sh`](../scripts/deploy-runtime.sh) — `sudo.setCode`, checks - `spec_version` rose. Run by the `deploy-runtime` job. + `spec_version` rose. Run by `runtime-upgrade.yml`. - [`scripts/check-deployment.sh`](../scripts/check-deployment.sh) — reads `spec_version` / client version per RPC; `--expect-spec` / `--expect-impl` fail the command if a node is off-version.