Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5f9be44
chore: initial commit
sphamjoli Sep 8, 2026
78896ef
ci: report upgrade fork tests as their own CI Summary shard
sphamjoli Sep 8, 2026
9bf7069
ci: run jobs on the parity xl runner and isolate the upgrade fork job
sphamjoli Sep 9, 2026
075de7e
chore: revert CI runners to ubuntu-latest and remove the upgrade-PR s…
sphamjoli Sep 9, 2026
f1fe9bc
chore: revert CI to ubuntu-latest and remove the upgrade-PR scaffolding
sphamjoli Sep 9, 2026
8d436cc
Merge feat/pop-numeric-namespace and add in-place upgrade artifacts
sphamjoli Sep 10, 2026
64473f8
Merge the subname-issuer and lite-node changes into the upgrade branch
sphamjoli Sep 10, 2026
8f84561
Merge the deferred store-write docs and tests into the upgrade branch
sphamjoli Sep 10, 2026
c852004
feat: in-place upgrade for the numeric-namespace proxies (#289)
sphamjoli Sep 14, 2026
837c921
Merge master (v0.8.0) into spha/registrar-upgrade
re-gius Sep 17, 2026
97d4b75
chore: merge master (v0.8.0) into the upgrade branch (#308)
re-gius Sep 17, 2026
ddce6f5
Snapshot the deployed implementations, and check them against the chain
re-gius Sep 17, 2026
c34bb4d
Run the layout diff for every upgraded proxy in ordinary CI, and fix …
re-gius Sep 17, 2026
29b5d0c
Add the upgrade scripts, their fork tests, and the store-factory migr…
re-gius Sep 18, 2026
e0641d2
Close the review findings on the store-factory migration
re-gius Sep 18, 2026
c311be5
Make the migration's deploy leg reachable, and stop it erasing the ol…
re-gius Sep 18, 2026
5a57a45
feat: in-place upgrade to v0.8.0 for every Paseo proxy (#310)
re-gius Sep 18, 2026
e2da058
Point the upgrade-branch references at dev/testnet-upgrades
re-gius Sep 18, 2026
2d5f746
chore: point the upgrade-branch references at dev/testnet-upgrades (#…
re-gius Sep 18, 2026
3d76ad9
Wire the key the declaration would have failed on, and run the fork s…
re-gius Sep 18, 2026
2599901
Rotate ownership to a fresh key, as step 0 of the runbook
re-gius Sep 21, 2026
091fcf0
Broadcast one runbook step per label on the review PR, gated and off …
re-gius Sep 21, 2026
fa9ad0d
docs: state the rotation rationale without provenance details
re-gius Sep 21, 2026
fb28eb0
chore: gate broadcasts behind an environment named for the key, not t…
re-gius Sep 21, 2026
94a5c7d
Sweep the old balance inside the rotation, and stop funding by hand
re-gius Sep 21, 2026
6e6b5c5
fix: mark the adapter wait script executable
re-gius Sep 21, 2026
3df6488
fix: wait for the adapter to be broadcast-ready, and no less
re-gius Sep 21, 2026
a08d7e8
fix: absorb transient RPC blips in the snapshot verifier
re-gius Sep 21, 2026
f82a696
Merge branch 'master' into dev/testnet-upgrades
re-gius Sep 21, 2026
f0b5aab
fix: give the adapter enough block history to serve a broadcaster
re-gius Sep 21, 2026
2128dc2
fix(migration): page the store import to fit pallet-revive block weight
re-gius Sep 22, 2026
269cd1e
Update Paseo manifest after upgrade to v0.8.0
re-gius Sep 22, 2026
a1da527
upgrade verification step
re-gius Sep 22, 2026
468f5a5
docs and runbook updates with latest findings
re-gius Sep 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions .github/workflows/paseo-upgrade-step.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
name: Paseo Upgrade Step

# Broadcasts one runbook step against Paseo Asset Hub Next, driven by labels on the review pull
# request. Adding `run:UpgradeRegistry` runs that script, once, after a human approves.
#
# Why labels on a PR, of all things. A dispatchable workflow only registers from the default
# branch, and nothing that can broadcast with the owner key is going on master. A `pull_request`
# workflow is taken from the PR itself, so this file lives on `dev/testnet-upgrades` alone and
# still runs, and the review PR whose base is master becomes the broadcast console. It is
# unconventional and it is the only shape that keeps master clean, keeps the approval gate, and
# gives one explicit human action per step.
#
# The gate is the `testnet-upgrades` environment: every run pauses until a required reviewer
# approves it, whoever added the label. The label is the request; the approval is the decision.
# Labels are also how retries work: the job removes its label when it finishes, so re-adding it
# re-runs the step. The rotation script skips what already moved and the twelve swaps are
# idempotent; step 13 is the exception, and its runbook section says to inspect, not re-add.
#
# Key handling. Step 0 signs with the repository-level `DOTNS_ADMIN_KEY`, because its value
# cannot be read by anyone, so it cannot be re-entered as an environment secret. That exposure
# is time-boxed: step 0's whole purpose is to make that key powerless. The fresh key the
# operator generates IS entered as an environment secret under the same name, which shadows the
# repository one for every later step, and the repository copy is then deleted. Runner logs on
# this public repository are world-readable, so nothing here may ever echo key material; the
# password below only encrypts a keystore that lives for one run.

on:
pull_request:
types: [labeled]
branches: [master]

permissions:
contents: read
pull-requests: write

# One broadcast at a time, queued not cancelled: two runs would race nonces on the same chain,
# and a queued step must never be dropped just because someone labelled twice.
concurrency:
group: testnet-upgrades-broadcast
cancel-in-progress: false

jobs:
broadcast:
name: ${{ github.event.label.name }}
# Same-repo head only. A fork's PR gets no secrets anyway, but the guard makes the intent
# readable instead of implicit.
if: >-
startsWith(github.event.label.name, 'run:')
&& github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
environment: testnet-upgrades
steps:
- name: Resolve and validate the requested step
id: step
env:
LABEL: ${{ github.event.label.name }}
run: |
set -euo pipefail
SCRIPT="${LABEL#run:}"
case "$SCRIPT" in
RotateOwnership|UpgradeProtocolRegistry|UpgradeRegistry|UpgradeRegistrar|\
UpgradeRegistrarController|UpgradePopController|UpgradePopRules|\
UpgradeNameEscrow|UpgradeNameWhitelist|UpgradeResolver|UpgradeReverseResolver|\
UpgradeContentResolver|UpgradePopResolver|MigrateStoreFactory|DeclareRelease) ;;
*)
echo "::error::'$SCRIPT' is not a runbook step; see docs/PASEO-V080-RUNBOOK.md"
exit 1
;;
esac
echo "script=$SCRIPT" >> "$GITHUB_OUTPUT"

# The head commit, not the merge ref. A pull_request checkout defaults to the PR merged
# into its base, and master merged into this branch is not the code that was reviewed or
# fork-tested. Pinning the SHA also means the approver knows exactly what will run: a push
# to the branch after the label does not move an approved run.
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
submodules: recursive

- name: Setup Bun
uses: oven-sh/setup-bun@v2
- run: bun install

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

# The same adapter the deploy pipeline broadcasts through, pointed at the same node. The
# hosted gateway has only ever been exercised read-only here, and a broadcast is the wrong
# moment to learn whether it accepts transactions.
- name: Start the ETH-RPC adapter
run: |
docker compose up --build -d eth-rpc
scripts/shell/wait-for-eth-rpc.sh http://127.0.0.1:8545

# Only before anything has been broadcast: the moment step 1 lands, the snapshots stop
# matching the chain by design, so this gate is pinned to step 0 instead of pretending to
# be a mid-flight health check.
- name: Verify snapshots against the deployed bytecode
if: steps.step.outputs.script == 'RotateOwnership'
env:
RPC_URL: http://127.0.0.1:8545
run: scripts/shell/verify-snapshots.sh

- name: Broadcast ${{ steps.step.outputs.script }}
env:
SCRIPT: ${{ steps.step.outputs.script }}
RPC_URL: http://127.0.0.1:8545
# `_account.sh` imports this into a run-local keystore when none exists. Environment
# secrets shadow repository ones for jobs that declare the environment, which is what
# switches every post-rotation step onto the fresh key without touching this file.
PRIVATE_KEY: ${{ secrets.DOTNS_ADMIN_KEY }}
# Encrypts a keystore that exists for the lifetime of this runner. Masked because no
# log line should print even throwaway credentials in cleartext.
ACCOUNT_PASSWORD: testnet-upgrades-run-${{ github.run_id }}
# Step parameters, configured as environment variables on `testnet-upgrades`:
# DOTNS_NEW_OWNER for step 0, DOTNS_RELEASE_TAG for step 14, and optionally
# DOTNS_OLD_STORE_FACTORY as the step 13 cross-check.
DOTNS_NEW_OWNER: ${{ vars.DOTNS_NEW_OWNER }}
DOTNS_RELEASE_TAG: ${{ vars.DOTNS_RELEASE_TAG }}
DOTNS_OLD_STORE_FACTORY: ${{ vars.DOTNS_OLD_STORE_FACTORY }}
run: |
set -euo pipefail
echo "::add-mask::$ACCOUNT_PASSWORD"
./scripts/deploy/upgrade.sh

# The broadcast record and the manifest are the two things a step changes on disk. The
# manifest cannot be pushed back from here: the branch requires signed commits, which a
# runner cannot produce, so step 13's manifest change is committed by the operator from
# the artifact. Everything else leaves the workspace unchanged.
- name: Keep the broadcast record
if: always()
uses: actions/upload-artifact@v4
with:
name: ${{ steps.step.outputs.script }}-${{ github.run_id }}
path: |
broadcast/
deployments/
if-no-files-found: ignore
retention-days: 90

- name: Report and clear the label
if: always()
env:
GH_TOKEN: ${{ github.token }}
RESULT: ${{ job.status }}
SCRIPT: ${{ steps.step.outputs.script }}
PR: ${{ github.event.pull_request.number }}
LABEL: ${{ github.event.label.name }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
gh pr comment "$PR" --repo "$GITHUB_REPOSITORY" \
--body "**${SCRIPT}**: ${RESULT}. [Run](${RUN_URL}). Broadcast record and manifest attached as artifacts. Re-add \`${LABEL}\` to retry; for MigrateStoreFactory, read the runbook before retrying anything."
gh pr edit "$PR" --repo "$GITHUB_REPOSITORY" --remove-label "$LABEL"
150 changes: 123 additions & 27 deletions .github/workflows/push_checking.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,17 @@ name: Run Solidity Tests
# inside a `pull_request_target` job would let a malicious PR exfiltrate
# repo secrets. Re-review carefully if this trigger surface widens.
on:
# `dev/**` covers the long-lived upgrade branches, which are never merged to
# master and therefore never get tested by the master triggers alone. Without
# them a PR into an upgrade branch runs lint and nothing else, which is how a
# storage snapshot that no longer matched the live chain reached review.
# Matched by prefix, not by name, so the next such branch is covered the day
# it is created instead of the day someone remembers this file.
pull_request:
branches: [master]
branches: [master, "dev/**"]
paths: ["contracts/**", "test/**", "**.sol"]
push:
branches: [master]
branches: [master, "dev/**"]
paths: ["contracts/**", "test/**", "**.sol"]
pull_request_target:
types: [closed]
Expand Down Expand Up @@ -78,15 +84,6 @@ jobs:

- run: bun install

# Only the unit-fuzz job needs the fork adapter, and only when fork tests
# actually exist in the tree. Fork tests are PR-scoped, so `test/fork/` is
# empty on master and `hashFiles` returns '' (step skipped, no docker build).
- name: Start revive-eth-rpc (paseo_local fork target)
if: matrix.kind.id == 'unit-fuzz' && hashFiles('test/fork/**') != ''
run: |
docker compose up -d --build
bash scripts/shell/wait-for-eth-rpc.sh

- name: Run tests
id: run
env:
Expand Down Expand Up @@ -122,20 +119,6 @@ jobs:
exit 1
fi

# Fork tests run against the revive-eth-rpc adapter started above, only
# when present. The preceding `forge build` already produced full build-info
# for the OZ upgrade validator, so this reuses it.
- name: Run fork tests
if: matrix.kind.id == 'unit-fuzz' && hashFiles('test/fork/**') != ''
env:
FOUNDRY_DISABLE_NIGHTLY_WARNING: "1"
FOUNDRY_PROFILE: "ci"
run: forge test -vv --match-path 'test/fork/**'

- name: Tear down revive-eth-rpc
if: always() && matrix.kind.id == 'unit-fuzz' && hashFiles('test/fork/**') != ''
run: docker compose down --volumes --remove-orphans

- name: Stage matrix-kind output for the aggregator
if: always()
run: |
Expand Down Expand Up @@ -163,9 +146,122 @@ jobs:
- if: steps.run.outputs.result && contains(steps.run.outputs.result, 'Failed')
run: exit 1

# Fork tests validate the PR-scoped upgrade scripts against live Paseo Asset Hub
# state through the ETH-RPC adapter. They exist only during an upgrade PR, so a
# cheap detect job decides whether the heavy fork runner is provisioned at all:
# nothing sets up on a PR without `test/fork/**`.
detect-fork:
if: github.event.action != 'closed'
runs-on: ubuntu-latest
outputs:
has_fork: ${{ steps.detect.outputs.has_fork }}
steps:
- uses: actions/checkout@v4
- id: detect
run: |
if ls test/fork/*.t.sol > /dev/null 2>&1; then
echo "has_fork=true" >> "$GITHUB_OUTPUT"
else
echo "has_fork=false" >> "$GITHUB_OUTPUT"
fi

fork:
name: Upgrade Fork Tests
needs: detect-fork
if: github.event.action != 'closed' && needs.detect-fork.outputs.has_fork == 'true'
# The fork job builds the revive ETH-RPC adapter image and runs the upgrade
# suite, so it is kept off the shared test runner and onto the Parity XL runner.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- uses: ./.github/actions/setup-foundry

- uses: oven-sh/setup-bun@v2
with:
bun-version: "1.2.6"
no-cache: true

- uses: actions/setup-node@v4
with:
node-version: "20"

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- uses: actions/cache@v4
with:
path: node_modules
key: bun-${{ hashFiles('bun.lock') }}
restore-keys: bun-

- run: bun install

# Reuse the repository's docker compose eth-rpc service as the paseo_local
# fork target.
- name: Start revive-eth-rpc (paseo_local fork target)
run: |
docker compose up -d --build
bash scripts/shell/wait-for-eth-rpc.sh

- name: Run fork tests
id: fork
env:
FOUNDRY_DISABLE_NIGHTLY_WARNING: "1"
FOUNDRY_PROFILE: "ci"
run: |
set +e
# A clean build-info keeps the OZ upgrade validator (vm.ffi) from reading
# a partial JSON on a warm self-hosted runner.
rm -rf out/build-info
# The layout diff compares whatever pair it is handed, so it stays green when a
# snapshot has drifted away from the implementation actually deployed, and a
# calldata-only change leaves no trace in a layout at all. This is the only check
# that catches that, and it belongs here as well as in `fork-tests.sh`: CI runs
# `forge test` directly, so a local-only guard protects nobody reviewing a PR.
scripts/shell/verify-snapshots.sh 2>&1 | tee -a fork.log
SNAPSHOTS=${PIPESTATUS[0]}
if [ "$SNAPSHOTS" -ne 0 ]; then
echo "result=Failed (snapshots)" >> "$GITHUB_OUTPUT"
exit 1
fi
forge test -vv --match-path 'test/fork/**' 2>&1 | tee -a fork.log
FORK=${PIPESTATUS[0]}
if [ "$FORK" -eq 0 ]; then
echo "result=Passed" >> "$GITHUB_OUTPUT"
else
echo "result=Failed" >> "$GITHUB_OUTPUT"
fi
exit "$FORK"

- name: Tear down revive-eth-rpc
if: always()
run: docker compose down --volumes --remove-orphans

# Report the outcome as its own CI Summary row through a shard artifact the
# report job renders. `always()` so a failed run still uploads the Failed row
# and the fork log for the reviewer.
- name: Stage upgrade-fork output for the aggregator
if: always()
run: |
mkdir -p forkshard
printf 'Upgrade Fork Tests|%s|false\n' \
"${{ steps.fork.outputs.result || 'Failed' }}" > forkshard/result.txt
[ -f fork.log ] && cp fork.log forkshard/fork.log || true

- uses: actions/upload-artifact@v4
if: always()
with:
name: test-shard-fork
path: forkshard
retention-days: 7

report:
name: Report Test Results
needs: test
needs: [test, fork]
if: always() && github.event_name == 'pull_request' && github.event.action != 'closed'
runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -267,7 +363,7 @@ jobs:
}
}

const order = ['4naly3er Analysis', 'Slither Analysis', 'Contract Tests (Unit + Fuzz)', 'Contract Tests (Invariant)', 'Gas Report', 'Coverage', 'Documentation', 'Format & Lint', 'File Validation', 'Deploy Contracts', 'PR Title', 'Labels'];
const order = ['4naly3er Analysis', 'Slither Analysis', 'Contract Tests (Unit + Fuzz)', 'Contract Tests (Invariant)', 'Upgrade Fork Tests', 'Gas Report', 'Coverage', 'Documentation', 'Format & Lint', 'File Validation', 'Deploy Contracts', 'PR Title', 'Labels'];
const sortedKeys = Object.keys(rows).sort((a, b) => (order.indexOf(a) === -1 ? 999 : order.indexOf(a)) - (order.indexOf(b) === -1 ? 999 : order.indexOf(b)));
let table = `| Check | Result |\n|:------|:-------|\n`;
for (const key of sortedKeys) table += `| ${key} | ${rows[key]} |\n`;
Expand Down
Loading
Loading