From b3edf43214c22c64578f6970436f0b23537b3dd9 Mon Sep 17 00:00:00 2001 From: giuseppere Date: Tue, 22 Sep 2026 11:44:29 +0200 Subject: [PATCH 1/2] Move dotNS admin key to `releases` environment --- .github/workflows/publish-prerelease.yml | 28 ++++++++++++++++++- .github/workflows/publish-release.yml | 34 +++++++++++++++++++++--- scripts/genesis/build-genesis.sh | 21 ++++++--------- 3 files changed, 65 insertions(+), 18 deletions(-) diff --git a/.github/workflows/publish-prerelease.yml b/.github/workflows/publish-prerelease.yml index 67cdf371a..e79be8a72 100644 --- a/.github/workflows/publish-prerelease.yml +++ b/.github/workflows/publish-prerelease.yml @@ -23,6 +23,9 @@ concurrency: jobs: beta-release: runs-on: ubuntu-latest + # Gated: this job reads the genesis owner key, so every run waits for approval on + # the `releases` environment, whose secret shadows any repository-level leftover. + environment: releases # Baked into the genesis registry, so it decides which networks the artifact suits. # Job-level: five steps name the file derived from it and must not disagree. env: @@ -120,12 +123,35 @@ jobs: # # Present on pre-releases as well as releases so the asset sets do not diverge, and so # this step is exercised before a real release depends on it. + # The environment pairs the key (secret) with its address (variable). Deriving one from + # the other before the build turns a mistyped key into a failed run instead of a genesis + # owned by an address nobody holds. The address is public; the key is never printed. + - name: Check the owner key against its declared address + env: + DOTNS_ADMIN_KEY: ${{ secrets.DOTNS_ADMIN_KEY }} + DOTNS_ADMIN_ADDRESS: ${{ vars.DOTNS_ADMIN_ADDRESS }} + run: | + set -euo pipefail + if [ -z "$DOTNS_ADMIN_KEY" ]; then + echo "::error::DOTNS_ADMIN_KEY is not configured on the releases environment" + exit 1 + fi + if [ -z "$DOTNS_ADMIN_ADDRESS" ]; then + echo "::error::DOTNS_ADMIN_ADDRESS is not configured on the releases environment" + exit 1 + fi + DERIVED="$(cast wallet address --private-key "$DOTNS_ADMIN_KEY")" + if [ "$(printf '%s' "$DERIVED" | tr 'A-Z' 'a-z')" != "$(printf '%s' "$DOTNS_ADMIN_ADDRESS" | tr 'A-Z' 'a-z')" ]; then + echo "::error::DOTNS_ADMIN_KEY derives to $DERIVED, the environment declares $DOTNS_ADMIN_ADDRESS" + exit 1 + fi + echo "Owner key derives to the declared address $DERIVED" + - name: Build pallet-revive genesis env: FOUNDRY_DISABLE_NIGHTLY_WARNING: "1" FACTORY_DEPLOYER_KEY: ${{ secrets.FACTORY_DEPLOYER_KEY }} DOTNS_ADMIN_KEY: ${{ secrets.DOTNS_ADMIN_KEY }} - DOTNS_ADMIN_MNEMONIC: ${{ secrets.DOTNS_ADMIN_MNEMONIC }} run: bash scripts/genesis/build-genesis.sh release - name: Extract ABIs diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 3ae5748f4..d6d257ad3 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -23,6 +23,9 @@ concurrency: jobs: release: runs-on: ubuntu-latest + # Gated: this job reads the genesis owner key, so every run waits for approval on + # the `releases` environment, whose secret shadows any repository-level leftover. + environment: releases # Baked into the genesis registry, so it decides which networks the artifact suits. # Job-level: five steps name the file derived from it and must not disagree. env: @@ -113,15 +116,38 @@ jobs: # contracts, the deploy scripts and the CREATE3 factory key that fixes every address, # and the artifact then ships from the same commit as the ABIs beside it. # - # The owner key is taken from DOTNS_ADMIN_KEY if set, otherwise derived from - # DOTNS_ADMIN_MNEMONIC. It ends up owning the registry, resolvers, registrar, store factory - # and beacons in the genesis storage, so the script refuses to run without one. + # The owner key is DOTNS_ADMIN_KEY, checked against its declared address above. It + # ends up owning the registry, resolvers, registrar, store factory and beacons in the + # genesis storage, so the script refuses to run without it. + # The environment pairs the key (secret) with its address (variable). Deriving one from + # the other before the build turns a mistyped key into a failed run instead of a genesis + # owned by an address nobody holds. The address is public; the key is never printed. + - name: Check the owner key against its declared address + env: + DOTNS_ADMIN_KEY: ${{ secrets.DOTNS_ADMIN_KEY }} + DOTNS_ADMIN_ADDRESS: ${{ vars.DOTNS_ADMIN_ADDRESS }} + run: | + set -euo pipefail + if [ -z "$DOTNS_ADMIN_KEY" ]; then + echo "::error::DOTNS_ADMIN_KEY is not configured on the releases environment" + exit 1 + fi + if [ -z "$DOTNS_ADMIN_ADDRESS" ]; then + echo "::error::DOTNS_ADMIN_ADDRESS is not configured on the releases environment" + exit 1 + fi + DERIVED="$(cast wallet address --private-key "$DOTNS_ADMIN_KEY")" + if [ "$(printf '%s' "$DERIVED" | tr 'A-Z' 'a-z')" != "$(printf '%s' "$DOTNS_ADMIN_ADDRESS" | tr 'A-Z' 'a-z')" ]; then + echo "::error::DOTNS_ADMIN_KEY derives to $DERIVED, the environment declares $DOTNS_ADMIN_ADDRESS" + exit 1 + fi + echo "Owner key derives to the declared address $DERIVED" + - name: Build pallet-revive genesis env: FOUNDRY_DISABLE_NIGHTLY_WARNING: "1" FACTORY_DEPLOYER_KEY: ${{ secrets.FACTORY_DEPLOYER_KEY }} DOTNS_ADMIN_KEY: ${{ secrets.DOTNS_ADMIN_KEY }} - DOTNS_ADMIN_MNEMONIC: ${{ secrets.DOTNS_ADMIN_MNEMONIC }} run: bash scripts/genesis/build-genesis.sh release - name: Extract ABIs diff --git a/scripts/genesis/build-genesis.sh b/scripts/genesis/build-genesis.sh index 135236438..4044ac426 100755 --- a/scripts/genesis/build-genesis.sh +++ b/scripts/genesis/build-genesis.sh @@ -43,13 +43,14 @@ CANONICAL_MANIFEST="deployments/expected.json" # does: this key ends up owning the registry, the resolvers, the registrar, the # store factory and the beacons. # -# Accepted, in order of precedence: -# DOTNS_ADMIN_KEY a raw private key — the admin credential this repo already holds -# DOTNS_ADMIN_MNEMONIC the admin mnemonic; index $DOTNS_ADMIN_INDEX (default 0) +# Accepted: +# DOTNS_ADMIN_KEY a raw private key — in CI it lives on the `releases` environment, +# behind a required reviewer, paired with a DOTNS_ADMIN_ADDRESS +# variable the workflow checks the key against before this runs # -# Deliberately NOT accepted: DOTNS_MNEMONIC. That is an operational credential for driving -# the `dotns` CLI, not the contract admin, and quietly making it the owner of every -# contract in a genesis would be a hard mistake to spot. +# Deliberately NOT accepted: DOTNS_ADMIN_MNEMONIC and DOTNS_MNEMONIC. A second credential +# accepted here would let a different secret silently decide who owns every contract in a +# genesis, and a wrong owner is a hard mistake to spot. One explicit key, or a loud failure. # Not DEPLOYER_KEY: that name is dotns-releases' own secret, and accepting it here # would make which key owns a published genesis depend on which repo the build ran in. ADMIN_KEY="${DOTNS_ADMIN_KEY:-}" @@ -84,15 +85,9 @@ for tool in forge anvil cast node jq curl; do command -v "$tool" >/dev/null 2>&1 || { echo "Error: $tool is not on PATH" >&2; exit 1; } done -# Needs cast, so it happens after the check above. -if [ -z "$ADMIN_KEY" ] && [ -n "${DOTNS_ADMIN_MNEMONIC:-}" ]; then - ADMIN_KEY="$(cast wallet private-key --mnemonic "$DOTNS_ADMIN_MNEMONIC" "${DOTNS_ADMIN_INDEX:-0}")" - echo "Owner key derived from DOTNS_ADMIN_MNEMONIC, index ${DOTNS_ADMIN_INDEX:-0}." -fi - if [ -z "$ADMIN_KEY" ]; then cat >&2 <<'MSG' -Error: no owner key. Set DOTNS_ADMIN_KEY or DOTNS_ADMIN_MNEMONIC. +Error: no owner key. Set DOTNS_ADMIN_KEY. Whichever is given becomes the owner of every DotNS contract in the genesis state, so this build refuses to fall back to a public dev key. From e0d1244970854f5166ca23878f5025a1f4917dab Mon Sep 17 00:00:00 2001 From: giuseppere Date: Tue, 22 Sep 2026 14:19:37 +0200 Subject: [PATCH 2/2] reorg CI steps + add docs --- .github/workflows/publish-prerelease.yml | 55 +++++++++++++----------- .github/workflows/publish-release.yml | 55 +++++++++++++----------- RELEASE_ARTIFACTS.md | 14 ++++++ 3 files changed, 72 insertions(+), 52 deletions(-) diff --git a/.github/workflows/publish-prerelease.yml b/.github/workflows/publish-prerelease.yml index e79be8a72..7b42df553 100644 --- a/.github/workflows/publish-prerelease.yml +++ b/.github/workflows/publish-prerelease.yml @@ -1,5 +1,8 @@ name: Publish Beta Package +# Creating a `v*` tag is restricted to the dotns team by the `release tags` ruleset, and the +# job below pauses on the `releases` environment for a reviewer, so a release takes two +# distinct human actions: cutting the tag, and approving the run it starts. on: push: tags: @@ -23,8 +26,8 @@ concurrency: jobs: beta-release: runs-on: ubuntu-latest - # Gated: this job reads the genesis owner key, so every run waits for approval on - # the `releases` environment, whose secret shadows any repository-level leftover. + # Gated: this job reads the genesis owner key, which lives only on the `releases` + # environment, so every run waits for a reviewer's approval there. environment: releases # Baked into the genesis registry, so it decides which networks the artifact suits. # Job-level: five steps name the file derived from it and must not disagree. @@ -90,6 +93,30 @@ jobs: - uses: ./.github/actions/setup-foundry + # The environment pairs the key (secret) with its address (variable). Deriving one from + # the other before the build turns a mistyped key into a failed run instead of a genesis + # owned by an address nobody holds. The address is public; the key is never printed. + - name: Check the owner key against its declared address + env: + DOTNS_ADMIN_KEY: ${{ secrets.DOTNS_ADMIN_KEY }} + DOTNS_ADMIN_ADDRESS: ${{ vars.DOTNS_ADMIN_ADDRESS }} + run: | + set -euo pipefail + if [ -z "$DOTNS_ADMIN_KEY" ]; then + echo "::error::DOTNS_ADMIN_KEY is not configured on the releases environment" + exit 1 + fi + if [ -z "$DOTNS_ADMIN_ADDRESS" ]; then + echo "::error::DOTNS_ADMIN_ADDRESS is not configured on the releases environment" + exit 1 + fi + DERIVED="$(cast wallet address --private-key "$DOTNS_ADMIN_KEY")" + if [ "$(printf '%s' "$DERIVED" | tr 'A-Z' 'a-z')" != "$(printf '%s' "$DOTNS_ADMIN_ADDRESS" | tr 'A-Z' 'a-z')" ]; then + echo "::error::DOTNS_ADMIN_KEY derives to $DERIVED, the environment declares $DOTNS_ADMIN_ADDRESS" + exit 1 + fi + echo "Owner key derives to the declared address $DERIVED" + - uses: oven-sh/setup-bun@v2 with: bun-version: "1.2.6" @@ -123,30 +150,6 @@ jobs: # # Present on pre-releases as well as releases so the asset sets do not diverge, and so # this step is exercised before a real release depends on it. - # The environment pairs the key (secret) with its address (variable). Deriving one from - # the other before the build turns a mistyped key into a failed run instead of a genesis - # owned by an address nobody holds. The address is public; the key is never printed. - - name: Check the owner key against its declared address - env: - DOTNS_ADMIN_KEY: ${{ secrets.DOTNS_ADMIN_KEY }} - DOTNS_ADMIN_ADDRESS: ${{ vars.DOTNS_ADMIN_ADDRESS }} - run: | - set -euo pipefail - if [ -z "$DOTNS_ADMIN_KEY" ]; then - echo "::error::DOTNS_ADMIN_KEY is not configured on the releases environment" - exit 1 - fi - if [ -z "$DOTNS_ADMIN_ADDRESS" ]; then - echo "::error::DOTNS_ADMIN_ADDRESS is not configured on the releases environment" - exit 1 - fi - DERIVED="$(cast wallet address --private-key "$DOTNS_ADMIN_KEY")" - if [ "$(printf '%s' "$DERIVED" | tr 'A-Z' 'a-z')" != "$(printf '%s' "$DOTNS_ADMIN_ADDRESS" | tr 'A-Z' 'a-z')" ]; then - echo "::error::DOTNS_ADMIN_KEY derives to $DERIVED, the environment declares $DOTNS_ADMIN_ADDRESS" - exit 1 - fi - echo "Owner key derives to the declared address $DERIVED" - - name: Build pallet-revive genesis env: FOUNDRY_DISABLE_NIGHTLY_WARNING: "1" diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index d6d257ad3..9d0a18dd2 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -1,5 +1,8 @@ name: Publish Release Package +# Creating a `v*` tag is restricted to the dotns team by the `release tags` ruleset, and the +# job below pauses on the `releases` environment for a reviewer, so a release takes two +# distinct human actions: cutting the tag, and approving the run it starts. on: push: tags: @@ -23,8 +26,8 @@ concurrency: jobs: release: runs-on: ubuntu-latest - # Gated: this job reads the genesis owner key, so every run waits for approval on - # the `releases` environment, whose secret shadows any repository-level leftover. + # Gated: this job reads the genesis owner key, which lives only on the `releases` + # environment, so every run waits for a reviewer's approval there. environment: releases # Baked into the genesis registry, so it decides which networks the artifact suits. # Job-level: five steps name the file derived from it and must not disagree. @@ -85,6 +88,30 @@ jobs: - uses: ./.github/actions/setup-foundry + # The environment pairs the key (secret) with its address (variable). Deriving one from + # the other before the build turns a mistyped key into a failed run instead of a genesis + # owned by an address nobody holds. The address is public; the key is never printed. + - name: Check the owner key against its declared address + env: + DOTNS_ADMIN_KEY: ${{ secrets.DOTNS_ADMIN_KEY }} + DOTNS_ADMIN_ADDRESS: ${{ vars.DOTNS_ADMIN_ADDRESS }} + run: | + set -euo pipefail + if [ -z "$DOTNS_ADMIN_KEY" ]; then + echo "::error::DOTNS_ADMIN_KEY is not configured on the releases environment" + exit 1 + fi + if [ -z "$DOTNS_ADMIN_ADDRESS" ]; then + echo "::error::DOTNS_ADMIN_ADDRESS is not configured on the releases environment" + exit 1 + fi + DERIVED="$(cast wallet address --private-key "$DOTNS_ADMIN_KEY")" + if [ "$(printf '%s' "$DERIVED" | tr 'A-Z' 'a-z')" != "$(printf '%s' "$DOTNS_ADMIN_ADDRESS" | tr 'A-Z' 'a-z')" ]; then + echo "::error::DOTNS_ADMIN_KEY derives to $DERIVED, the environment declares $DOTNS_ADMIN_ADDRESS" + exit 1 + fi + echo "Owner key derives to the declared address $DERIVED" + - uses: oven-sh/setup-bun@v2 with: bun-version: "1.2.6" @@ -119,30 +146,6 @@ jobs: # The owner key is DOTNS_ADMIN_KEY, checked against its declared address above. It # ends up owning the registry, resolvers, registrar, store factory and beacons in the # genesis storage, so the script refuses to run without it. - # The environment pairs the key (secret) with its address (variable). Deriving one from - # the other before the build turns a mistyped key into a failed run instead of a genesis - # owned by an address nobody holds. The address is public; the key is never printed. - - name: Check the owner key against its declared address - env: - DOTNS_ADMIN_KEY: ${{ secrets.DOTNS_ADMIN_KEY }} - DOTNS_ADMIN_ADDRESS: ${{ vars.DOTNS_ADMIN_ADDRESS }} - run: | - set -euo pipefail - if [ -z "$DOTNS_ADMIN_KEY" ]; then - echo "::error::DOTNS_ADMIN_KEY is not configured on the releases environment" - exit 1 - fi - if [ -z "$DOTNS_ADMIN_ADDRESS" ]; then - echo "::error::DOTNS_ADMIN_ADDRESS is not configured on the releases environment" - exit 1 - fi - DERIVED="$(cast wallet address --private-key "$DOTNS_ADMIN_KEY")" - if [ "$(printf '%s' "$DERIVED" | tr 'A-Z' 'a-z')" != "$(printf '%s' "$DOTNS_ADMIN_ADDRESS" | tr 'A-Z' 'a-z')" ]; then - echo "::error::DOTNS_ADMIN_KEY derives to $DERIVED, the environment declares $DOTNS_ADMIN_ADDRESS" - exit 1 - fi - echo "Owner key derives to the declared address $DERIVED" - - name: Build pallet-revive genesis env: FOUNDRY_DISABLE_NIGHTLY_WARNING: "1" diff --git a/RELEASE_ARTIFACTS.md b/RELEASE_ARTIFACTS.md index 33fd160e1..584730ad4 100644 --- a/RELEASE_ARTIFACTS.md +++ b/RELEASE_ARTIFACTS.md @@ -115,6 +115,20 @@ With `--tag vX.Y.Z` it additionally checks the chain's own declarations: `protoc ## Publishing +Both publish workflows run in the `releases` environment, because building a genesis reads the +key that owns every contract in it. The environment holds: + +- `DOTNS_ADMIN_KEY` (secret): the genesis owner's private key. +- `DOTNS_ADMIN_ADDRESS` (variable): the address that key derives to. The workflow checks the + pair right after the toolchain is installed and fails the run on a mismatch, so a mistyped + key dies before anything is built. +- Required reviewers: the dotns team. Every release run pauses for one approval. +- Deployment refs: `master` (for `workflow_dispatch`) and `v[0-9]*` tags. A dispatch started + from any other branch stops at the environment gate. + +Creating a `v*` tag is itself restricted to the dotns team by the `release tags` ruleset, so a +release takes two distinct human actions: cutting the tag, and approving the run it starts. + `deployments.json`, `release-manifest.json`, and `codehashes.json` are generated during the release by `scripts/js/release-metadata.mjs build`, from the committed deployment manifests and the build that just ran; `abi-diff.json` comes from `abidiff` against the previous release's published ABIs. Neither is committed: an address stored in two tracked files eventually disagrees with itself, so `deployments//.json` is the only tracked copy. That file holds exactly one address per contract, the current one. Each deploy overwrites the entries it produces, so it tracks only the latest deployment for a network and never a history of them; previous address sets exist only in this repository's git history. It also carries no implementation addresses behind the UUPS proxies, and no record of which commit was deployed.