diff --git a/.github/workflows/deploy-contracts.yml b/.github/workflows/deploy-contracts.yml index 52e4184c..54ee9b15 100644 --- a/.github/workflows/deploy-contracts.yml +++ b/.github/workflows/deploy-contracts.yml @@ -3,10 +3,12 @@ name: Deploy Contracts and Verify artifacts on: pull_request: branches: [master] - paths: ["contracts/**", "scripts/**", "**.sol", "package.json", "bun.lock"] + paths: + ["contracts/**", "scripts/**", "deployments/**", "**.sol", "package.json", "bun.lock"] push: branches: [master] - paths: ["contracts/**", "scripts/**", "**.sol", "package.json", "bun.lock"] + paths: + ["contracts/**", "scripts/**", "deployments/**", "**.sol", "package.json", "bun.lock"] permissions: contents: read @@ -23,12 +25,13 @@ jobs: # Shared across steps. ACCOUNT_* is anvil test account 7 (public test keys, # never valid on a real network) used to run the pipeline. FACTORY_DEPLOYER is - # the public address of the single-purpose factory key; PINNED_FACTORY is its - # nonce-0 CREATE address and the one recorded in every manifest. CANONICAL is - # the committed manifest that seeds DEPLOYMENTS.md; MANIFEST is the file this - # CI deploy writes. PRIVATE_KEY is deliberately absent here: it is set only on - # the first deploy to import the keystore, and must not reach the resume run, - # which reuses the already-imported account. + # the public address of the single-purpose factory key. CANONICAL is the + # committed manifest, the address set this deploy has to reproduce; MANIFEST is + # the file this CI deploy writes. PINNED_FACTORY is read out of CANONICAL after + # checkout rather than repeated here, so the expectation cannot drift from the + # manifest it is meant to describe. PRIVATE_KEY is deliberately absent: it is set + # only on the first deploy to import the keystore, and must not reach the resume + # run, which reuses the already-imported account. env: ACCOUNT_NAME: anvil-default-7 ACCOUNT_PASSWORD: anvil-default-password @@ -38,7 +41,6 @@ jobs: DOTNS_TLD: dot WHITELIST_OPERATOR: "0x14dC79964da2C08b23698B3D3cc7Ca32193d9955" FACTORY_DEPLOYER: "0xd498F7BC5bB3cBdd0068c3deEbbd814b69C3F164" - PINNED_FACTORY: "0x8533c79E058c5a6489CAFeCA86dc600E029D75f5" CANONICAL: deployments/paseo-assethub/420420417.json MANIFEST: deployments/paseo-local/420420420.json @@ -49,6 +51,16 @@ jobs: submodules: recursive fetch-depth: 1 + - name: Resolve the pinned factory address + run: | + FACTORY=$(jq -r '.Create3Factory // empty' "$CANONICAL") + if [[ ! "$FACTORY" =~ ^0x[0-9a-fA-F]{40}$ ]]; then + echo "::error::$CANONICAL has no usable Create3Factory entry." + exit 1 + fi + echo "PINNED_FACTORY=$FACTORY" >> "$GITHUB_ENV" + echo "Expecting the factory at $FACTORY, per $CANONICAL." + - name: Setup Bun uses: oven-sh/setup-bun@v2 with: @@ -267,10 +279,10 @@ jobs: # This CI deploy reproduces the published address set. The canonical factory # was deployed above, and every DotNS address is a pure function of that # factory plus a fixed salt, so the freshly deployed manifest must equal the - # committed manifest that seeds DEPLOYMENTS.md. Assert that, print the - # expected-vs-actual table, then rerun the pipeline to prove the deploy is - # resumable: a rerun adopts every contract and still lands on the same set. - - name: Verify addresses match DEPLOYMENTS.md + # committed manifest. Assert that, print the expected-vs-actual table, then + # rerun the pipeline to prove the deploy is resumable: a rerun adopts every + # contract and still lands on the same set. + - name: Verify addresses match the committed manifest id: verify_addresses if: steps.verify.outcome == 'success' run: | @@ -279,7 +291,7 @@ jobs: # Markdown expected-vs-actual table: expected = committed manifest, # actual = this CI deployment. Any row whose addresses differ is MOVED. emit_table() { - echo "### Deployed addresses vs DEPLOYMENTS.md" + echo "### Deployed addresses vs the committed manifest" echo "" echo "Expected is the committed manifest; actual is this CI deployment of the same pipeline." echo "" @@ -312,15 +324,15 @@ jobs: if ! matches_canonical; then { - echo "### Deployed addresses do not match DEPLOYMENTS.md" + echo "### Deployed addresses do not match the committed manifest" echo "" echo "The pipeline no longer reproduces the published set. Rows marked MOVED" - echo "below differ from the committed manifest; regenerate DEPLOYMENTS.md and" - echo "the manifest, or restore the salt or label that moved." + echo "below differ from the committed manifest; update it, or restore the" + echo "salt or label that moved." echo "" cat table.md } > deploy-error.md - fail "Failed - addresses differ from DEPLOYMENTS.md" + fail "Failed - addresses differ from the committed manifest" fi # Resume: rerun the same pinned pipeline. It must adopt every contract @@ -344,7 +356,7 @@ jobs: { echo "### Resume moved addresses" echo "" - echo "The rerun no longer matches DEPLOYMENTS.md, so a resumed deploy would" + echo "The rerun no longer matches the committed manifest, so a resumed deploy would" echo "relocate contracts." echo "" emit_table @@ -355,7 +367,7 @@ jobs: # Surface the table under the passing row too, so the address set is on # record every run, not only on failure. cp table.md deploy-error.md - echo "result=Reproduces DEPLOYMENTS.md; resume verified" >> "$GITHUB_OUTPUT" + echo "result=Reproduces the committed manifest; resume verified" >> "$GITHUB_OUTPUT" echo "has_details=true" >> "$GITHUB_OUTPUT" - name: Set final result diff --git a/.github/workflows/publish-prerelease.yml b/.github/workflows/publish-prerelease.yml index 9a4ee361..410ab90c 100644 --- a/.github/workflows/publish-prerelease.yml +++ b/.github/workflows/publish-prerelease.yml @@ -133,13 +133,23 @@ jobs: ls -1 release/abis | sort > release/expected-assets.txt echo "Extracted $(wc -l < release/expected-assets.txt) ABIs" + # No addresses: a pre-release is cut to be deployed, so the recorded addresses still + # belong to the previous deployment of different code. Publishing them under this tag + # would break the promise that a release's addresses and ABIs come from the same release. + - name: Generate release manifest + run: | + bun scripts/js/release-metadata.mjs build --tag "$RELEASE_TAG" --out release \ + --addresses false + - name: Package pre-release artifacts run: | TAG="$RELEASE_TAG" cd release - zip -r "../dotns-abis-${TAG}.zip" abis/ + zip -r "../dotns-abis-${TAG}.zip" abis/ release-manifest.json - name: Generate release body + env: + GH_TOKEN: ${{ github.token }} run: | TAG="$RELEASE_TAG" ASSET_BASE="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/download/$TAG" @@ -179,6 +189,16 @@ jobs: # substitution, which also leaves ${TAG} unexpanded. Substitute it here. sed -i "s|\${ASSET_BASE}|$ASSET_BASE|g; s|\${TAG}|$TAG|g" release-body.md + { + echo "" + echo "### Deployments" + echo "" + echo "No addresses are published with a pre-release: this code is not deployed yet." + echo "Deploy this tag, then cut the release, which carries the addresses it produced." + echo "" + echo "- **Release contents:** [release-manifest.json]($ASSET_BASE/release-manifest.json)" + } >> release-body.md + - name: Create draft pre-release with artifacts uses: softprops/action-gh-release@v2 with: @@ -189,6 +209,7 @@ jobs: files: | dotns-abis-*.zip release/abis/*.json + release/release-manifest.json body_path: release-body.md draft: true prerelease: true @@ -202,8 +223,10 @@ jobs: TAG="$RELEASE_TAG" gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets \ --jq '.assets[].name' | sort > "$RUNNER_TEMP/actual-assets.txt" - { cat release/expected-assets.txt; echo "dotns-abis-${TAG}.zip"; } \ - | sort > "$RUNNER_TEMP/wanted-assets.txt" + { cat release/expected-assets.txt + echo "dotns-abis-${TAG}.zip" + echo "release-manifest.json" + } | sort > "$RUNNER_TEMP/wanted-assets.txt" if ! diff -u "$RUNNER_TEMP/wanted-assets.txt" "$RUNNER_TEMP/actual-assets.txt"; then echo "::error::Draft pre-release $TAG does not match the expected asset set; delete the draft and re-run." exit 1 diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index fcf830ba..4d9e897a 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -130,13 +130,21 @@ jobs: ls -1 release/abis | sort > release/expected-assets.txt echo "Extracted $(wc -l < release/expected-assets.txt) ABIs" + # Addresses come from the committed deployment manifests, the same files the + # deployment pipeline in dotns-releases measures a live deploy against. A consumer + # that only has the release needs them to reach any contract at all. + - name: Generate deployments and release manifest + run: bun scripts/js/release-metadata.mjs build --tag "$RELEASE_TAG" --out release + - name: Package release artifacts run: | TAG="$RELEASE_TAG" cd release - zip -r "../dotns-abis-${TAG}.zip" abis/ + zip -r "../dotns-abis-${TAG}.zip" abis/ deployments.json release-manifest.json - name: Generate release body + env: + GH_TOKEN: ${{ github.token }} run: | TAG="$RELEASE_TAG" ASSET_BASE="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/download/$TAG" @@ -174,6 +182,34 @@ jobs: # substitution, which also leaves ${TAG} unexpanded. Substitute it here. sed -i "s|\${ASSET_BASE}|$ASSET_BASE|g; s|\${TAG}|$TAG|g" release-body.md + # Networks and chain ids only. The addresses stay in the asset, so the body + # cannot become another copy of them. Chain ids are not unique across networks, + # which is why the table is keyed by network. + { + echo "" + echo "### Deployments" + echo "" + echo "| Network | Chain id |" + echo "|---------|---------:|" + jq -r '.networks | to_entries[] | "| \(.key) | \(.value.chainId) |"' release/deployments.json + echo "" + echo "- **Addresses:** [deployments.json]($ASSET_BASE/deployments.json)" + echo "- **Release contents:** [release-manifest.json]($ASSET_BASE/release-manifest.json)" + } >> release-body.md + + # `gh release view` with no tag resolves through /releases/latest, which skips + # drafts, so this is the previously published release and never the draft this run + # is about to create. + PREV=$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName --jq .tagName 2>/dev/null || true) + if [ -n "$PREV" ] && gh release download "$PREV" --repo "$GITHUB_REPOSITORY" \ + --pattern deployments.json --dir prev >/dev/null 2>&1; then + bun scripts/js/release-metadata.mjs changelog --current release/deployments.json \ + --previous prev/deployments.json --previous-tag "$PREV" >> release-body.md + else + bun scripts/js/release-metadata.mjs changelog \ + --current release/deployments.json >> release-body.md + fi + - name: Create draft release with artifacts uses: softprops/action-gh-release@v2 with: @@ -184,6 +220,8 @@ jobs: files: | dotns-abis-*.zip release/abis/*.json + release/deployments.json + release/release-manifest.json body_path: release-body.md draft: true prerelease: false @@ -197,8 +235,11 @@ jobs: TAG="$RELEASE_TAG" gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets \ --jq '.assets[].name' | sort > "$RUNNER_TEMP/actual-assets.txt" - { cat release/expected-assets.txt; echo "dotns-abis-${TAG}.zip"; } \ - | sort > "$RUNNER_TEMP/wanted-assets.txt" + { cat release/expected-assets.txt + echo "dotns-abis-${TAG}.zip" + echo "deployments.json" + echo "release-manifest.json" + } | sort > "$RUNNER_TEMP/wanted-assets.txt" if ! diff -u "$RUNNER_TEMP/wanted-assets.txt" "$RUNNER_TEMP/actual-assets.txt"; then echo "::error::Draft release $TAG does not match the expected asset set; delete the draft and re-run." exit 1 diff --git a/.github/workflows/release-metadata.yml b/.github/workflows/release-metadata.yml new file mode 100644 index 00000000..d0ff5577 --- /dev/null +++ b/.github/workflows/release-metadata.yml @@ -0,0 +1,31 @@ +name: Release Metadata + +# The release generates deployments.json and release-manifest.json from the committed +# manifests and the contract list. Without this, a duplicate address, a missing +# Create3Factory or a malformed manifest would only surface when a release is cut. No +# contract build is needed: manifest checks read JSON, and listed contract names are +# resolved against sources. +on: + pull_request: + paths: + - "deployments/**" + - ".github/abi-contracts.txt" + - "scripts/js/release-metadata.mjs" + - ".github/workflows/release-metadata.yml" + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.2.6" + no-cache: true + + - name: Validate deployment manifests and the contract list + run: bun scripts/js/release-metadata.mjs validate diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 47bfc8e4..2e722729 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to DotNS -These guidelines apply to the DotNS repository ("dotns"). Contributions are welcome via issues, pull requests, reviews, and testing feedback. Protocol behaviour and the deployments table live in [README.md](./README.md); this file is the contributor mechanics. +These guidelines apply to the DotNS repository ("dotns"). Contributions are welcome via issues, pull requests, reviews, and testing feedback. Protocol behaviour is documented in [README.md](./README.md) and network addresses in `deployments//.json`; this file is the contributor mechanics. ## Types of contributing @@ -269,8 +269,7 @@ forge test --no-match-path 'test/fork/**' 2. Delete the paired fork test under `test/fork/`. 3. Delete every `*Old.sol` and `I*Old.sol` referenced only by the upgrade script. 4. Delete temporary forge artefacts: `broadcast/