Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 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
e1f180a
chore(upgrades): Paseo v0.8.0 to v1.0.0 in-place upgrade campaign (#331)
re-gius Oct 5, 2026
1dabc1f
Merge master up to #325 into dev/testnet-upgrades (content already pr…
re-gius Oct 6, 2026
3894681
Merge remote-tracking branch 'origin/master' into chore/dev-merge-master
re-gius Oct 6, 2026
6e983db
chore(upgrades): record the v1.0.0 PopLens on Paseo
re-gius Oct 6, 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
151 changes: 151 additions & 0 deletions .github/workflows/paseo-upgrade-step.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
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. Re-running one that already landed is harmless: a single-proxy upgrade swaps
# in the same code again, and the other steps skip what is already on chain.
#
# Key handling. Every step signs with `DOTNS_ADMIN_KEY` from the `testnet-upgrades`
# environment, the owner of every proxy and of the protocol registry. 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:}"
# Each step names the contracts it upgrades, whose snapshots are checked against the
# chain right before it broadcasts. Steps that upgrade no proxy name none.
case "$SCRIPT" in
UpgradeRegistry) SUBJECTS="DotnsRegistry" ;;
UpgradePopRules) SUBJECTS="PopRules" ;;
UpgradePopResolverAndController) SUBJECTS="DotnsPopResolver DotnsPopController" ;;
RedeployPopLens|DeclareRelease) SUBJECTS="" ;;
*)
echo "::error::'$SCRIPT' is not a runbook step; see docs/PASEO-V100-RUNBOOK.md"
exit 1
;;
esac
echo "script=$SCRIPT" >> "$GITHUB_OUTPUT"
echo "subjects=$SUBJECTS" >> "$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

# Scoped to the contracts this step upgrades. Every proxy an earlier step upgraded has
# moved past its snapshot by design; the ones this step is about to swap must still match,
# and a mismatch here means the code changed underneath the campaign.
- name: Verify snapshots against the deployed bytecode
if: steps.step.outputs.subjects != ''
env:
RPC_URL: http://127.0.0.1:8545
SNAPSHOT_SUBJECTS: ${{ steps.step.outputs.subjects }}
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.
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 parameter, configured as an environment variable on `testnet-upgrades`: the
# release tag `RedeployPopLens` scopes its salt to and `DeclareRelease` declares.
DOTNS_RELEASE_TAG: ${{ vars.DOTNS_RELEASE_TAG }}
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 the manifest `RedeployPopLens` writes 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."
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,10 +12,16 @@ 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/**"]
push:
branches: [master]
branches: [master, "dev/**"]
paths: ["contracts/**", "test/**", "**.sol"]
pull_request_target:
types: [closed]
Expand Down Expand Up @@ -107,15 +113,6 @@ jobs:
restore-keys: |
foundry-build-${{ runner.os }}-

# 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 @@ -166,20 +163,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 @@ -215,9 +198,122 @@ jobs:
if: always()
run: rm -rf cache/fuzz cache/invariant cache/test-failures

# 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 in a job of its own, separate from the matrix test runners.
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]
# A pull request from a fork gets a read-only token, so publishing the reports and the comment
# would fail.
if: >-
Expand Down Expand Up @@ -324,7 +420,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
11 changes: 11 additions & 0 deletions .github/workflows/release-metadata.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ jobs:
IS_DEPLOY_RECORD: ${{ contains(github.event.pull_request.labels.*.name, 'deployment-record') }}
run: |
changed=$(git diff --name-only "$BASE_SHA"...HEAD -- 'deployments/*/*.json')
# A manifest identical to master's was carried in by merging master into a long-lived
# branch, which records nothing new. On a pull request into master it cannot match.
edited=""
for f in $changed; do
if git diff --quiet origin/master HEAD -- "$f" 2>/dev/null; then
echo "Carried from master unchanged: $f"
else
edited="$edited $f"
fi
done
changed="${edited# }"
if [ -z "$changed" ]; then
echo "No live network manifest was edited."
exit 0
Expand Down
Loading
Loading