From 641eef9ad409dc44ad9f58956c8eb96e170d3d86 Mon Sep 17 00:00:00 2001 From: giuseppere Date: Mon, 17 Aug 2026 14:51:00 +0200 Subject: [PATCH 1/4] CI: attach release assets before publishing + verify --- .github/abi-contracts.txt | 46 +++++++++++ .github/workflows/publish-prerelease.yml | 98 +++++++++++++----------- .github/workflows/publish-release.yml | 98 +++++++++++++----------- CONTRIBUTING.md | 2 +- 4 files changed, 157 insertions(+), 87 deletions(-) create mode 100644 .github/abi-contracts.txt diff --git a/.github/abi-contracts.txt b/.github/abi-contracts.txt new file mode 100644 index 00000000..e9e20feb --- /dev/null +++ b/.github/abi-contracts.txt @@ -0,0 +1,46 @@ +# Contracts and interfaces whose ABIs ship in the release artifact. +# +# Read by .github/workflows/publish-release.yml and publish-prerelease.yml. The +# release surface is a deliberate decision, so it is listed explicitly here rather +# than globbed from contracts/**, which would make it a side effect of the directory +# layout. Listing it once keeps the two workflows from drifting apart. +# +# One name per line, matching the artifact path out/.sol/.json. Blank +# lines and # comments are ignored. A name with no build artifact fails the release. +# +# Adding a contract? Add it and its interface here, or its ABI never reaches +# consumers. See the protocol-registry section of CONTRIBUTING.md. + +StoreFactory +LabelStore +UserStore +DotnsRegistrar +DotnsReverseResolver +DotnsRegistry +DotnsContentResolver +DotnsResolver +PopRules +DotnsRegistrarController +DotnsProtocolRegistry +DotnsNameEscrow +DotnsPopController +DotnsPopResolver +DotnsRoleManager +RootGatewayDispatcher + +IStoreFactory +ILabelStore +IUserStore +IDotnsRegistrar +IDotnsRegistrarController +IDotnsRegistry +IDotnsReverseResolver +IDotnsContentResolver +IDotnsResolver +IPopRules +IDotnsProtocolRegistry +IDotnsNameEscrow +IDotnsPopController +IDotnsPopResolver +IDotnsController +IDotnsRoleManager diff --git a/.github/workflows/publish-prerelease.yml b/.github/workflows/publish-prerelease.yml index 0bc48429..fc484566 100644 --- a/.github/workflows/publish-prerelease.yml +++ b/.github/workflows/publish-prerelease.yml @@ -8,10 +8,28 @@ on: permissions: contents: write +# Two runs for the same tag would race to attach assets to the same draft. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + jobs: beta-release: runs-on: ubuntu-latest steps: + - name: Reject a pre-published release + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG="${GITHUB_REF_NAME}" + state=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" \ + --json isDraft --jq '.isDraft' 2>/dev/null || echo absent) + if [ "$state" = "false" ]; then + echo "::error::Pre-release $TAG is already published and its assets are locked; use this workflow with a different version tag." + exit 1 + fi + echo "Release $TAG is $state; assets can still be attached." + - uses: actions/checkout@v4 with: submodules: recursive @@ -50,51 +68,21 @@ jobs: run: | mkdir -p release/abis - contracts=( - "StoreFactory" - "LabelStore" - "UserStore" - "DotnsRegistrar" - "DotnsReverseResolver" - "DotnsRegistry" - "DotnsContentResolver" - "DotnsResolver" - "PopRules" - "DotnsRegistrarController" - "DotnsProtocolRegistry" - "DotnsNameEscrow" - "DotnsPopController" - "DotnsPopResolver" - "DotnsRoleManager" - "RootGatewayDispatcher" - "IStoreFactory" - "ILabelStore" - "IUserStore" - "IDotnsRegistrar" - "IDotnsRegistrarController" - "IDotnsRegistry" - "IDotnsReverseResolver" - "IDotnsContentResolver" - "IDotnsResolver" - "IPopRules" - "IDotnsProtocolRegistry" - "IDotnsNameEscrow" - "IDotnsPopController" - "IDotnsPopResolver" - "IDotnsController" - "IDotnsRoleManager" - ) - - for name in "${contracts[@]}"; do + while IFS= read -r name || [ -n "$name" ]; do + case "$name" in '' | '#'*) continue ;; esac abi_file="out/${name}.sol/${name}.json" - if [ -f "$abi_file" ]; then - jq '.abi' "$abi_file" > "release/abis/${name}.json" - echo "Extracted ${name}" - else - echo "Error: ${abi_file} not found" + if [ ! -f "$abi_file" ]; then + echo "::error::${abi_file} not found" exit 1 fi - done + jq '.abi' "$abi_file" > "release/abis/${name}.json" + echo "Extracted ${name}" + done < .github/abi-contracts.txt + + # Record what the build actually produced, so the post-upload check compares + # against it rather than against a second hand-maintained list. + ls -1 release/abis | sort > release/expected-assets.txt + echo "Extracted $(wc -l < release/expected-assets.txt) ABIs" - name: Package pre-release artifacts run: | @@ -138,7 +126,7 @@ jobs: sed -i 's/^ //' release-body.md - - name: Create pre-release with artifacts + - name: Create draft pre-release with artifacts uses: softprops/action-gh-release@v2 with: files: | @@ -148,3 +136,27 @@ jobs: draft: true prerelease: true generate_release_notes: true + + - name: Verify draft assets + env: + GH_TOKEN: ${{ github.token }} + run: | + set -o pipefail + TAG="${GITHUB_REF_NAME}" + 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" + if ! diff -u "$RUNNER_TEMP/wanted-assets.txt" "$RUNNER_TEMP/actual-assets.txt"; then + echo "::error::Draft pre-release $TAG does not carry the expected asset set; leaving it unpublished." + exit 1 + fi + echo "Verified $(wc -l < "$RUNNER_TEMP/wanted-assets.txt") assets on draft $TAG." + + - name: Publish pre-release + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG="${GITHUB_REF_NAME}" + gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" --draft=false + echo "Published $TAG with its complete asset set." diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index fc279d58..12802f12 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -8,10 +8,28 @@ on: permissions: contents: write +# Two runs for the same tag would race to attach assets to the same draft. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + jobs: release: runs-on: ubuntu-latest steps: + - name: Reject a pre-published release + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG="${GITHUB_REF_NAME}" + state=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" \ + --json isDraft --jq '.isDraft' 2>/dev/null || echo absent) + if [ "$state" = "false" ]; then + echo "::error::Release $TAG is already published and its assets are locked; use this workflow with a different version tag." + exit 1 + fi + echo "Release $TAG is $state; assets can still be attached." + - uses: actions/checkout@v4 with: submodules: recursive @@ -50,51 +68,21 @@ jobs: run: | mkdir -p release/abis - contracts=( - "StoreFactory" - "LabelStore" - "UserStore" - "DotnsRegistrar" - "DotnsReverseResolver" - "DotnsRegistry" - "DotnsContentResolver" - "DotnsResolver" - "PopRules" - "DotnsRegistrarController" - "DotnsProtocolRegistry" - "DotnsNameEscrow" - "DotnsPopController" - "DotnsPopResolver" - "DotnsRoleManager" - "RootGatewayDispatcher" - "IStoreFactory" - "ILabelStore" - "IUserStore" - "IDotnsRegistrar" - "IDotnsRegistrarController" - "IDotnsRegistry" - "IDotnsReverseResolver" - "IDotnsContentResolver" - "IDotnsResolver" - "IPopRules" - "IDotnsProtocolRegistry" - "IDotnsNameEscrow" - "IDotnsPopController" - "IDotnsPopResolver" - "IDotnsController" - "IDotnsRoleManager" - ) - - for name in "${contracts[@]}"; do + while IFS= read -r name || [ -n "$name" ]; do + case "$name" in '' | '#'*) continue ;; esac abi_file="out/${name}.sol/${name}.json" - if [ -f "$abi_file" ]; then - jq '.abi' "$abi_file" > "release/abis/${name}.json" - echo "Extracted ${name}" - else - echo "Error: ${abi_file} not found" + if [ ! -f "$abi_file" ]; then + echo "::error::${abi_file} not found" exit 1 fi - done + jq '.abi' "$abi_file" > "release/abis/${name}.json" + echo "Extracted ${name}" + done < .github/abi-contracts.txt + + # Record what the build actually produced, so the post-upload check compares + # against it rather than against a second hand-maintained list. + ls -1 release/abis | sort > release/expected-assets.txt + echo "Extracted $(wc -l < release/expected-assets.txt) ABIs" - name: Package release artifacts run: | @@ -136,7 +124,7 @@ jobs: sed -i 's/^ //' release-body.md - - name: Create release with artifacts + - name: Create draft release with artifacts uses: softprops/action-gh-release@v2 with: files: | @@ -146,3 +134,27 @@ jobs: draft: true prerelease: false generate_release_notes: true + + - name: Verify draft assets + env: + GH_TOKEN: ${{ github.token }} + run: | + set -o pipefail + TAG="${GITHUB_REF_NAME}" + 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" + if ! diff -u "$RUNNER_TEMP/wanted-assets.txt" "$RUNNER_TEMP/actual-assets.txt"; then + echo "::error::Draft release $TAG does not carry the expected asset set; leaving it unpublished." + exit 1 + fi + echo "Verified $(wc -l < "$RUNNER_TEMP/wanted-assets.txt") assets on draft $TAG." + + - name: Publish release + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG="${GITHUB_REF_NAME}" + gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" --draft=false + echo "Published $TAG with its complete asset set." diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bc363fef..47bfc8e4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -127,7 +127,7 @@ Example query paths. Each row starts from a small set of known contracts; every Any new contract address that other contracts need to read must be looked up through `DotnsProtocolRegistry` at the point of use. Do not hardcode it in a constructor, store it in an `immutable`, or expose a one-off `setX(address)` setter. The protocol registry is the only address a contract may hold directly; everything else is fetched on demand so rotation is a single `protocolRegistry.set(KEY, newAddress)` call with no upgrade. -If you are adding a new contract category, add a `bytes32` key for it in `DotnsConstants.sol` and wire it up in `WireDeployments.s.sol`. Read it the same way every existing contract does. +If you are adding a new contract category, add a `bytes32` key for it in `DotnsConstants.sol`, wire it up in `WireDeployments.s.sol`, and list the contract and its interface in `.github/abi-contracts.txt` so their ABIs ship in the release artifact. Read it the same way every existing contract does. Bad — the registrar address is frozen at construction, so rotating it needs an upgrade: From d585741cee3a15a3d323c16535c5078e0265d668 Mon Sep 17 00:00:00 2001 From: giuseppere Date: Mon, 17 Aug 2026 15:23:21 +0200 Subject: [PATCH 2/4] move to GitHub action + fix README --- .github/workflows/publish-prerelease.yml | 44 ++++++++++++++++++++---- .github/workflows/publish-release.yml | 44 ++++++++++++++++++++---- README.md | 12 +++++++ 3 files changed, 86 insertions(+), 14 deletions(-) diff --git a/.github/workflows/publish-prerelease.yml b/.github/workflows/publish-prerelease.yml index fc484566..bb0069db 100644 --- a/.github/workflows/publish-prerelease.yml +++ b/.github/workflows/publish-prerelease.yml @@ -4,24 +4,50 @@ 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 tag would race to attach assets to the same draft. +# Two runs for the same version would race to attach assets to the same draft. On a +# dispatch `github.ref` is the branch, so key on the requested version instead. concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ inputs.version || github.ref }} cancel-in-progress: false jobs: beta-release: runs-on: ubuntu-latest 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. + - name: Resolve release tag + env: + INPUT_VERSION: ${{ inputs.version }} + run: | + if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then + TAG="$INPUT_VERSION" + else + TAG="$GITHUB_REF_NAME" + fi + if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-.+$ ]]; then + echo "::error::Pre-release version must look like v1.2.3-rc1, got '$TAG'." + exit 1 + fi + echo "RELEASE_TAG=$TAG" >> "$GITHUB_ENV" + echo "Releasing $TAG from $GITHUB_REF_NAME." + - name: Reject a pre-published release env: GH_TOKEN: ${{ github.token }} run: | - TAG="${GITHUB_REF_NAME}" + TAG="$RELEASE_TAG" state=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" \ --json isDraft --jq '.isDraft' 2>/dev/null || echo absent) if [ "$state" = "false" ]; then @@ -86,13 +112,13 @@ jobs: - name: Package pre-release artifacts run: | - TAG="${GITHUB_REF_NAME}" + TAG="$RELEASE_TAG" cd release zip -r "../dotns-abis-${TAG}.zip" abis/ - name: Generate release body run: | - TAG="${GITHUB_REF_NAME}" + TAG="$RELEASE_TAG" cat > release-body.md << 'ENDOFBODY' ## DotNS ABI Package (Pre-release) @@ -129,6 +155,10 @@ 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. + tag_name: ${{ env.RELEASE_TAG }} + target_commitish: ${{ github.sha }} files: | dotns-abis-*.zip release/abis/*.json @@ -142,7 +172,7 @@ jobs: GH_TOKEN: ${{ github.token }} run: | set -o pipefail - TAG="${GITHUB_REF_NAME}" + 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"; } \ @@ -157,6 +187,6 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - TAG="${GITHUB_REF_NAME}" + TAG="$RELEASE_TAG" gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" --draft=false echo "Published $TAG with its complete asset set." diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 12802f12..dcfc0f03 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -4,24 +4,50 @@ 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 tag would race to attach assets to the same draft. +# Two runs for the same version would race to attach assets to the same draft. On a +# dispatch `github.ref` is the branch, so key on the requested version instead. concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ inputs.version || github.ref }} cancel-in-progress: false jobs: release: runs-on: ubuntu-latest 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. + - name: Resolve release tag + env: + INPUT_VERSION: ${{ inputs.version }} + run: | + if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then + TAG="$INPUT_VERSION" + else + TAG="$GITHUB_REF_NAME" + fi + if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Version must look like v1.2.3, got '$TAG'." + exit 1 + fi + echo "RELEASE_TAG=$TAG" >> "$GITHUB_ENV" + echo "Releasing $TAG from $GITHUB_REF_NAME." + - name: Reject a pre-published release env: GH_TOKEN: ${{ github.token }} run: | - TAG="${GITHUB_REF_NAME}" + TAG="$RELEASE_TAG" state=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" \ --json isDraft --jq '.isDraft' 2>/dev/null || echo absent) if [ "$state" = "false" ]; then @@ -86,13 +112,13 @@ jobs: - name: Package release artifacts run: | - TAG="${GITHUB_REF_NAME}" + TAG="$RELEASE_TAG" cd release zip -r "../dotns-abis-${TAG}.zip" abis/ - name: Generate release body run: | - TAG="${GITHUB_REF_NAME}" + TAG="$RELEASE_TAG" cat > release-body.md << 'ENDOFBODY' ## DotNS ABI Package @@ -127,6 +153,10 @@ 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. + tag_name: ${{ env.RELEASE_TAG }} + target_commitish: ${{ github.sha }} files: | dotns-abis-*.zip release/abis/*.json @@ -140,7 +170,7 @@ jobs: GH_TOKEN: ${{ github.token }} run: | set -o pipefail - TAG="${GITHUB_REF_NAME}" + 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"; } \ @@ -155,6 +185,6 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - TAG="${GITHUB_REF_NAME}" + TAG="$RELEASE_TAG" gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" --draft=false echo "Published $TAG with its complete asset set." diff --git a/README.md b/README.md index b657d632..5ac8c3a6 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,18 @@ DotNS is a naming system for Polkadot. An account can register a .dot name, rece Current network addresses and deployment notes are listed in [DEPLOYMENTS.md](./DEPLOYMENTS.md). +### Cutting a release + +A release publishes the contract ABIs as GitHub release assets. 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), 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. + +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. + +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. If the version has already been published, use a different one, since its assets cannot be changed. + ## Economics dotNS uses a single tunable constant, written **D** throughout the protocol. D is the starting price used by PopRules and equals ten DOT at launch; governance can adjust it under the same gate as the upgrade authority. D is the only money quantity the protocol charges; everything else is a composition of D with zero. From ca36d0cf03516577c4cc00b9df683aa8be0d6033 Mon Sep 17 00:00:00 2001 From: giuseppere Date: Tue, 18 Aug 2026 09:54:22 +0200 Subject: [PATCH 3/4] cr fixes --- .gitattributes | 5 +++ .github/workflows/publish-prerelease.yml | 48 ++++++++++++++++++------ .github/workflows/publish-release.yml | 43 ++++++++++++++++----- README.md | 2 +- scripts/shell/pre-commit.sh | 12 ++++++ 5 files changed, 88 insertions(+), 22 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..49a38517 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# The publish workflows read each line of this list into an artifact path, so a CRLF +# checkout would look for out/Name\r.sol/Name\r.json and abort the release. Pin it to +# LF regardless of the platform's autocrlf setting. The pre-commit hook rejects a CR +# on the way in; this covers the way out. +.github/abi-contracts.txt text eol=lf diff --git a/.github/workflows/publish-prerelease.yml b/.github/workflows/publish-prerelease.yml index bb0069db..e76b357d 100644 --- a/.github/workflows/publish-prerelease.yml +++ b/.github/workflows/publish-prerelease.yml @@ -15,9 +15,9 @@ permissions: contents: write # Two runs for the same version would race to attach assets to the same draft. On a -# dispatch `github.ref` is the branch, so key on the requested version instead. +# dispatch `github.ref_name` is the branch, so key on the requested version instead. concurrency: - group: ${{ github.workflow }}-${{ inputs.version || github.ref }} + group: ${{ github.workflow }}-${{ inputs.version || github.ref_name }} cancel-in-progress: false jobs: @@ -29,6 +29,7 @@ jobs: # interpolating the input into the script. - name: Resolve release tag env: + GH_TOKEN: ${{ github.token }} INPUT_VERSION: ${{ inputs.version }} run: | if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then @@ -36,10 +37,21 @@ jobs: else TAG="$GITHUB_REF_NAME" fi - if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-.+$ ]]; then + # 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. + if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-[0-9A-Za-z.]+$ ]]; then 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." @@ -48,13 +60,20 @@ jobs: GH_TOKEN: ${{ github.token }} run: | TAG="$RELEASE_TAG" - state=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" \ - --json isDraft --jq '.isDraft' 2>/dev/null || echo absent) - if [ "$state" = "false" ]; then - echo "::error::Pre-release $TAG is already published and its assets are locked; use this workflow with a different version tag." + if state=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" \ + --json isDraft --jq '.isDraft' 2>"$RUNNER_TEMP/gh-err.txt"); then + if [ "$state" = "false" ]; then + echo "::error::Pre-release $TAG is already published; use a different version." + exit 1 + fi + echo "$TAG exists as a draft; assets can still be attached." + elif grep -qi "release not found" "$RUNNER_TEMP/gh-err.txt"; then + echo "No release for $TAG yet." + else + echo "::error::Could not read the state of release $TAG; refusing to continue." + cat "$RUNNER_TEMP/gh-err.txt" exit 1 fi - echo "Release $TAG is $state; assets can still be attached." - uses: actions/checkout@v4 with: @@ -94,11 +113,15 @@ jobs: run: | mkdir -p release/abis - while IFS= read -r name || [ -n "$name" ]; do + # read trims stray spaces and tabs on its own. A carriage return, from a list + # saved with CRLF line endings, it does not: that would end up inside the + # artifact path below, so strip it explicitly. + while read -r name || [ -n "$name" ]; do + name="${name%$'\r'}" case "$name" in '' | '#'*) continue ;; esac abi_file="out/${name}.sol/${name}.json" if [ ! -f "$abi_file" ]; then - echo "::error::${abi_file} not found" + echo "::error::${abi_file} not found; check .github/abi-contracts.txt" exit 1 fi jq '.abi' "$abi_file" > "release/abis/${name}.json" @@ -151,6 +174,9 @@ jobs: ENDOFBODY sed -i 's/^ //' release-body.md + # The heredocs above are quoted so the ```ts fence is not treated as command + # substitution, which also leaves ${TAG} unexpanded. Substitute it here. + sed -i "s|\${TAG}|$TAG|g" release-body.md - name: Create draft pre-release with artifacts uses: softprops/action-gh-release@v2 @@ -178,7 +204,7 @@ jobs: { cat release/expected-assets.txt; echo "dotns-abis-${TAG}.zip"; } \ | 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 carry the expected asset set; leaving it unpublished." + echo "::error::Draft pre-release $TAG does not match the expected asset set; delete the draft and re-run." exit 1 fi echo "Verified $(wc -l < "$RUNNER_TEMP/wanted-assets.txt") assets on draft $TAG." diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index dcfc0f03..e462452c 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -15,9 +15,9 @@ permissions: contents: write # Two runs for the same version would race to attach assets to the same draft. On a -# dispatch `github.ref` is the branch, so key on the requested version instead. +# dispatch `github.ref_name` is the branch, so key on the requested version instead. concurrency: - group: ${{ github.workflow }}-${{ inputs.version || github.ref }} + group: ${{ github.workflow }}-${{ inputs.version || github.ref_name }} cancel-in-progress: false jobs: @@ -29,6 +29,7 @@ jobs: # interpolating the input into the script. - name: Resolve release tag env: + GH_TOKEN: ${{ github.token }} INPUT_VERSION: ${{ inputs.version }} run: | if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then @@ -40,6 +41,14 @@ jobs: 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." @@ -48,13 +57,20 @@ jobs: GH_TOKEN: ${{ github.token }} run: | TAG="$RELEASE_TAG" - state=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" \ - --json isDraft --jq '.isDraft' 2>/dev/null || echo absent) - if [ "$state" = "false" ]; then - echo "::error::Release $TAG is already published and its assets are locked; use this workflow with a different version tag." + if state=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" \ + --json isDraft --jq '.isDraft' 2>"$RUNNER_TEMP/gh-err.txt"); then + if [ "$state" = "false" ]; then + echo "::error::Release $TAG is already published; use a different version." + exit 1 + fi + echo "$TAG exists as a draft; assets can still be attached." + elif grep -qi "release not found" "$RUNNER_TEMP/gh-err.txt"; then + echo "No release for $TAG yet." + else + echo "::error::Could not read the state of release $TAG; refusing to continue." + cat "$RUNNER_TEMP/gh-err.txt" exit 1 fi - echo "Release $TAG is $state; assets can still be attached." - uses: actions/checkout@v4 with: @@ -94,11 +110,15 @@ jobs: run: | mkdir -p release/abis - while IFS= read -r name || [ -n "$name" ]; do + # read trims stray spaces and tabs on its own. A carriage return, from a list + # saved with CRLF line endings, it does not: that would end up inside the + # artifact path below, so strip it explicitly. + while read -r name || [ -n "$name" ]; do + name="${name%$'\r'}" case "$name" in '' | '#'*) continue ;; esac abi_file="out/${name}.sol/${name}.json" if [ ! -f "$abi_file" ]; then - echo "::error::${abi_file} not found" + echo "::error::${abi_file} not found; check .github/abi-contracts.txt" exit 1 fi jq '.abi' "$abi_file" > "release/abis/${name}.json" @@ -149,6 +169,9 @@ jobs: ENDOFBODY sed -i 's/^ //' release-body.md + # The heredocs above are quoted so the ```ts fence is not treated as command + # substitution, which also leaves ${TAG} unexpanded. Substitute it here. + sed -i "s|\${TAG}|$TAG|g" release-body.md - name: Create draft release with artifacts uses: softprops/action-gh-release@v2 @@ -176,7 +199,7 @@ jobs: { cat release/expected-assets.txt; echo "dotns-abis-${TAG}.zip"; } \ | 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 carry the expected asset set; leaving it unpublished." + echo "::error::Draft release $TAG does not match the expected asset set; delete the draft and re-run." exit 1 fi echo "Verified $(wc -l < "$RUNNER_TEMP/wanted-assets.txt") assets on draft $TAG." diff --git a/README.md b/README.md index 5ac8c3a6..dbc770b2 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Pre-releases use **Publish Beta Package** with a suffixed version, `v0.5.5-rc1`. 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. 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 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. ## Economics diff --git a/scripts/shell/pre-commit.sh b/scripts/shell/pre-commit.sh index f565d43f..64ce0db6 100755 --- a/scripts/shell/pre-commit.sh +++ b/scripts/shell/pre-commit.sh @@ -116,6 +116,15 @@ validate_git_config_file() { run_validation "$file" "git-config validation" git config --file "$file" --list } +validate_abi_contracts() { + local file="$1" + + # Each line becomes part of an artifact path in the publish workflows, so a + # carriage return from a CRLF save turns into out/Name\r.sol/Name\r.json and + # aborts the release. Reject it here instead. + run_validation "$file" "line-ending validation" awk '/\r/ { exit 1 }' "$file" +} + echo "pre-commit: validating repository files" while IFS= read -r -d '' file; do [ -f "$file" ] || continue @@ -148,6 +157,9 @@ while IFS= read -r -d '' file; do .gitmodules) validate_git_config_file "$file" ;; + .github/abi-contracts.txt) + validate_abi_contracts "$file" + ;; esac done < <(git ls-files -z) From da753f05a6ea9415d471d850fb74102cc49e9d1f Mon Sep 17 00:00:00 2001 From: giuseppere Date: Tue, 18 Aug 2026 11:39:21 +0200 Subject: [PATCH 4/4] urls fix --- .github/workflows/publish-prerelease.yml | 7 ++++--- .github/workflows/publish-release.yml | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish-prerelease.yml b/.github/workflows/publish-prerelease.yml index e76b357d..9a4ee361 100644 --- a/.github/workflows/publish-prerelease.yml +++ b/.github/workflows/publish-prerelease.yml @@ -142,6 +142,7 @@ jobs: - name: Generate release body run: | TAG="$RELEASE_TAG" + ASSET_BASE="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/download/$TAG" cat > release-body.md << 'ENDOFBODY' ## DotNS ABI Package (Pre-release) @@ -155,14 +156,14 @@ jobs: for f in release/abis/*.json; do name=$(basename "$f" .json) - echo " | \`${name}\` | [${name}.json](dotns-abis-${TAG}.zip) |" >> release-body.md + echo " | \`${name}\` | [${name}.json]($ASSET_BASE/${name}.json) |" >> release-body.md done cat >> release-body.md << 'ENDOFBODY' ### Download - - **All ABIs (zip):** `dotns-abis-${TAG}.zip` + - **All ABIs (zip):** [dotns-abis-${TAG}.zip](${ASSET_BASE}/dotns-abis-${TAG}.zip) - **Individual ABIs:** Each contract ABI is also attached as a separate artifact ### Usage @@ -176,7 +177,7 @@ jobs: sed -i 's/^ //' release-body.md # The heredocs above are quoted so the ```ts fence is not treated as command # substitution, which also leaves ${TAG} unexpanded. Substitute it here. - sed -i "s|\${TAG}|$TAG|g" release-body.md + sed -i "s|\${ASSET_BASE}|$ASSET_BASE|g; s|\${TAG}|$TAG|g" release-body.md - name: Create draft pre-release with artifacts uses: softprops/action-gh-release@v2 diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index e462452c..fcf830ba 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -139,6 +139,7 @@ jobs: - name: Generate release body run: | TAG="$RELEASE_TAG" + ASSET_BASE="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/download/$TAG" cat > release-body.md << 'ENDOFBODY' ## DotNS ABI Package @@ -150,14 +151,14 @@ jobs: for f in release/abis/*.json; do name=$(basename "$f" .json) - echo " | \`${name}\` | [${name}.json](dotns-abis-${TAG}.zip) |" >> release-body.md + echo " | \`${name}\` | [${name}.json]($ASSET_BASE/${name}.json) |" >> release-body.md done cat >> release-body.md << 'ENDOFBODY' ### Download - - **All ABIs (zip):** `dotns-abis-${TAG}.zip` + - **All ABIs (zip):** [dotns-abis-${TAG}.zip](${ASSET_BASE}/dotns-abis-${TAG}.zip) - **Individual ABIs:** Each contract ABI is also attached as a separate artifact ### Usage @@ -171,7 +172,7 @@ jobs: sed -i 's/^ //' release-body.md # The heredocs above are quoted so the ```ts fence is not treated as command # substitution, which also leaves ${TAG} unexpanded. Substitute it here. - sed -i "s|\${TAG}|$TAG|g" release-body.md + sed -i "s|\${ASSET_BASE}|$ASSET_BASE|g; s|\${TAG}|$TAG|g" release-body.md - name: Create draft release with artifacts uses: softprops/action-gh-release@v2