From 39ec3d4ad10c509c79f0f273764eaafc5deb5526 Mon Sep 17 00:00:00 2001 From: giuseppere Date: Thu, 1 Oct 2026 13:41:55 +0200 Subject: [PATCH 1/2] ci(release): publish only from tag pushes; fix the release body's Usage and Deployments --- .github/workflows/publish-prerelease.yml | 41 +++++---------------- .github/workflows/publish-release.yml | 45 +++++++----------------- README.md | 6 ++-- RELEASE_ARTIFACTS.md | 4 +-- 4 files changed, 27 insertions(+), 69 deletions(-) diff --git a/.github/workflows/publish-prerelease.yml b/.github/workflows/publish-prerelease.yml index 35d759ba..364d48a8 100644 --- a/.github/workflows/publish-prerelease.yml +++ b/.github/workflows/publish-prerelease.yml @@ -7,20 +7,13 @@ on: push: tags: - "v[0-9]+.[0-9]+.[0-9]+-*" - workflow_dispatch: - inputs: - version: - description: "Pre-release version, e.g. v0.5.5-rc1. The tag is created from the selected branch." - required: true - type: string permissions: contents: write -# Two runs for the same version would race to attach assets to the same draft. On a -# dispatch `github.ref_name` is the branch, so key on the requested version instead. +# Two runs for the same version would race to attach assets to the same draft. concurrency: - group: ${{ github.workflow }}-${{ inputs.version || github.ref_name }} + group: ${{ github.workflow }}-${{ github.ref_name }} cancel-in-progress: false jobs: @@ -34,19 +27,12 @@ jobs: env: DOTNS_TLDS: testnet paseo steps: - # On workflow_dispatch the tag does not exist yet; the release step creates it - # from the branch this run was started on. Read through an env var rather than - # interpolating the input into the script. + # The run is started by pushing the tag, so the tag already exists and is the release's + # identity. Only the tag push triggers this workflow: the `release tags` ruleset keeps + # the workflow's token from creating a `v*` tag, so a run without one could not publish. - name: Resolve release tag - env: - GH_TOKEN: ${{ github.token }} - INPUT_VERSION: ${{ inputs.version }} run: | - if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then - TAG="$INPUT_VERSION" - else - TAG="$GITHUB_REF_NAME" - fi + TAG="$GITHUB_REF_NAME" # The suffix is restricted to characters GitHub keeps verbatim in an asset # name; a space, for instance, is rewritten to a dot and would fail the # asset check after a full build. Segmented like run.sh's semver check @@ -56,16 +42,8 @@ jobs: echo "::error::Pre-release version must look like v1.2.3-rc1, got '$TAG'." exit 1 fi - # A release created for an existing tag is cut at that tag's commit: GitHub - # ignores target_commitish when the tag is already there. The ABIs would come - # from this branch while the release pointed somewhere else. - if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ] \ - && gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$TAG" >/dev/null 2>&1; then - echo "::error::Tag $TAG already exists; use a different version." - exit 1 - fi echo "RELEASE_TAG=$TAG" >> "$GITHUB_ENV" - echo "Releasing $TAG from $GITHUB_REF_NAME." + echo "Releasing $TAG." - name: Reject a pre-published release env: @@ -238,7 +216,7 @@ jobs: ### Usage ```ts - import StoreAbi from "./abis/Store.json"; + import LabelStoreAbi from "./abis/LabelStore.json"; import RegistrarAbi from "./abis/DotnsRegistrar.json"; ``` ENDOFBODY @@ -291,8 +269,7 @@ jobs: - name: Create draft pre-release with artifacts uses: softprops/action-gh-release@v2 with: - # Explicit because on workflow_dispatch there is no tag to infer; the action - # creates it at this run's commit. + # The tag that started this run; `target_commitish` is that tag's commit. tag_name: ${{ env.RELEASE_TAG }} target_commitish: ${{ github.sha }} files: | diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 1fb0af90..6964cd9a 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -7,20 +7,13 @@ on: push: tags: - "v[0-9]+.[0-9]+.[0-9]+" - workflow_dispatch: - inputs: - version: - description: "Version to release, e.g. v0.5.5. The tag is created from the selected branch." - required: true - type: string permissions: contents: write -# Two runs for the same version would race to attach assets to the same draft. On a -# dispatch `github.ref_name` is the branch, so key on the requested version instead. +# Two runs for the same version would race to attach assets to the same draft. concurrency: - group: ${{ github.workflow }}-${{ inputs.version || github.ref_name }} + group: ${{ github.workflow }}-${{ github.ref_name }} cancel-in-progress: false jobs: @@ -34,33 +27,18 @@ jobs: env: DOTNS_TLDS: testnet paseo steps: - # On workflow_dispatch the tag does not exist yet; the release step creates it - # from the branch this run was started on. Read through an env var rather than - # interpolating the input into the script. + # The run is started by pushing the tag, so the tag already exists and is the release's + # identity. Only the tag push triggers this workflow: the `release tags` ruleset keeps + # the workflow's token from creating a `v*` tag, so a run without one could not publish. - name: Resolve release tag - env: - GH_TOKEN: ${{ github.token }} - INPUT_VERSION: ${{ inputs.version }} run: | - if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then - TAG="$INPUT_VERSION" - else - TAG="$GITHUB_REF_NAME" - fi + TAG="$GITHUB_REF_NAME" if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "::error::Version must look like v1.2.3, got '$TAG'." exit 1 fi - # A release created for an existing tag is cut at that tag's commit: GitHub - # ignores target_commitish when the tag is already there. The ABIs would come - # from this branch while the release pointed somewhere else. - if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ] \ - && gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$TAG" >/dev/null 2>&1; then - echo "::error::Tag $TAG already exists; use a different version." - exit 1 - fi echo "RELEASE_TAG=$TAG" >> "$GITHUB_ENV" - echo "Releasing $TAG from $GITHUB_REF_NAME." + echo "Releasing $TAG." - name: Reject a pre-published release env: @@ -230,7 +208,7 @@ jobs: ### Usage ```ts - import StoreAbi from "./abis/Store.json"; + import LabelStoreAbi from "./abis/LabelStore.json"; import RegistrarAbi from "./abis/DotnsRegistrar.json"; ``` ENDOFBODY @@ -251,6 +229,10 @@ jobs: echo "|---------|---------:|" jq -r '.networks | to_entries[] | "| \(.key) | \(.value.chainId) |"' release/deployments.json echo "" + echo "A network can still run an earlier release until it is upgraded to this one. Its protocol" + echo "registry's \`protocolVersion()\` returns the release it runs, and \`deployments:verify --tag\`" + echo "checks the chain against a release ([Verifying a release]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/blob/$TAG/RELEASE_ARTIFACTS.md#verifying-a-release))." + echo "" echo "- **Addresses:** [deployments.json]($ASSET_BASE/deployments.json)" echo "- **Release contents:** [release-manifest.json]($ASSET_BASE/release-manifest.json)" echo "- **Code identity:** [codehashes.json]($ASSET_BASE/codehashes.json) (stripped-metadata build hashes of this release's contracts; input to upgrade checks)" @@ -307,8 +289,7 @@ jobs: - name: Create draft release with artifacts uses: softprops/action-gh-release@v2 with: - # Explicit because on workflow_dispatch there is no tag to infer; the action - # creates it at this run's commit. + # The tag that started this run; `target_commitish` is that tag's commit. tag_name: ${{ env.RELEASE_TAG }} target_commitish: ${{ github.sha }} files: | diff --git a/README.md b/README.md index 1b30b045..8f73a1f3 100644 --- a/README.md +++ b/README.md @@ -37,13 +37,13 @@ Deployment notes are in [DEPLOYMENTS.md](./DEPLOYMENTS.md). Network addresses ar A release publishes the contract ABIs and the deployed addresses as GitHub release assets, described in [RELEASE_ARTIFACTS.md](./RELEASE_ARTIFACTS.md). It does not deploy anything; deploying contracts to a network is a separate process, described in [DEPLOYMENTS.md](./DEPLOYMENTS.md). -Run **Publish Release Package** from the Actions tab, pick the branch to release from, and enter the version (`v0.5.5`). The workflow does the rest: it builds, tests, extracts the ABIs listed in [.github/abi-contracts.txt](./.github/abi-contracts.txt), generates the address and manifest files, creates the release as a draft with every asset attached, verifies the set against what the build produced, and only then publishes. Pushing a matching tag runs the same workflow, so `git tag v0.5.5 && git push origin v0.5.5` remains equivalent. +A release starts from its tag. A member of the dotns team tags the commit to release and pushes the tag: `git tag v0.5.5 && git push origin v0.5.5`. Only the team can create `v*` tags, which the `release tags` ruleset enforces, so the workflow cannot create one itself. The push starts **Publish Release Package**, which waits for a reviewer's approval on the `releases` environment and then does the rest: it builds, tests, extracts the ABIs listed in [.github/abi-contracts.txt](./.github/abi-contracts.txt), generates the address and manifest files, creates the release as a draft with every asset attached, verifies the set against what the build produced, and only then publishes. -Pre-releases use **Publish Beta Package** with a suffixed version, `v0.5.5-rc1`. The version is the release identity; the `version` field in `package.json` is unrelated and nothing reads it. +Pre-releases work the same way with a suffixed tag, `v0.5.5-rc1`, whose push starts **Publish Beta Package**. The tag is the release identity; the `version` field in `package.json` is unrelated and nothing reads it. Do not create releases through the GitHub UI's release form, or with `gh release create`. Both publish immediately, and because this repository has immutable releases enabled, a published release can no longer accept assets: only its title and notes stay editable. A release made that way carries no ABIs at all. The workflow rejects an already-published version before building, so the mistake fails in seconds rather than silently shipping an empty release. -If a run fails partway, re-run it from the Actions tab; the draft is updated rather than duplicated. One case needs a manual step: the upload replaces an asset of the same name but never removes others, so if the contract list changed since the failed run, the draft still carries the assets it no longer expects and the verification step will keep refusing to publish. Delete the draft and re-run. If the version has already been published, use a different one, since its assets cannot be changed. +If a run fails partway, re-run it from its page in the Actions tab; the draft is updated in place. One case needs a manual step: the upload replaces an asset of the same name but never removes others, so if the contract list changed since the failed run, the draft still carries the assets it no longer expects and the verification step will keep refusing to publish. Delete the draft and re-run. If the version has already been published, use a different one, since its assets cannot be changed. ## Economics diff --git a/RELEASE_ARTIFACTS.md b/RELEASE_ARTIFACTS.md index dd544d13..b80046fe 100644 --- a/RELEASE_ARTIFACTS.md +++ b/RELEASE_ARTIFACTS.md @@ -129,8 +129,8 @@ key that owns every contract in it. The environment holds: 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. +- Deployment refs: `v[0-9]*` tags. Both workflows run only on a tag push, so a run from any + other ref 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. From b1c56ab9511b7e7f0d71a873fc9116b8ec2c02da Mon Sep 17 00:00:00 2001 From: giuseppere Date: Fri, 2 Oct 2026 11:37:53 +0200 Subject: [PATCH 2/2] ci(release): release only master commits, address review --- .github/workflows/publish-prerelease.yml | 33 ++++++++++++++++++---- .github/workflows/publish-release.yml | 36 ++++++++++++++++++++---- README.md | 6 ++-- scripts/js/release-metadata.mjs | 2 ++ 4 files changed, 63 insertions(+), 14 deletions(-) diff --git a/.github/workflows/publish-prerelease.yml b/.github/workflows/publish-prerelease.yml index 364d48a8..51d7caf8 100644 --- a/.github/workflows/publish-prerelease.yml +++ b/.github/workflows/publish-prerelease.yml @@ -1,8 +1,9 @@ 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. +# Creating a `v*` tag is restricted to the dotns team by the `release tags` ruleset, the +# `guard` job refuses a tag whose commit is not on master, and the gated job pauses on the +# `releases` environment for a reviewer. A release therefore ships reviewed master code and +# takes two distinct human actions: cutting the tag, and approving the run it starts. on: push: tags: @@ -17,7 +18,30 @@ concurrency: cancel-in-progress: false jobs: + # Runs first, with no environment and read-only access: a tag that does not point at a + # commit on master stops here, before a reviewer is asked to approve and before any + # secret is read. master only takes pull requests, which need an approving review, so + # this keeps every release on reviewed code. + guard: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Require the tagged commit to be on master + run: | + if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/master; then + echo "::error::$GITHUB_REF_NAME points at $GITHUB_SHA, which is not on master. Tag a commit on master." + exit 1 + fi + echo "$GITHUB_REF_NAME points at $GITHUB_SHA, which is on master." + beta-release: + needs: guard 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. @@ -269,9 +293,8 @@ jobs: - name: Create draft pre-release with artifacts uses: softprops/action-gh-release@v2 with: - # The tag that started this run; `target_commitish` is that tag's commit. + # The tag that started this run. tag_name: ${{ env.RELEASE_TAG }} - target_commitish: ${{ github.sha }} files: | dotns-abis-*.zip release/abis/*.json diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 6964cd9a..b55a8f8d 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -1,8 +1,9 @@ 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. +# Creating a `v*` tag is restricted to the dotns team by the `release tags` ruleset, the +# `guard` job refuses a tag whose commit is not on master, and the gated job pauses on the +# `releases` environment for a reviewer. A release therefore ships reviewed master code and +# takes two distinct human actions: cutting the tag, and approving the run it starts. on: push: tags: @@ -17,7 +18,30 @@ concurrency: cancel-in-progress: false jobs: + # Runs first, with no environment and read-only access: a tag that does not point at a + # commit on master stops here, before a reviewer is asked to approve and before any + # secret is read. master only takes pull requests, which need an approving review, so + # this keeps every release on reviewed code. + guard: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Require the tagged commit to be on master + run: | + if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/master; then + echo "::error::$GITHUB_REF_NAME points at $GITHUB_SHA, which is not on master. Tag a commit on master." + exit 1 + fi + echo "$GITHUB_REF_NAME points at $GITHUB_SHA, which is on master." + release: + needs: guard 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. @@ -230,7 +254,8 @@ jobs: jq -r '.networks | to_entries[] | "| \(.key) | \(.value.chainId) |"' release/deployments.json echo "" echo "A network can still run an earlier release until it is upgraded to this one. Its protocol" - echo "registry's \`protocolVersion()\` returns the release it runs, and \`deployments:verify --tag\`" + echo "registry's \`protocolVersion()\` returns the release it runs as bare semver (\`1.0.0\`, no \`v\`)," + echo "or an empty string when the network has never declared one. \`deployments:verify --tag\`" echo "checks the chain against a release ([Verifying a release]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/blob/$TAG/RELEASE_ARTIFACTS.md#verifying-a-release))." echo "" echo "- **Addresses:** [deployments.json]($ASSET_BASE/deployments.json)" @@ -289,9 +314,8 @@ jobs: - name: Create draft release with artifacts uses: softprops/action-gh-release@v2 with: - # The tag that started this run; `target_commitish` is that tag's commit. + # The tag that started this run. tag_name: ${{ env.RELEASE_TAG }} - target_commitish: ${{ github.sha }} files: | dotns-abis-*.zip release/abis/*.json diff --git a/README.md b/README.md index 8f73a1f3..9a0d5933 100644 --- a/README.md +++ b/README.md @@ -37,13 +37,13 @@ Deployment notes are in [DEPLOYMENTS.md](./DEPLOYMENTS.md). Network addresses ar A release publishes the contract ABIs and the deployed addresses as GitHub release assets, described in [RELEASE_ARTIFACTS.md](./RELEASE_ARTIFACTS.md). It does not deploy anything; deploying contracts to a network is a separate process, described in [DEPLOYMENTS.md](./DEPLOYMENTS.md). -A release starts from its tag. A member of the dotns team tags the commit to release and pushes the tag: `git tag v0.5.5 && git push origin v0.5.5`. Only the team can create `v*` tags, which the `release tags` ruleset enforces, so the workflow cannot create one itself. The push starts **Publish Release Package**, which waits for a reviewer's approval on the `releases` environment and then does the rest: it builds, tests, extracts the ABIs listed in [.github/abi-contracts.txt](./.github/abi-contracts.txt), generates the address and manifest files, creates the release as a draft with every asset attached, verifies the set against what the build produced, and only then publishes. +A release starts from its tag. A member of the dotns team tags the commit to release and pushes the tag: `git tag v0.5.5 && git push origin v0.5.5`. Only the team can create `v*` tags, which the `release tags` ruleset enforces, so the workflow cannot create one itself. The tagged commit must be on `master`, which only takes pull requests that need an approving review; the workflow refuses any other tag before asking for approval. The push starts **Publish Release Package**, which waits for a reviewer's approval on the `releases` environment and then does the rest: it builds, tests, extracts the ABIs listed in [.github/abi-contracts.txt](./.github/abi-contracts.txt), generates the address and manifest files, creates the release as a draft with every asset attached, verifies the set against what the build produced, and only then publishes. Pre-releases work the same way with a suffixed tag, `v0.5.5-rc1`, whose push starts **Publish Beta Package**. The tag is the release identity; the `version` field in `package.json` is unrelated and nothing reads it. -Do not create releases through the GitHub UI's release form, or with `gh release create`. Both publish immediately, and because this repository has immutable releases enabled, a published release can no longer accept assets: only its title and notes stay editable. A release made that way carries no ABIs at all. The workflow rejects an already-published version before building, so the mistake fails in seconds rather than silently shipping an empty release. +Do not create releases through the GitHub UI's release form, or with `gh release create`. Both publish immediately, and because this repository has immutable releases enabled, a published release can no longer accept assets: only its title and notes stay editable. A release made that way carries no ABIs at all. The workflow rejects an already-published version before building, so the mistake fails in seconds and no empty release ships. -If a run fails partway, re-run it from its page in the Actions tab; the draft is updated in place. One case needs a manual step: the upload replaces an asset of the same name but never removes others, so if the contract list changed since the failed run, the draft still carries the assets it no longer expects and the verification step will keep refusing to publish. Delete the draft and re-run. If the version has already been published, use a different one, since its assets cannot be changed. +If a run fails partway, re-run it from its page in the Actions tab; the draft is updated in place. A re-run builds the same commit, because a pushed `v*` tag stays where it is: the `release tags` ruleset blocks deleting or moving one for everyone outside the dotns team, and the team does not move a pushed release tag either. So a re-run only helps when the failure is outside the code (a flaky step, an expired approval); a failure that needs a code fix needs a new version. When you abandon a version that way, delete its draft from the Releases page so it does not linger unpublished. One case needs a manual step: the upload replaces an asset of the same name but never removes others, so if the contract list changed since the failed run, the draft still carries the assets it no longer expects and the verification step will keep refusing to publish. Delete the draft and re-run. If the version has already been published, use a different one, since its assets cannot be changed. ## Economics diff --git a/scripts/js/release-metadata.mjs b/scripts/js/release-metadata.mjs index acaf49fb..c5692c5b 100644 --- a/scripts/js/release-metadata.mjs +++ b/scripts/js/release-metadata.mjs @@ -837,6 +837,8 @@ function verify(args) { ]).replace(/^"|"$/g, ""); if (declaredVersion === tag) { console.log(` ok protocolVersion ${declaredVersion}`); + } else if (declaredVersion === "") { + problems.push(`chain declares no protocol version, expected '${tag}'`); } else { problems.push(`chain declares protocol version '${declaredVersion}', expected '${tag}'`); }