diff --git a/.github/workflows/publish-prerelease.yml b/.github/workflows/publish-prerelease.yml index 67cdf371a..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,6 +26,9 @@ concurrency: jobs: beta-release: runs-on: ubuntu-latest + # 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. env: @@ -87,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" @@ -125,7 +155,6 @@ jobs: 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..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,6 +26,9 @@ concurrency: jobs: release: runs-on: ubuntu-latest + # 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. env: @@ -82,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" @@ -113,15 +143,14 @@ 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. - 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/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. 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.