From 890b5c378aea6f3bd57a3e926b393bf77fa77321 Mon Sep 17 00:00:00 2001 From: galargh Date: Thu, 10 Apr 2025 16:42:40 +0100 Subject: [PATCH 01/27] ci: automate the v-next release process using changesets --- .changeset/config.json | 25 +++- .github/actions/setup-env/action.yml | 5 +- .github/workflows/check-changeset-added.yml | 10 +- .github/workflows/release.yml | 122 +++++++++++++++++- v-next/hardhat/test/internal/cli/init/init.ts | 5 +- 5 files changed, 154 insertions(+), 13 deletions(-) diff --git a/.changeset/config.json b/.changeset/config.json index 6c07d22e1d0..06e4421d082 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,6 +1,6 @@ { "$schema": "https://unpkg.com/@changesets/config@1.6.0/schema.json", - "changelog": false, + "changelog": "@changesets/cli/changelog", "commit": false, "linked": [], "access": "public", @@ -11,6 +11,29 @@ "@nomicfoundation/template-package", "template-*" ], + "fixed": [ + [ + "hardhat", + "@nomicfoundation/hardhat-errors", + "@nomicfoundation/hardhat-ethers", + "@nomicfoundation/hardhat-ethers-chai-matchers", + "@nomicfoundation/hardhat-ignition", + "@nomicfoundation/ignition-core", + "@nomicfoundation/hardhat-ignition-ethers", + "@nomicfoundation/ignition-ui", + "@nomicfoundation/hardhat-ignition-viem", + "@nomicfoundation/hardhat-keystore", + "@nomicfoundation/hardhat-mocha", + "@nomicfoundation/hardhat-network-helpers", + "@nomicfoundation/hardhat-node-test-reporter", + "@nomicfoundation/hardhat-node-test-runner", + "@nomicfoundation/hardhat-test-utils", + "@nomicfoundation/hardhat-typechain", + "@nomicfoundation/hardhat-utils", + "@nomicfoundation/hardhat-viem", + "@nomicfoundation/hardhat-zod-utils" + ] + ], "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { "onlyUpdatePeerDependentsWhenOutOfRange": true } diff --git a/.github/actions/setup-env/action.yml b/.github/actions/setup-env/action.yml index f1d165f6855..c3a89a86c78 100644 --- a/.github/actions/setup-env/action.yml +++ b/.github/actions/setup-env/action.yml @@ -25,12 +25,13 @@ runs: - uses: pnpm/action-setup@v4 with: version: ${{ inputs.pnpm-version }} - - uses: actions/setup-node@v4 - id: setup-node + - id: setup-node + uses: actions/setup-node@v4 with: node-version: ${{ inputs.node-version }} cache: ${{ inputs.cache-save == 'true' && 'pnpm' || '' }} cache-dependency-path: "**/pnpm-lock.yaml" + registry-url: https://npm.pkg.github.com - id: pnpm if: inputs.cache-save == 'false' run: pnpm store path --silent | xargs -I {} -0 echo "path={}" | tee -a $GITHUB_OUTPUT diff --git a/.github/workflows/check-changeset-added.yml b/.github/workflows/check-changeset-added.yml index d5a08438185..299e5c19015 100644 --- a/.github/workflows/check-changeset-added.yml +++ b/.github/workflows/check-changeset-added.yml @@ -20,20 +20,23 @@ jobs: check-if-changeset: name: Check that PR has a changeset runs-on: ubuntu-latest - # don't run this check in the changesets PR - if: github.head_ref != 'changeset-release/main' steps: - uses: actions/github-script@v7 with: script: | const isMergeGroup = context.eventName === "merge_group"; - // Merge group context if (isMergeGroup) { console.log("Ignore changeset check for merge group."); return; } + const isReleasePr = process.env.GITHUB_HEAD_REF.startsWith("changeset-release/"); + if (isReleasePr) { + console.log("Ignore changeset check for release PR."); + return; + } + // Single PR context const pullNumber = context.issue.number; @@ -51,7 +54,6 @@ jobs: console.log("No changeset found"); - const { data: pull } = await github.rest.pulls.get({ ...context.issue, pull_number: pullNumber diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index afc3d2560e4..45874f0f11f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,17 +1,22 @@ name: Release on: + workflow_dispatch: push: branches: - - main + - v-next + +defaults: + run: + shell: bash jobs: release: name: Release runs-on: ubuntu-latest permissions: - contents: write - pull-requests: write + contents: write # This allows us to push to the repository and create GitHub releases + pull-requests: write # This allows us to create pull requests steps: - name: Checkout Repo uses: actions/checkout@v4 @@ -19,12 +24,119 @@ jobs: # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits fetch-depth: 0 - - uses: ./.github/actions/setup-env + - name: Set up the environment + uses: ./.github/actions/setup-env - name: Install Dependencies run: pnpm install --frozen-lockfile --prefer-offline - name: Create Release Pull Request + id: pr + env: + GITHUB_TOKEN: ${{ github.token }} uses: changesets/action@v1 + + - name: Build All Packages + if: steps.pr.outputs.hasChangesets == '' + run: pnpm run --recursive -no-bail --filter './v-next/**' --if-present build + + - name: Publish All Packages (dry-run) + if: steps.pr.outputs.hasChangesets == '' + run: pnpm publish --filter "./v-next/**" -r --no-git-checks --tag next --access public --dry-run + + - name: Publish All Packages + id: publish + if: steps.pr.outputs.hasChangesets == '' + env: + NODE_AUTH_TOKEN: ${{ github.token }} + NPM_CONFIG_PROVENANCE: true + run: | + echo "stdout<> $GITHUB_OUTPUT + pnpm publish --filter "./v-next/**" -r --no-git-checks --tag next --access public | tee -a $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Check if hardhat was published + id: hardhat + if: steps.pr.outputs.hasChangesets == '' + env: + STDOUT: ${{ steps.publish.outputs.stdout }} + PREFIX: 'New tag: hardhat@' + run: echo "$STDOUT" | grep "$PREFIX" | tr -d "$PREFIX" | xargs -I {} -0 echo "version={}" | tee -a $GITHUB_OUTPUT + + - name: Find the changelog entry + id: changelog + if: steps.hardhat.outputs.version != '' + env: + VERSION: steps.hardhat.outputs.version + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs') + + const changelog = fs.readFileSync('./v-next/hardhat/CHANGELOG.md') + core.debug(`Changelog: ${changelog}`) + + core.info('Parsing changelog...') + const lines = changelog.split('\n') + const headerIndex = lines.findIndex((line) => line == `## ${process.env.VERSION}`) + + if (headerIndex == -1) { + core.error(`Changelog entry for version ${process.env.VERSION} not found in ./v-next/hardhat/CHANGELOG.md`) + process.exit(1) + } + + const entryLines = []; + + for (const line of lines.slice(headerIndex + 1)) { + if (line.startsWith('## ')) { + break + } + entryLines.push(line) + } + + const entry = entryLines.join('\n').trim() + + core.debug(`Entry: ${entry}`) + core.setOutput('entry', entry) + + - name: Retrieve the current latest version + id: latest + if: steps.hardhat.outputs.version != '' + run: gh release view --json tagName --jq .tagName | tr -d 'hardhat@' | xargs -I {} -0 echo "version={}" | tee -a $GITHUB_OUTPUT + + - name: Check version + id: version + if: steps.hardhat.outputs.version != '' env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: steps.hardhat.outputs.version + LATEST: steps.latest.outputs.version + run: | + # NOTE: This check would mark non-prerelease versions with - in the build tag as prereleases + [[ "$VERSION" == *-* ]] && echo "true" || echo "false" | xargs -I {} -0 echo "prerelease={}" | tee -a $GITHUB_OUTPUT + latest="$((echo "$VERSION" && echo "$LATEST") | sort -V | tail -n1)" + [[ "$VERSION" == "$latest" ]] && echo "true" || echo "false" | xargs -I {} -0 echo "latest={}" | tee -a $GITHUB_OUTPUT + + - name: Create GitHub Release + if: steps.hardhat.outputs.version != '' + env: + GITHUB_TOKEN: ${{ github.token }} + # NOTE: The action updates the release if it already exists + uses: galargh/action-gh-release@571276229e7c9e6ea18f99bad24122a4c3ec813f # https://github.com/galargh/action-gh-release/pull/1 + with: + draft: false + tag_name: hardhat@${{ steps.hardhat.outputs.version }} + generate_release_notes: false + target_commitish: ${{ github.sha }} + make_latest: ${{ steps.version.outputs.prerelease == 'false' && steps.version.outputs.latest == 'true' }} + prerelease: ${{ steps.version.outputs.prerelease == 'true' }} + body: | + # ${{ steps.hardhat.outputs.version }} + + ### Changes + + ${{ steps.changelog.outputs.entry }} + + --- + > 💡 **The Nomic Foundation is hiring! Check [our open positions](https://www.nomic.foundation/jobs).** + --- + token: ${{ github.token }} diff --git a/v-next/hardhat/test/internal/cli/init/init.ts b/v-next/hardhat/test/internal/cli/init/init.ts index 0837aa8b2c0..92246fa98c1 100644 --- a/v-next/hardhat/test/internal/cli/init/init.ts +++ b/v-next/hardhat/test/internal/cli/init/init.ts @@ -393,7 +393,10 @@ describe("initHardhat", async () => { it( `should initialize the project using the ${template.name} template in an empty folder`, { - skip: process.env.HARDHAT_DISABLE_SLOW_TESTS === "true", + skip: + process.env.HARDHAT_DISABLE_SLOW_TESTS === "true" || + process.env.GITHUB_EVENT_NAME === "merge_group" || + process.env.GITHUB_HEAD_REF?.startsWith("changeset-release/"), }, async () => { await initHardhat({ From c45cf557f5c8f42c567461c0f15d2863768ff278 Mon Sep 17 00:00:00 2001 From: galargh Date: Fri, 11 Apr 2025 00:22:15 +0200 Subject: [PATCH 02/27] chore: fix the hardhat-ethers version --- pnpm-lock.yaml | 10 +++++----- v-next/example-project/package.json | 2 +- v-next/hardhat-ethers-chai-matchers/package.json | 2 +- v-next/hardhat-ethers/CHANGELOG.md | 4 ++-- v-next/hardhat-ethers/package.json | 2 +- v-next/hardhat-ignition-ethers/package.json | 4 ++-- v-next/hardhat-typechain/package.json | 2 +- v-next/hardhat/templates/02-mocha-ethers/package.json | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3733262f459..5f4a64f95f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,7 +21,7 @@ importers: specifier: workspace:^3.0.0-next.3 version: link:../hardhat-errors '@nomicfoundation/hardhat-ethers': - specifier: workspace:^4.0.0-next.3 + specifier: workspace:^3.0.0-next.3 version: link:../hardhat-ethers '@nomicfoundation/hardhat-ethers-chai-matchers': specifier: workspace:^3.0.0-next.3 @@ -360,7 +360,7 @@ importers: specifier: workspace:^3.0.0-next.3 version: link:../hardhat-errors '@nomicfoundation/hardhat-ethers': - specifier: workspace:^4.0.0-next.3 + specifier: workspace:^3.0.0-next.3 version: link:../hardhat-ethers '@nomicfoundation/hardhat-utils': specifier: workspace:^3.0.0-next.3 @@ -712,7 +712,7 @@ importers: specifier: 1.0.2 version: 1.0.2(nyc@15.1.0) '@nomicfoundation/hardhat-ethers': - specifier: workspace:^4.0.0-next.3 + specifier: workspace:^3.0.0-next.3 version: link:../hardhat-ethers '@nomicfoundation/hardhat-ignition': specifier: workspace:^3.0.0-next.3 @@ -1445,7 +1445,7 @@ importers: specifier: workspace:^3.0.0-next.3 version: link:../hardhat-errors '@nomicfoundation/hardhat-ethers': - specifier: workspace:^4.0.0-next.3 + specifier: workspace:^3.0.0-next.3 version: link:../hardhat-ethers '@nomicfoundation/hardhat-utils': specifier: workspace:^3.0.0-next.3 @@ -1788,7 +1788,7 @@ importers: v-next/hardhat/templates/02-mocha-ethers: devDependencies: '@nomicfoundation/hardhat-ethers': - specifier: workspace:^4.0.0-next.3 + specifier: workspace:^3.0.0-next.3 version: link:../../../hardhat-ethers '@nomicfoundation/hardhat-ethers-chai-matchers': specifier: workspace:^3.0.0-next.3 diff --git a/v-next/example-project/package.json b/v-next/example-project/package.json index c36a93c647a..48ace9a214f 100644 --- a/v-next/example-project/package.json +++ b/v-next/example-project/package.json @@ -26,7 +26,7 @@ "hardhat": "workspace:^3.0.0-next.3", "@nomicfoundation/hardhat-ethers-chai-matchers": "workspace:^3.0.0-next.3", "@nomicfoundation/hardhat-errors": "workspace:^3.0.0-next.3", - "@nomicfoundation/hardhat-ethers": "workspace:^4.0.0-next.3", + "@nomicfoundation/hardhat-ethers": "workspace:^3.0.0-next.3", "@nomicfoundation/hardhat-ignition": "workspace:^3.0.0-next.3", "@nomicfoundation/ignition-core": "workspace:^3.0.0-next.3", "@nomicfoundation/hardhat-ignition-viem": "workspace:^3.0.0-next.3", diff --git a/v-next/hardhat-ethers-chai-matchers/package.json b/v-next/hardhat-ethers-chai-matchers/package.json index 63e87a7cc18..1cb0537cd4c 100644 --- a/v-next/hardhat-ethers-chai-matchers/package.json +++ b/v-next/hardhat-ethers-chai-matchers/package.json @@ -79,7 +79,7 @@ }, "peerDependencies": { "hardhat": "workspace:^3.0.0-next.3", - "@nomicfoundation/hardhat-ethers": "workspace:^4.0.0-next.3", + "@nomicfoundation/hardhat-ethers": "workspace:^3.0.0-next.3", "chai": "^5.1.2", "ethers": "^6.13.4" } diff --git a/v-next/hardhat-ethers/CHANGELOG.md b/v-next/hardhat-ethers/CHANGELOG.md index 413d304eff6..93c43dabb2b 100644 --- a/v-next/hardhat-ethers/CHANGELOG.md +++ b/v-next/hardhat-ethers/CHANGELOG.md @@ -1,12 +1,12 @@ # @nomicfoundation/hardhat-ethers -## 4.0.0-next.2 +## 3.0.0-next.2 ### Patch Changes - Hardhat 3 Alpha release (2025-03-20T08:38:27.809Z) -## 4.0.0-next.1 +## 3.0.0-next.1 ### Patch Changes diff --git a/v-next/hardhat-ethers/package.json b/v-next/hardhat-ethers/package.json index cb88690e399..a343a4eaa52 100644 --- a/v-next/hardhat-ethers/package.json +++ b/v-next/hardhat-ethers/package.json @@ -1,6 +1,6 @@ { "name": "@nomicfoundation/hardhat-ethers", - "version": "4.0.0-next.3", + "version": "3.0.0-next.3", "description": "Hardhat plugin for ethers", "homepage": "https://github.com/nomicfoundation/hardhat/tree/v-next/v-next/hardhat-ethers", "repository": { diff --git a/v-next/hardhat-ignition-ethers/package.json b/v-next/hardhat-ignition-ethers/package.json index 07cf09fdbdb..8c13dd97330 100644 --- a/v-next/hardhat-ignition-ethers/package.json +++ b/v-next/hardhat-ignition-ethers/package.json @@ -34,7 +34,7 @@ "devDependencies": { "@istanbuljs/nyc-config-typescript": "1.0.2", "hardhat": "workspace:^3.0.0-next.3", - "@nomicfoundation/hardhat-ethers": "workspace:^4.0.0-next.3", + "@nomicfoundation/hardhat-ethers": "workspace:^3.0.0-next.3", "@nomicfoundation/hardhat-ignition": "workspace:^3.0.0-next.3", "@nomicfoundation/ignition-core": "workspace:^3.0.0-next.3", "@nomicfoundation/hardhat-test-utils": "workspace:^", @@ -69,7 +69,7 @@ }, "peerDependencies": { "hardhat": "workspace:^3.0.0-next.3", - "@nomicfoundation/hardhat-ethers": "workspace:^4.0.0-next.3", + "@nomicfoundation/hardhat-ethers": "workspace:^3.0.0-next.3", "@nomicfoundation/hardhat-ignition": "workspace:^3.0.0-next.3", "@nomicfoundation/ignition-core": "workspace:^3.0.0-next.3", "ethers": "^6.13.4" diff --git a/v-next/hardhat-typechain/package.json b/v-next/hardhat-typechain/package.json index d0f94fc4b3a..d0bb355cf19 100644 --- a/v-next/hardhat-typechain/package.json +++ b/v-next/hardhat-typechain/package.json @@ -76,7 +76,7 @@ }, "peerDependencies": { "hardhat": "workspace:^3.0.0-next.3", - "@nomicfoundation/hardhat-ethers": "workspace:^4.0.0-next.3", + "@nomicfoundation/hardhat-ethers": "workspace:^3.0.0-next.3", "ethers": "^6.13.4" } } diff --git a/v-next/hardhat/templates/02-mocha-ethers/package.json b/v-next/hardhat/templates/02-mocha-ethers/package.json index 84e3a02039f..23ba913c71c 100644 --- a/v-next/hardhat/templates/02-mocha-ethers/package.json +++ b/v-next/hardhat/templates/02-mocha-ethers/package.json @@ -7,7 +7,7 @@ "devDependencies": { "hardhat": "workspace:^3.0.0-next.3", "@nomicfoundation/hardhat-ethers-chai-matchers": "workspace:^3.0.0-next.3", - "@nomicfoundation/hardhat-ethers": "workspace:^4.0.0-next.3", + "@nomicfoundation/hardhat-ethers": "workspace:^3.0.0-next.3", "@nomicfoundation/hardhat-ignition": "workspace:^3.0.0-next.3", "@nomicfoundation/hardhat-ignition-ethers": "workspace:^3.0.0-next.3", "@nomicfoundation/hardhat-keystore": "workspace:^3.0.0-next.3", From 32a8235ae97f4859e22aedf27d62bb616e699b0c Mon Sep 17 00:00:00 2001 From: galargh Date: Fri, 11 Apr 2025 00:43:16 +0200 Subject: [PATCH 03/27] fix: update lockfile after changeset version --- .github/workflows/release.yml | 7 +++++++ package.json | 1 + 2 files changed, 8 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 45874f0f11f..8898780ec63 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,10 @@ name: Release on: + # TODO: When a release PR is merged we should run a merge_group check to ensure: + # 1. The release PR is the only one in the merge group + # 2. The release PR is up-to-date with the target branch + # i.e. no other PR has been merged since the release PR was added to the merge queue workflow_dispatch: push: branches: @@ -33,8 +37,11 @@ jobs: - name: Create Release Pull Request id: pr env: + # NOTE: If we use the GITHUB_TOKEN to create the release PR, the checks will not be triggered automatically GITHUB_TOKEN: ${{ github.token }} uses: changesets/action@v1 + with: + version: pnpm run version - name: Build All Packages if: steps.pr.outputs.hasChangesets == '' diff --git a/package.json b/package.json index 89052cd02de..5447fa47291 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "typescript": "~5.5.0" }, "scripts": { + "version": "changeset version && pnpm install --lockfile-only", "build": "pnpm run --recursive build", "clean": "pnpm run --recursive clean", "test": "pnpm run --recursive test", From 1b010420458d5369fea58cdcdf8cf03b997f67ed Mon Sep 17 00:00:00 2001 From: galargh Date: Fri, 11 Apr 2025 00:50:18 +0200 Subject: [PATCH 04/27] fix: change the expected value of hasChangesets output --- .github/workflows/release.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8898780ec63..448a830862f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,16 +44,16 @@ jobs: version: pnpm run version - name: Build All Packages - if: steps.pr.outputs.hasChangesets == '' + if: steps.pr.outputs.hasChangesets == 'false' run: pnpm run --recursive -no-bail --filter './v-next/**' --if-present build - name: Publish All Packages (dry-run) - if: steps.pr.outputs.hasChangesets == '' + if: steps.pr.outputs.hasChangesets == 'false' run: pnpm publish --filter "./v-next/**" -r --no-git-checks --tag next --access public --dry-run - name: Publish All Packages id: publish - if: steps.pr.outputs.hasChangesets == '' + if: steps.pr.outputs.hasChangesets == 'false' env: NODE_AUTH_TOKEN: ${{ github.token }} NPM_CONFIG_PROVENANCE: true @@ -64,7 +64,7 @@ jobs: - name: Check if hardhat was published id: hardhat - if: steps.pr.outputs.hasChangesets == '' + if: steps.pr.outputs.hasChangesets == 'false' env: STDOUT: ${{ steps.publish.outputs.stdout }} PREFIX: 'New tag: hardhat@' From 5764c245d0a0fd7667ac4aa58dbfd45304c4bc5d Mon Sep 17 00:00:00 2001 From: galargh Date: Fri, 11 Apr 2025 01:14:27 +0200 Subject: [PATCH 05/27] fix: authenticate with the npmjs registry --- .github/actions/setup-env/action.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/setup-env/action.yml b/.github/actions/setup-env/action.yml index c3a89a86c78..153734b4603 100644 --- a/.github/actions/setup-env/action.yml +++ b/.github/actions/setup-env/action.yml @@ -31,7 +31,7 @@ runs: node-version: ${{ inputs.node-version }} cache: ${{ inputs.cache-save == 'true' && 'pnpm' || '' }} cache-dependency-path: "**/pnpm-lock.yaml" - registry-url: https://npm.pkg.github.com + registry-url: https://registry.npmjs.org - id: pnpm if: inputs.cache-save == 'false' run: pnpm store path --silent | xargs -I {} -0 echo "path={}" | tee -a $GITHUB_OUTPUT diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 448a830862f..b229f6e454e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,7 +55,7 @@ jobs: id: publish if: steps.pr.outputs.hasChangesets == 'false' env: - NODE_AUTH_TOKEN: ${{ github.token }} + NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} NPM_CONFIG_PROVENANCE: true run: | echo "stdout<> $GITHUB_OUTPUT From 79e71bdc32c84e8680ff92471ef42255498e45a8 Mon Sep 17 00:00:00 2001 From: galargh Date: Fri, 11 Apr 2025 09:20:29 +0200 Subject: [PATCH 06/27] chore: replace complex logic with js scripts --- .github/workflows/release.yml | 95 +++++++++++++++++++++++++++-------- 1 file changed, 74 insertions(+), 21 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b229f6e454e..61e2dd2caa8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,10 +67,80 @@ jobs: if: steps.pr.outputs.hasChangesets == 'false' env: STDOUT: ${{ steps.publish.outputs.stdout }} - PREFIX: 'New tag: hardhat@' - run: echo "$STDOUT" | grep "$PREFIX" | tr -d "$PREFIX" | xargs -I {} -0 echo "version={}" | tee -a $GITHUB_OUTPUT + uses: actions/github-script@v7 + with: + script: | + const lines = process.env.STDOUT.split('\n') + + core.info('Checking if Hardhat was published') + + const line = lines.find(line => line.startsWith('+ hardhat@')) + + if (line === undefined) { + core.info('Hardhat was not published') + core.setOutput('version', '') + process.exit(0) + } + + const version = line.split('@')[1] + core.info(`Hardhat was published with version ${version}`) + core.setOutput('version', version) + + - name: Check the version of the published package + id: version + if: steps.hardhat.outputs.version != '' + env: + VERSION: steps.hardhat.outputs.version + uses: actions/github-script@v7 + with: + script: | + const version = process.env.VERSION + + core.info('Checking if Hardhat was published as a prerelease') + + // NOTE: This check would mark non-prerelease versions with - in the build tag as prereleases + const prerelease = version.includes('-') + if (prerelease) { + core.info('Hardhat was published as a prerelease') + core.setOutput('prerelease', true) + } else { + core.info('Hardhat was not published as a prerelease') + core.setOutput('prerelease', false) + } + + core.info('Checking if this is the latest version') - - name: Find the changelog entry + if (prerelease) { + core.info('This is a prerelease, so this is not the latest version') + core.setOutput('latest', false) + process.exit(0) + } + + const { data: release } = await github.rest.repos.getLatestRelease(context.repo) + + if (release === undefined) { + core.info('No existing latest release found, so this is the latest version') + core.setOutput('latest', true) + process.exit(0) + } + + const latestVersion = release.tag_name.split('@')[1] + core.info(`Current latest version is ${latestVersion}`) + + const ([major, minor, patch]) = version.split('.') + const ([latestMajor, latestMinor, latestPatch]) = latestVersion.split('.') + + if (parseInt(major, 10) > parseInt(latestMajor, 10) || + parseInt(minor, 10) > parseInt(latestMinor, 10) || + parseInt(patch, 10) > parseInt(latestPatch, 10)) { + core.info('This is a new latest version') + core.setOutput('latest', true) + } else { + core.info('This is not a new latest version') + core.setOutput('latest', false) + } + + - name: Find the relevant changelog entry id: changelog if: steps.hardhat.outputs.version != '' env: @@ -106,23 +176,6 @@ jobs: core.debug(`Entry: ${entry}`) core.setOutput('entry', entry) - - name: Retrieve the current latest version - id: latest - if: steps.hardhat.outputs.version != '' - run: gh release view --json tagName --jq .tagName | tr -d 'hardhat@' | xargs -I {} -0 echo "version={}" | tee -a $GITHUB_OUTPUT - - - name: Check version - id: version - if: steps.hardhat.outputs.version != '' - env: - VERSION: steps.hardhat.outputs.version - LATEST: steps.latest.outputs.version - run: | - # NOTE: This check would mark non-prerelease versions with - in the build tag as prereleases - [[ "$VERSION" == *-* ]] && echo "true" || echo "false" | xargs -I {} -0 echo "prerelease={}" | tee -a $GITHUB_OUTPUT - latest="$((echo "$VERSION" && echo "$LATEST") | sort -V | tail -n1)" - [[ "$VERSION" == "$latest" ]] && echo "true" || echo "false" | xargs -I {} -0 echo "latest={}" | tee -a $GITHUB_OUTPUT - - name: Create GitHub Release if: steps.hardhat.outputs.version != '' env: @@ -134,7 +187,7 @@ jobs: tag_name: hardhat@${{ steps.hardhat.outputs.version }} generate_release_notes: false target_commitish: ${{ github.sha }} - make_latest: ${{ steps.version.outputs.prerelease == 'false' && steps.version.outputs.latest == 'true' }} + make_latest: ${{ steps.version.outputs.latest == 'true' }} prerelease: ${{ steps.version.outputs.prerelease == 'true' }} body: | # ${{ steps.hardhat.outputs.version }} From a95cb21195247d8b9ac698648fafa765c4a5583c Mon Sep 17 00:00:00 2001 From: galargh Date: Fri, 11 Apr 2025 09:43:29 +0200 Subject: [PATCH 07/27] fix: syntax and error handling in release scripts --- .github/workflows/release.yml | 51 +++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 61e2dd2caa8..e68ef38cf79 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,6 +51,8 @@ jobs: if: steps.pr.outputs.hasChangesets == 'false' run: pnpm publish --filter "./v-next/**" -r --no-git-checks --tag next --access public --dry-run + # NOTE: When running publish on an already published package we expect it to succeed but not to release anything and not include + hardhat@ in the output. + # If it does, however, include + hardhat@ in the output, it's OK, because we'll just update the release without moving the release tag forward. - name: Publish All Packages id: publish if: steps.pr.outputs.hasChangesets == 'false' @@ -90,7 +92,7 @@ jobs: id: version if: steps.hardhat.outputs.version != '' env: - VERSION: steps.hardhat.outputs.version + VERSION: ${{ steps.hardhat.outputs.version }} uses: actions/github-script@v7 with: script: | @@ -116,41 +118,44 @@ jobs: process.exit(0) } - const { data: release } = await github.rest.repos.getLatestRelease(context.repo) + try { + const { data: release } = await github.rest.repos.getLatestRelease(context.repo) - if (release === undefined) { - core.info('No existing latest release found, so this is the latest version') - core.setOutput('latest', true) - process.exit(0) - } + const latestVersion = release.tag_name.split('@')[1] + core.info(`Current latest version is ${latestVersion}`) - const latestVersion = release.tag_name.split('@')[1] - core.info(`Current latest version is ${latestVersion}`) + const [major, minor, patch] = version.split('.') + const [latestMajor, latestMinor, latestPatch] = latestVersion.split('.') - const ([major, minor, patch]) = version.split('.') - const ([latestMajor, latestMinor, latestPatch]) = latestVersion.split('.') - - if (parseInt(major, 10) > parseInt(latestMajor, 10) || - parseInt(minor, 10) > parseInt(latestMinor, 10) || - parseInt(patch, 10) > parseInt(latestPatch, 10)) { - core.info('This is a new latest version') - core.setOutput('latest', true) - } else { - core.info('This is not a new latest version') - core.setOutput('latest', false) + if (parseInt(major, 10) > parseInt(latestMajor, 10) || + parseInt(minor, 10) > parseInt(latestMinor, 10) || + parseInt(patch, 10) > parseInt(latestPatch, 10)) { + core.info('This is a new latest version') + core.setOutput('latest', true) + } else { + core.info('This is not a new latest version') + core.setOutput('latest', false) + } + } catch (error) { + if (error.status === 404) { + core.info('No existing latest release found, so this is the latest version') + core.setOutput('latest', true) + process.exit(0) + } + throw error } - name: Find the relevant changelog entry id: changelog if: steps.hardhat.outputs.version != '' env: - VERSION: steps.hardhat.outputs.version + VERSION: ${{ steps.hardhat.outputs.version }} uses: actions/github-script@v7 with: script: | const fs = require('fs') - const changelog = fs.readFileSync('./v-next/hardhat/CHANGELOG.md') + const changelog = fs.readFileSync('./v-next/hardhat/CHANGELOG.md').toString() core.debug(`Changelog: ${changelog}`) core.info('Parsing changelog...') @@ -192,7 +197,7 @@ jobs: body: | # ${{ steps.hardhat.outputs.version }} - ### Changes + ## Changes ${{ steps.changelog.outputs.entry }} From 3b67142d222ed207cdd24b348209a03e03a3fe1e Mon Sep 17 00:00:00 2001 From: galargh Date: Fri, 11 Apr 2025 10:33:24 +0200 Subject: [PATCH 08/27] fix: do not run dependency install tests on release PR merges --- v-next/hardhat/test/internal/cli/init/init.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/v-next/hardhat/test/internal/cli/init/init.ts b/v-next/hardhat/test/internal/cli/init/init.ts index 92246fa98c1..107bca2d66f 100644 --- a/v-next/hardhat/test/internal/cli/init/init.ts +++ b/v-next/hardhat/test/internal/cli/init/init.ts @@ -278,7 +278,11 @@ describe("installProjectDependencies", async () => { it( "should install any existing template dependencies that are out of date if the user opts-in to the update", { - skip: process.env.HARDHAT_DISABLE_SLOW_TESTS === "true", + skip: + process.env.HARDHAT_DISABLE_SLOW_TESTS === "true" || + process.env.GITHUB_EVENT_NAME === "push" || // TODO: This check should be limited to push events associated with a release PR merge + process.env.GITHUB_EVENT_NAME === "merge_group" || // TODO: This check should be limited to merge_group events associated with a release PR merge + process.env.GITHUB_HEAD_REF?.startsWith("changeset-release/"), }, async () => { const template = await getTemplate("mocha-ethers"); @@ -395,7 +399,8 @@ describe("initHardhat", async () => { { skip: process.env.HARDHAT_DISABLE_SLOW_TESTS === "true" || - process.env.GITHUB_EVENT_NAME === "merge_group" || + process.env.GITHUB_EVENT_NAME === "push" || // TODO: This check should be limited to push events associated with a release PR merge + process.env.GITHUB_EVENT_NAME === "merge_group" || // TODO: This check should be limited to merge_group events associated with a release PR merge process.env.GITHUB_HEAD_REF?.startsWith("changeset-release/"), }, async () => { From 43e9c5d9d53b88fdf86a26f2986ea65ca309abf2 Mon Sep 17 00:00:00 2001 From: galargh Date: Mon, 5 May 2025 14:18:17 +0100 Subject: [PATCH 09/27] chore: revert the hardhat-ethers version change --- pnpm-lock.yaml | 10 +++++----- v-next/example-project/package.json | 2 +- v-next/hardhat-ethers-chai-matchers/package.json | 2 +- v-next/hardhat-ethers/package.json | 2 +- v-next/hardhat-ignition-ethers/package.json | 4 ++-- v-next/hardhat-typechain/package.json | 2 +- v-next/hardhat/templates/02-mocha-ethers/package.json | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f4a64f95f2..3733262f459 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,7 +21,7 @@ importers: specifier: workspace:^3.0.0-next.3 version: link:../hardhat-errors '@nomicfoundation/hardhat-ethers': - specifier: workspace:^3.0.0-next.3 + specifier: workspace:^4.0.0-next.3 version: link:../hardhat-ethers '@nomicfoundation/hardhat-ethers-chai-matchers': specifier: workspace:^3.0.0-next.3 @@ -360,7 +360,7 @@ importers: specifier: workspace:^3.0.0-next.3 version: link:../hardhat-errors '@nomicfoundation/hardhat-ethers': - specifier: workspace:^3.0.0-next.3 + specifier: workspace:^4.0.0-next.3 version: link:../hardhat-ethers '@nomicfoundation/hardhat-utils': specifier: workspace:^3.0.0-next.3 @@ -712,7 +712,7 @@ importers: specifier: 1.0.2 version: 1.0.2(nyc@15.1.0) '@nomicfoundation/hardhat-ethers': - specifier: workspace:^3.0.0-next.3 + specifier: workspace:^4.0.0-next.3 version: link:../hardhat-ethers '@nomicfoundation/hardhat-ignition': specifier: workspace:^3.0.0-next.3 @@ -1445,7 +1445,7 @@ importers: specifier: workspace:^3.0.0-next.3 version: link:../hardhat-errors '@nomicfoundation/hardhat-ethers': - specifier: workspace:^3.0.0-next.3 + specifier: workspace:^4.0.0-next.3 version: link:../hardhat-ethers '@nomicfoundation/hardhat-utils': specifier: workspace:^3.0.0-next.3 @@ -1788,7 +1788,7 @@ importers: v-next/hardhat/templates/02-mocha-ethers: devDependencies: '@nomicfoundation/hardhat-ethers': - specifier: workspace:^3.0.0-next.3 + specifier: workspace:^4.0.0-next.3 version: link:../../../hardhat-ethers '@nomicfoundation/hardhat-ethers-chai-matchers': specifier: workspace:^3.0.0-next.3 diff --git a/v-next/example-project/package.json b/v-next/example-project/package.json index 48ace9a214f..c36a93c647a 100644 --- a/v-next/example-project/package.json +++ b/v-next/example-project/package.json @@ -26,7 +26,7 @@ "hardhat": "workspace:^3.0.0-next.3", "@nomicfoundation/hardhat-ethers-chai-matchers": "workspace:^3.0.0-next.3", "@nomicfoundation/hardhat-errors": "workspace:^3.0.0-next.3", - "@nomicfoundation/hardhat-ethers": "workspace:^3.0.0-next.3", + "@nomicfoundation/hardhat-ethers": "workspace:^4.0.0-next.3", "@nomicfoundation/hardhat-ignition": "workspace:^3.0.0-next.3", "@nomicfoundation/ignition-core": "workspace:^3.0.0-next.3", "@nomicfoundation/hardhat-ignition-viem": "workspace:^3.0.0-next.3", diff --git a/v-next/hardhat-ethers-chai-matchers/package.json b/v-next/hardhat-ethers-chai-matchers/package.json index 1cb0537cd4c..63e87a7cc18 100644 --- a/v-next/hardhat-ethers-chai-matchers/package.json +++ b/v-next/hardhat-ethers-chai-matchers/package.json @@ -79,7 +79,7 @@ }, "peerDependencies": { "hardhat": "workspace:^3.0.0-next.3", - "@nomicfoundation/hardhat-ethers": "workspace:^3.0.0-next.3", + "@nomicfoundation/hardhat-ethers": "workspace:^4.0.0-next.3", "chai": "^5.1.2", "ethers": "^6.13.4" } diff --git a/v-next/hardhat-ethers/package.json b/v-next/hardhat-ethers/package.json index a343a4eaa52..cb88690e399 100644 --- a/v-next/hardhat-ethers/package.json +++ b/v-next/hardhat-ethers/package.json @@ -1,6 +1,6 @@ { "name": "@nomicfoundation/hardhat-ethers", - "version": "3.0.0-next.3", + "version": "4.0.0-next.3", "description": "Hardhat plugin for ethers", "homepage": "https://github.com/nomicfoundation/hardhat/tree/v-next/v-next/hardhat-ethers", "repository": { diff --git a/v-next/hardhat-ignition-ethers/package.json b/v-next/hardhat-ignition-ethers/package.json index 8c13dd97330..07cf09fdbdb 100644 --- a/v-next/hardhat-ignition-ethers/package.json +++ b/v-next/hardhat-ignition-ethers/package.json @@ -34,7 +34,7 @@ "devDependencies": { "@istanbuljs/nyc-config-typescript": "1.0.2", "hardhat": "workspace:^3.0.0-next.3", - "@nomicfoundation/hardhat-ethers": "workspace:^3.0.0-next.3", + "@nomicfoundation/hardhat-ethers": "workspace:^4.0.0-next.3", "@nomicfoundation/hardhat-ignition": "workspace:^3.0.0-next.3", "@nomicfoundation/ignition-core": "workspace:^3.0.0-next.3", "@nomicfoundation/hardhat-test-utils": "workspace:^", @@ -69,7 +69,7 @@ }, "peerDependencies": { "hardhat": "workspace:^3.0.0-next.3", - "@nomicfoundation/hardhat-ethers": "workspace:^3.0.0-next.3", + "@nomicfoundation/hardhat-ethers": "workspace:^4.0.0-next.3", "@nomicfoundation/hardhat-ignition": "workspace:^3.0.0-next.3", "@nomicfoundation/ignition-core": "workspace:^3.0.0-next.3", "ethers": "^6.13.4" diff --git a/v-next/hardhat-typechain/package.json b/v-next/hardhat-typechain/package.json index d0bb355cf19..d0f94fc4b3a 100644 --- a/v-next/hardhat-typechain/package.json +++ b/v-next/hardhat-typechain/package.json @@ -76,7 +76,7 @@ }, "peerDependencies": { "hardhat": "workspace:^3.0.0-next.3", - "@nomicfoundation/hardhat-ethers": "workspace:^3.0.0-next.3", + "@nomicfoundation/hardhat-ethers": "workspace:^4.0.0-next.3", "ethers": "^6.13.4" } } diff --git a/v-next/hardhat/templates/02-mocha-ethers/package.json b/v-next/hardhat/templates/02-mocha-ethers/package.json index 23ba913c71c..84e3a02039f 100644 --- a/v-next/hardhat/templates/02-mocha-ethers/package.json +++ b/v-next/hardhat/templates/02-mocha-ethers/package.json @@ -7,7 +7,7 @@ "devDependencies": { "hardhat": "workspace:^3.0.0-next.3", "@nomicfoundation/hardhat-ethers-chai-matchers": "workspace:^3.0.0-next.3", - "@nomicfoundation/hardhat-ethers": "workspace:^3.0.0-next.3", + "@nomicfoundation/hardhat-ethers": "workspace:^4.0.0-next.3", "@nomicfoundation/hardhat-ignition": "workspace:^3.0.0-next.3", "@nomicfoundation/hardhat-ignition-ethers": "workspace:^3.0.0-next.3", "@nomicfoundation/hardhat-keystore": "workspace:^3.0.0-next.3", From 43d46252d102f4cecc83579d5351bf788c95b976 Mon Sep 17 00:00:00 2001 From: galargh Date: Mon, 5 May 2025 14:40:59 +0100 Subject: [PATCH 10/27] chore: revert the hardhat-ethers version change --- v-next/hardhat-ethers/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/v-next/hardhat-ethers/CHANGELOG.md b/v-next/hardhat-ethers/CHANGELOG.md index 93c43dabb2b..413d304eff6 100644 --- a/v-next/hardhat-ethers/CHANGELOG.md +++ b/v-next/hardhat-ethers/CHANGELOG.md @@ -1,12 +1,12 @@ # @nomicfoundation/hardhat-ethers -## 3.0.0-next.2 +## 4.0.0-next.2 ### Patch Changes - Hardhat 3 Alpha release (2025-03-20T08:38:27.809Z) -## 3.0.0-next.1 +## 4.0.0-next.1 ### Patch Changes From 6ab7aa37797ae185b241c052834731da770417c4 Mon Sep 17 00:00:00 2001 From: galargh Date: Mon, 5 May 2025 14:47:30 +0100 Subject: [PATCH 11/27] ci: do not include hardhat-ether in the set of fixed packages --- .changeset/config.json | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/config.json b/.changeset/config.json index 06e4421d082..9402c94467e 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -15,7 +15,6 @@ [ "hardhat", "@nomicfoundation/hardhat-errors", - "@nomicfoundation/hardhat-ethers", "@nomicfoundation/hardhat-ethers-chai-matchers", "@nomicfoundation/hardhat-ignition", "@nomicfoundation/ignition-core", From 7545b1befa9e9bbeadb6e0b4cc0f10fe6da2e7a4 Mon Sep 17 00:00:00 2001 From: galargh Date: Mon, 5 May 2025 16:34:43 +0100 Subject: [PATCH 12/27] ci: use version-alpha script as the versioning script --- .changeset/config.json | 2 +- package.json | 2 +- scripts/version-alpha.mjs | 144 ++++++++++++-------------------------- 3 files changed, 47 insertions(+), 101 deletions(-) diff --git a/.changeset/config.json b/.changeset/config.json index 9402c94467e..4b8658602a8 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,6 +1,6 @@ { "$schema": "https://unpkg.com/@changesets/config@1.6.0/schema.json", - "changelog": "@changesets/cli/changelog", + "changelog": false, "commit": false, "linked": [], "access": "public", diff --git a/package.json b/package.json index 3b0162260c5..6b98b2e15e4 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "typescript": "~5.5.0" }, "scripts": { - "version": "changeset version && pnpm install --lockfile-only", + "version": "node scripts/version.mjs", "build": "pnpm run --recursive --if-present build", "clean": "pnpm run --recursive --if-present clean", "test": "pnpm run --recursive --if-present test", diff --git a/scripts/version-alpha.mjs b/scripts/version-alpha.mjs index a10e612161a..0750b697004 100644 --- a/scripts/version-alpha.mjs +++ b/scripts/version-alpha.mjs @@ -2,6 +2,7 @@ import { readdir, readFile, writeFile, stat } from "node:fs/promises"; import path from "node:path"; import { exec } from "node:child_process"; import { promisify } from "node:util"; +import { randomUUID } from "node:crypto"; const execAsync = promisify(exec); @@ -9,47 +10,41 @@ const changesetDir = ".changeset"; const packagesDir = "v-next"; /** - * This script applies changesets and creates a new Alpha version, - * based on changesets pre-release versioning. + * This script applies changesets based on changesets pre-release versioning. * * It reads all the new changesets and validates that: - * - they patch the main Hardhat package * - there are no major or minor changesets * * It then combines the new changesets to create a new changelog * section for the new version. * - * The next step is to create a release changeset that patches all of the - * packages (except Hardhat, which by definition is already covered). - * By applying a changeset to all packages we eliminate issues - * with peer depenendencies not being updated. + * The next step is to create a changeset for hardhat-ethers if it does not + * already exists. This is necessary because the hardhat-ethers package + * is one major version ahead of the rest of the packages so we cannot include + * it in the fixed packages set. * * The release changeset is then applied, bumping versions across * the packages (including the template packages). * * Finally the Hardhat packages changelog is updated with the * prepared changelog section. - * - * It is up to the user to commit and push the changes as a release - * branch. */ async function versionAlpha() { const changesets = await readAllNewChangsets(); validateChangesets(changesets); - const currentHardhatAlphaVersion = await readCurrentHardhatAlphaVersion(); - const nextHardhatAlphaVersion = incrementHardhatAlphaVersion( - currentHardhatAlphaVersion - ); - - await createAllPackageChangesetFor(nextHardhatAlphaVersion); + if (shouldCreateHardhatEthersPackageChangeset(changesets)) { + await createHardhatEthersPackageChangeset(); + } await executeChangesetVersion(); - await updateHardhatChangelog(nextHardhatAlphaVersion, changesets); + const hardhatVersion = await readHardhatVersion(); + + await updateHardhatChangelog(hardhatVersion, changesets); - printFollowupInstructions(nextHardhatAlphaVersion, changesets); + printFollowupInstructions(hardhatVersion, changesets); } /** @@ -95,25 +90,17 @@ async function readAllNewChangsets() { * changeset, logging and killing the script otherwise. * * The validations are: - * - every changeset must include a `"hardhat": patch` entry * - no major or minor changesets are allowed */ function validateChangesets(changesets) { if (changesets.length === 0) { - console.log("Error: No new changesets found."); - process.exit(1); + console.log("No new changesets found."); + process.exit(0); } let validationFailed = false; for (const { frontMatter, path: changesetPath } of changesets) { - if (!/^\s*"hardhat": patch$/m.test(frontMatter)) { - validationFailed = true; - console.log( - `Error: ${changesetPath}: No "hardhat: patch", every Alpha changeset must include hardhat` - ); - } - if (/: (major|minor)\s*$/m.test(frontMatter)) { validationFailed = true; console.log( @@ -130,7 +117,7 @@ function validateChangesets(changesets) { /** * Read the current Alpha version based on the hardhat package.json */ -async function readCurrentHardhatAlphaVersion() { +async function readHardhatVersion() { const hardhatPackageJson = JSON.parse( await readFile(path.join("v-next", "hardhat", "package.json")) ); @@ -139,47 +126,41 @@ async function readCurrentHardhatAlphaVersion() { } /** - * Increment the Alpha version by 1. We assume that the `next` - * tag is always used. + * Checks whether the hardhat-ethers package changeset should be created. */ -function incrementHardhatAlphaVersion(version) { - const match = version.match(/(\d+\.\d+\.\d+)-next\.(\d+)/); - - if (!match) { - console.log(`Unsupported version format: ${version}`); - process.exit(1); +function shouldCreateHardhatEthersPackageChangeset(changesets) { + if (changesets.length === 0) { + return false; } - const [, base, num] = match; - const nextNum = Number(num) + 1; + for (const { frontMatter } of changesets) { + if (/"@nomicfoundation\/hardhat-ethers": patch$/.test(frontMatter)) { + return false; + } + } - return `${base}-next.${nextNum}`; + return true; } /** - * Write a changeset file that has one entry for every package - * under `./v-next` excluding the hardhat package (this is - * covered definitionally because of the validation rules). + * Write a hardhat-ethers changeset file that has a patch entry for the package. */ -async function createAllPackageChangesetFor(nextHardhatAlphaVersion) { - const releaseChangesetPath = path.join( +async function createHardhatEthersPackageChangeset() { + const changesetPath = path.join( changesetDir, - `release-${nextHardhatAlphaVersion}.md` + `${randomUUID()}.md` ); - const packageNames = await readAllPackageNames(); + const packageName = '@nomicfoundation/hardhat-ethers'; - const releaseChangesetContent = `--- -${packageNames - .filter((name) => name !== "hardhat") - .map((name) => `"${name}": patch`) - .join("\n")} ---- - -Hardhat 3 Alpha release (${new Date().toISOString()}) -`; + const releaseChangesetContent = [ + '---', + `"${packageName}": patch`, + '---', + '', + ].join('\n'); - await writeFile(releaseChangesetPath, releaseChangesetContent); + await writeFile(changesetPath, releaseChangesetContent); } /** @@ -188,16 +169,16 @@ Hardhat 3 Alpha release (${new Date().toISOString()}) */ async function executeChangesetVersion() { await execAsync("pnpm changeset version"); - await execAsync("pnpm install"); + await execAsync("pnpm install --lockfile-only"); } /** * Prepend a new changelog section to the Hardhat package's * changelog based on the new changesets. */ -async function updateHardhatChangelog(nextHardhatAlphaVersion, changesets) { +async function updateHardhatChangelog(hardhatVersion, changesets) { const newChangelogSection = generateChangelogFrom( - nextHardhatAlphaVersion, + hardhatVersion, changesets ); @@ -217,54 +198,19 @@ async function updateHardhatChangelog(nextHardhatAlphaVersion, changesets) { await writeFile(hardhatChangelogPath, newChangelog); } -function printFollowupInstructions(nextHardhatAlphaVersion, changesets) { +function printFollowupInstructions(hardhatVersion, changesets) { console.log(` -# ${nextHardhatAlphaVersion} +# ${hardhatVersion} ${generateReleaseMessage(changesets)} `); } -async function readAllPackageNames() { - const ignoredChangesetPackages = JSON.parse( - await readFile(path.join(changesetDir, "config.json")) - ).ignore; - - const subdirs = await readdir(packagesDir); - - const packageNames = []; - - for (const dir of subdirs) { - const packageJsonPath = path.join(packagesDir, dir, "package.json"); - - try { - const stats = await stat(packageJsonPath); - - if (!stats.isFile()) { - continue; - } - - const pkgJson = JSON.parse(await readFile(packageJsonPath, "utf8")); - - if (ignoredChangesetPackages.includes(pkgJson.name)) { - continue; - } - - packageNames.push(pkgJson.name); - } catch (error) { - console.log(error); - process.exit(1); - } - } - - return packageNames.sort(); -} - -function generateChangelogFrom(nextHardhatAlphaVersion, changesets) { +function generateChangelogFrom(hardhatVersion, changesets) { return `# hardhat -## ${nextHardhatAlphaVersion} +## ${hardhatVersion} ### Patch Changes From 269ef94202e8d2037f6878c7a034ee243cda791d Mon Sep 17 00:00:00 2001 From: galargh Date: Mon, 5 May 2025 23:50:15 +0100 Subject: [PATCH 13/27] ci: update the changeset check --- .changeset/config.json | 1 + .github/workflows/check-changeset-added.yml | 82 ++++++++++++++------- .github/workflows/release.yml | 4 - scripts/version-alpha.mjs | 4 +- 4 files changed, 59 insertions(+), 32 deletions(-) diff --git a/.changeset/config.json b/.changeset/config.json index 4b8658602a8..51b03815b4b 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -7,6 +7,7 @@ "baseBranch": "v-next", "updateInternalDependencies": "patch", "ignore": [ + "@nomicfoundation/config", "@nomicfoundation/example-project", "@nomicfoundation/template-package", "template-*" diff --git a/.github/workflows/check-changeset-added.yml b/.github/workflows/check-changeset-added.yml index 299e5c19015..cf9d843131e 100644 --- a/.github/workflows/check-changeset-added.yml +++ b/.github/workflows/check-changeset-added.yml @@ -17,54 +17,82 @@ on: - unlabeled jobs: - check-if-changeset: - name: Check that PR has a changeset + merge-group: + if: github.event_name == 'merge_group' runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 - uses: actions/github-script@v7 with: script: | - const isMergeGroup = context.eventName === "merge_group"; + // NOTE: If we ever need to, we could access the PR number from the merge_group event like so: + // const headRef = context.payload.merge_group.head_ref; // E.g. refs/heads/gh-readonly-queue/master/pr-6-90b00e094e6de7fc4f3fac9a0087c01cf48acad8 + // const [, baseRefName, pullNumber, headSha] = headRef.match(/^refs\/heads\/gh-readonly-queue\/([^\/]+)\/pr-(\d+)-([a-z0-9]+)$/); - if (isMergeGroup) { - console.log("Ignore changeset check for merge group."); - return; - } + const fs = require('fs'); + const path = require('path'); + + const {changesets: knownChangesets} = JSON.parse(fs.readFileSync('./.changeset/pre.json', 'utf-8')); + const allChangesets = fs.readdirSync('./.changeset') + .filter(file => file.endsWith('.md')) + .map(file => file.replace('.md', '')); + const newChangests = allChangesets.filter(changeset => !knownChangesets.includes(changeset)); + + if (newChangests.length === 0) { + console.log('No new changesets found'); + return; + } + + const packages = fs.readdirSync('./v-next') + .filter(file => ['config', 'example-project', 'template-package'].includes(file)) + .map(file => `./v-next/${file}/package.json`) + .map(path => JSON.parse(fs.readFileSync(path, 'utf-8'))); + + for (const package of packages) { + const url = `https://registry.npmjs.org/${package.name}/${package.version}`; + const response = await fetch(url); + if (response.status !== 200) { + throw new Error(`Package ${package.name} is not released to npm`); + } + } + console.log('All packages are released to npm'); + return; + pull-request: + name: Check that PR has a changeset + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + with: + script: | const isReleasePr = process.env.GITHUB_HEAD_REF.startsWith("changeset-release/"); + if (isReleasePr) { console.log("Ignore changeset check for release PR."); return; } - // Single PR context - const pullNumber = context.issue.number; + const noChangesetNeeded = context.payload.pull_request.labels + .some(l => l.name === "no changeset needed"); + if (noChangesetNeeded) { + console.log("The PR is labeled as 'no changeset needed'"); + return; + } + + const pullNumber = context.payload.pull_request.number; const { data: files } = await github.rest.pulls.listFiles({ - ...context.issue, + ...context.repo, pull_number: pullNumber }); const changeset = files.find( file => file.status === "added" && file.filename.startsWith(".changeset/") ); - if (changeset !== undefined) { - console.log("Changeset found:", changeset.filename); - return; - } - - console.log("No changeset found"); - const { data: pull } = await github.rest.pulls.get({ - ...context.issue, - pull_number: pullNumber - }); - const noChangesetNeededLabel = pull.labels - .some(l => l.name === "no changeset needed"); - if (noChangesetNeededLabel) { - console.log('The PR is labeled as "no changeset needed"'); + if (changeset !== undefined) { + console.log(`Changeset found: ${changeset.filename}`); return; } - console.log('The PR is not labeled as "no changeset needed"'); - - process.exit(1); + throw new Error("No changeset found"); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e68ef38cf79..19f349dfb8b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,6 @@ name: Release on: - # TODO: When a release PR is merged we should run a merge_group check to ensure: - # 1. The release PR is the only one in the merge group - # 2. The release PR is up-to-date with the target branch - # i.e. no other PR has been merged since the release PR was added to the merge queue workflow_dispatch: push: branches: diff --git a/scripts/version-alpha.mjs b/scripts/version-alpha.mjs index 0750b697004..3b53230cbc1 100644 --- a/scripts/version-alpha.mjs +++ b/scripts/version-alpha.mjs @@ -1,4 +1,6 @@ -import { readdir, readFile, writeFile, stat } from "node:fs/promises"; +// @ts-check + +import { readdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { exec } from "node:child_process"; import { promisify } from "node:util"; From 2c99e918c4763df113e27de3914001c1307ec662 Mon Sep 17 00:00:00 2001 From: galargh Date: Tue, 6 May 2025 00:06:28 +0100 Subject: [PATCH 14/27] fix: the changeset workflow --- .github/workflows/check-changeset-added.yml | 87 +++++++++++---------- 1 file changed, 47 insertions(+), 40 deletions(-) diff --git a/.github/workflows/check-changeset-added.yml b/.github/workflows/check-changeset-added.yml index cf9d843131e..5e6905765c8 100644 --- a/.github/workflows/check-changeset-added.yml +++ b/.github/workflows/check-changeset-added.yml @@ -17,53 +17,60 @@ on: - unlabeled jobs: - merge-group: - if: github.event_name == 'merge_group' + check-if-changeset: + name: Check that PR has a changeset runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/github-script@v7 + - name: Checkout the repository + if: github.event_name == 'merge_group' + uses: actions/checkout@v4 + + - name: Check if merge group is valid + if: github.event_name == 'merge_group' + uses: actions/github-script@v7 with: + # NOTE: A merge group is valid if it contains no new changesets or all the + # versions of the packages are released to npm. The former is an indication + # that the merge group consists of a release PR and/or PRs that don't need + # a changeset. The latter is an indication that the merge group does not + # contain a release PR. script: | - // NOTE: If we ever need to, we could access the PR number from the merge_group event like so: - // const headRef = context.payload.merge_group.head_ref; // E.g. refs/heads/gh-readonly-queue/master/pr-6-90b00e094e6de7fc4f3fac9a0087c01cf48acad8 - // const [, baseRefName, pullNumber, headSha] = headRef.match(/^refs\/heads\/gh-readonly-queue\/([^\/]+)\/pr-(\d+)-([a-z0-9]+)$/); - - const fs = require('fs'); - const path = require('path'); - - const {changesets: knownChangesets} = JSON.parse(fs.readFileSync('./.changeset/pre.json', 'utf-8')); - const allChangesets = fs.readdirSync('./.changeset') - .filter(file => file.endsWith('.md')) - .map(file => file.replace('.md', '')); - const newChangests = allChangesets.filter(changeset => !knownChangesets.includes(changeset)); - - if (newChangests.length === 0) { - console.log('No new changesets found'); - return; - } + // NOTE: If we ever need to, we could access the PR number from the merge_group event like so: + // const headRef = context.payload.merge_group.head_ref; // E.g. refs/heads/gh-readonly-queue/master/pr-6-90b00e094e6de7fc4f3fac9a0087c01cf48acad8 + // const [, baseRefName, pullNumber, headSha] = headRef.match(/^refs\/heads\/gh-readonly-queue\/([^\/]+)\/pr-(\d+)-([a-z0-9]+)$/); - const packages = fs.readdirSync('./v-next') - .filter(file => ['config', 'example-project', 'template-package'].includes(file)) - .map(file => `./v-next/${file}/package.json`) - .map(path => JSON.parse(fs.readFileSync(path, 'utf-8'))); - - for (const package of packages) { - const url = `https://registry.npmjs.org/${package.name}/${package.version}`; - const response = await fetch(url); - if (response.status !== 200) { - throw new Error(`Package ${package.name} is not released to npm`); - } - } + const fs = require('fs'); + const path = require('path'); - console.log('All packages are released to npm'); + const {changesets: knownChangesets} = JSON.parse(fs.readFileSync('./.changeset/pre.json', 'utf-8')); + const allChangesets = fs.readdirSync('./.changeset') + .filter(file => file.endsWith('.md')) + .map(file => file.replace('.md', '')); + const newChangests = allChangesets.filter(changeset => !knownChangesets.includes(changeset)); + + if (newChangests.length === 0) { + console.log('No new changesets found'); return; - pull-request: - name: Check that PR has a changeset - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - steps: - - uses: actions/github-script@v7 + } + + const packages = fs.readdirSync('./v-next') + .filter(file => ['config', 'example-project', 'template-package'].includes(file)) + .map(file => `./v-next/${file}/package.json`) + .map(path => JSON.parse(fs.readFileSync(path, 'utf-8'))); + + for (const package of packages) { + const url = `https://registry.npmjs.org/${package.name}/${package.version}`; + const response = await fetch(url); + if (response.status !== 200) { + throw new Error(`Package ${package.name} is not released to npm`); + } + } + + console.log('All packages are released to npm'); + return; + - name: Check if PR has a changeset + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 with: script: | const isReleasePr = process.env.GITHUB_HEAD_REF.startsWith("changeset-release/"); From 1384c366fd4ed445223060540e1490c389dbd389 Mon Sep 17 00:00:00 2001 From: galargh Date: Tue, 6 May 2025 11:54:52 +0100 Subject: [PATCH 15/27] chore: rearrange the changeset check --- .github/workflows/check-changeset-added.yml | 78 ++------------------- scripts/lib/changesets.mjs | 71 +++++++++++++++++++ scripts/validate-merge-group.mjs | 68 ++++++++++++++++++ scripts/validate-pull-request.mjs | 69 ++++++++++++++++++ scripts/version-alpha.mjs | 67 ++---------------- 5 files changed, 216 insertions(+), 137 deletions(-) create mode 100644 scripts/lib/changesets.mjs create mode 100644 scripts/validate-merge-group.mjs create mode 100644 scripts/validate-pull-request.mjs diff --git a/.github/workflows/check-changeset-added.yml b/.github/workflows/check-changeset-added.yml index 5e6905765c8..68178104c4e 100644 --- a/.github/workflows/check-changeset-added.yml +++ b/.github/workflows/check-changeset-added.yml @@ -22,84 +22,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - if: github.event_name == 'merge_group' uses: actions/checkout@v4 - name: Check if merge group is valid if: github.event_name == 'merge_group' - uses: actions/github-script@v7 - with: - # NOTE: A merge group is valid if it contains no new changesets or all the - # versions of the packages are released to npm. The former is an indication - # that the merge group consists of a release PR and/or PRs that don't need - # a changeset. The latter is an indication that the merge group does not - # contain a release PR. - script: | - // NOTE: If we ever need to, we could access the PR number from the merge_group event like so: - // const headRef = context.payload.merge_group.head_ref; // E.g. refs/heads/gh-readonly-queue/master/pr-6-90b00e094e6de7fc4f3fac9a0087c01cf48acad8 - // const [, baseRefName, pullNumber, headSha] = headRef.match(/^refs\/heads\/gh-readonly-queue\/([^\/]+)\/pr-(\d+)-([a-z0-9]+)$/); - - const fs = require('fs'); - const path = require('path'); - - const {changesets: knownChangesets} = JSON.parse(fs.readFileSync('./.changeset/pre.json', 'utf-8')); - const allChangesets = fs.readdirSync('./.changeset') - .filter(file => file.endsWith('.md')) - .map(file => file.replace('.md', '')); - const newChangests = allChangesets.filter(changeset => !knownChangesets.includes(changeset)); - - if (newChangests.length === 0) { - console.log('No new changesets found'); - return; - } - - const packages = fs.readdirSync('./v-next') - .filter(file => ['config', 'example-project', 'template-package'].includes(file)) - .map(file => `./v-next/${file}/package.json`) - .map(path => JSON.parse(fs.readFileSync(path, 'utf-8'))); - - for (const package of packages) { - const url = `https://registry.npmjs.org/${package.name}/${package.version}`; - const response = await fetch(url); - if (response.status !== 200) { - throw new Error(`Package ${package.name} is not released to npm`); - } - } + run: node scripts/validate-merge-group.mjs - console.log('All packages are released to npm'); - return; - name: Check if PR has a changeset if: github.event_name == 'pull_request' - uses: actions/github-script@v7 - with: - script: | - const isReleasePr = process.env.GITHUB_HEAD_REF.startsWith("changeset-release/"); - - if (isReleasePr) { - console.log("Ignore changeset check for release PR."); - return; - } - - const noChangesetNeeded = context.payload.pull_request.labels - .some(l => l.name === "no changeset needed"); - - if (noChangesetNeeded) { - console.log("The PR is labeled as 'no changeset needed'"); - return; - } - - const pullNumber = context.payload.pull_request.number; - const { data: files } = await github.rest.pulls.listFiles({ - ...context.repo, - pull_number: pullNumber - }); - const changeset = files.find( - file => file.status === "added" && file.filename.startsWith(".changeset/") - ); - - if (changeset !== undefined) { - console.log(`Changeset found: ${changeset.filename}`); - return; - } - - throw new Error("No changeset found"); + env: + GITHUB_EVENT_PULL_REQUEST_LABELS: ${{ toJson(github.event.pull_request.labels) }} + run: node scripts/validate-pull-request.mjs diff --git a/scripts/lib/changesets.mjs b/scripts/lib/changesets.mjs new file mode 100644 index 00000000000..b7e73a96a70 --- /dev/null +++ b/scripts/lib/changesets.mjs @@ -0,0 +1,71 @@ +// @ts-check + +import { exec as execSync } from "node:child_process"; +import { readdir, readFile } from "node:fs/promises"; +import path from "node:path"; +import { promisify } from "node:util"; + +const exec = promisify(execSync); + +const changesetDir = ".changeset"; + +/** + * Read all the changesets that have not yet been applied + * based on the pre.json file. + */ +export async function readAllNewChangsets() { + const allChangesetNames = (await readdir(changesetDir)) + .filter((file) => file.endsWith(".md")) + .map((file) => file.slice(0, -3)); + + const alreadyAppliedChangesetNames = JSON.parse( + (await readFile(path.join(changesetDir, "pre.json"))).toString() + ); + + const newChangesetNames = allChangesetNames.filter( + (name) => !alreadyAppliedChangesetNames.changesets.includes(name) + ); + + const changesets = []; + + for (const newChangeSetName of newChangesetNames) { + const changesetFilePath = path.join(changesetDir, `${newChangeSetName}.md`); + + const changesetContent = await readFile(changesetFilePath, "utf-8"); + + const { content, frontMatter } = parseFrontMatter(changesetContent); + const commitHash = await getAddingCommit(changesetFilePath); + + changesets.push({ + frontMatter, + content, + path: changesetFilePath, + commitHash, + }); + } + + return changesets; +} + +function parseFrontMatter(markdown) { + const match = markdown.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); + if (!match) { + return { frontMatter: null, content: markdown }; + } + + return { + frontMatter: match[1], + content: match[2], + }; +} + +async function getAddingCommit(filePath) { + try { + const { stdout } = await exec( + `git log --diff-filter=A --follow --format=%h -- "${filePath}"` + ); + return stdout.trim() || null; + } catch { + return null; + } +} diff --git a/scripts/validate-merge-group.mjs b/scripts/validate-merge-group.mjs new file mode 100644 index 00000000000..3c8fc8295e5 --- /dev/null +++ b/scripts/validate-merge-group.mjs @@ -0,0 +1,68 @@ +// @ts-check + +/** + * A merge group is valid if it contains no new changesets or all the + * currently checked-in versions of the packages are released to npm. + * The former is an indicationthat the merge group consists of a release PR + * and/or PRs that don't needa changeset. The latter is an indication that the + * merge group does notcontain a release PR. + */ + +import { readdir, readFile } from "node:fs/promises"; + +import { readAllNewChangsets } from './lib/changesets.mjs'; + +const packagesDir = "v-next"; + +// NOTE: This function is currently unused but it could be useful to preserve +// the information about how to do it. +function getPullNumber(headRef) { + // The head ref (github.event.merge_group.head_ref) is of the form: + // refs/heads/gh-readonly-queue/master/pr-6-90b00e094e6de7fc4f3fac9a0087c01cf48acad8 + const [, _baseRefName, pullNumber, _headSha] = headRef.match( + /^refs\/heads\/gh-readonly-queue\/([^\/]+)\/pr-(\d+)-([a-z0-9]+)$/, + ); + return parseInt(pullNumber, 10); +} + +/** + * Read all the package.json files of the packages that we release to npm. + */ +async function readAllReleasedPackages() { + const allPackageNames = (await readdir(packagesDir)) + .filter(file => ['config', 'example-project', 'template-package'].includes(file)); + + const allPackages = allPackageNames + .map(file => `./v-next/${file}/package.json`) + .map(async (path) => JSON.parse(await readFile(path, 'utf-8'))); + + return Promise.all(allPackages); +} + +async function isPackageReleasedToNpm(pkg) { + const url = `https://registry.npmjs.org/${pkg.name}/${pkg.version}`; + const response = await fetch(url); + return response.status === 200; +} + +async function validateMergeGroup() { + const changesets = await readAllNewChangsets(); + + if (changesets.length === 0) { + console.log('No new changesets found'); + return; + } + + const packages = await readAllReleasedPackages(); + + for (const pkg of packages) { + if (!(await isPackageReleasedToNpm(pkg))) { + throw new Error(`Package ${pkg.name} is not released to npm`); + } + } + + console.log('All packages are released to npm'); + return; +} + +await validateMergeGroup(); diff --git a/scripts/validate-pull-request.mjs b/scripts/validate-pull-request.mjs new file mode 100644 index 00000000000..4363ae31e5f --- /dev/null +++ b/scripts/validate-pull-request.mjs @@ -0,0 +1,69 @@ +// @ts-check + +/** + * A pull request is valid if: + * - it is a release PR (i.e. it's head ref starts with "changeset-release/") + * - or it is labeled as "no changeset needed" + * - or it has a changeset. + */ + +import { exec as execSync } from "node:child_process"; +import { promisify } from "node:util"; + +const exec = promisify(execSync); + +const changesetDir = ".changeset"; + +function isReleasePR() { + if (process.env.GITHUB_HEAD_REF === undefined) { + throw new Error("GITHUB_HEAD_REF is not defined"); + } + + return process.env.GITHUB_HEAD_REF.startsWith("changeset-release/"); +} + +function hasNoChangesetNeededLabel() { + if (process.env.GITHUB_EVENT_PULL_REQUEST_LABELS === undefined) { + throw new Error("GITHUB_EVENT_PULL_REQUEST_LABELS is not defined"); + } + + const labels = JSON.parse(process.env.GITHUB_EVENT_PULL_REQUEST_LABELS); + + return labels.some(l => l.name === "no changeset needed"); +} + +async function hasChangeset() { + if (process.env.GITHUB_BASE_REF === undefined) { + throw new Error("GITHUB_BASE_REF is not defined"); + } + + const { stdout } = await exec( + `git diff --name-only --diff-filter=A ${process.env.GITHUB_BASE_REF} -- ${changesetDir}` + ); + + const changesets = stdout.trim().split("\n") + .filter((file) => file.endsWith(".md")); + + return changesets.length > 0; +} + +async function validatePullRequest() { + if (isReleasePR()) { + console.log("Ignore changeset check for release PR."); + return; + } + + if (await hasNoChangesetNeededLabel()) { + console.log("The PR is labeled as 'no changeset needed'"); + return; + } + + if (await hasChangeset()) { + console.log("Changeset found"); + return; + } + + throw new Error("No changeset found"); +} + +await validatePullRequest(); diff --git a/scripts/version-alpha.mjs b/scripts/version-alpha.mjs index 3b53230cbc1..585718115f2 100644 --- a/scripts/version-alpha.mjs +++ b/scripts/version-alpha.mjs @@ -1,11 +1,13 @@ // @ts-check -import { readdir, readFile, writeFile } from "node:fs/promises"; +import { readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { exec } from "node:child_process"; import { promisify } from "node:util"; import { randomUUID } from "node:crypto"; +import { readAllNewChangsets } from "./lib/changesets.mjs"; + const execAsync = promisify(exec); const changesetDir = ".changeset"; @@ -49,44 +51,6 @@ async function versionAlpha() { printFollowupInstructions(hardhatVersion, changesets); } -/** - * Read all the changesets that have not yet been applied - * based on the pre.json file. - */ -async function readAllNewChangsets() { - const allChangesetNames = (await readdir(changesetDir)) - .filter((file) => file.endsWith(".md")) - .map((file) => file.slice(0, -3)); - - const alreadyAppliedChangesetNames = JSON.parse( - await readFile(path.join(changesetDir, "pre.json")) - ); - - const newChangesetNames = allChangesetNames.filter( - (name) => !alreadyAppliedChangesetNames.changesets.includes(name) - ); - - const changesets = []; - - for (const newChangeSetName of newChangesetNames) { - const changesetFilePath = path.join(changesetDir, `${newChangeSetName}.md`); - - const changesetContent = await readFile(changesetFilePath, "utf-8"); - - const { content, frontMatter } = parseFrontMatter(changesetContent); - const commitHash = await getAddingCommit(changesetFilePath); - - changesets.push({ - frontMatter, - content, - path: changesetFilePath, - commitHash, - }); - } - - return changesets; -} - /** * Validate that the changesets meet our rules for an Alpha release * changeset, logging and killing the script otherwise. @@ -121,7 +85,7 @@ function validateChangesets(changesets) { */ async function readHardhatVersion() { const hardhatPackageJson = JSON.parse( - await readFile(path.join("v-next", "hardhat", "package.json")) + (await readFile(path.join("v-next", "hardhat", "package.json"))).toString() ); return hardhatPackageJson.version; @@ -250,27 +214,4 @@ function generateChangesTextFrom(changesets) { .join("\n"); } -function parseFrontMatter(markdown) { - const match = markdown.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); - if (!match) { - return { frontMatter: null, content: markdown }; - } - - return { - frontMatter: match[1], - content: match[2], - }; -} - -async function getAddingCommit(filePath) { - try { - const { stdout } = await execAsync( - `git log --diff-filter=A --follow --format=%h -- "${filePath}"` - ); - return stdout.trim() || null; - } catch { - return null; - } -} - await versionAlpha(); From 90febd188233310a26255af85eeb17bd67facad8 Mon Sep 17 00:00:00 2001 From: galargh Date: Tue, 6 May 2025 12:36:41 +0100 Subject: [PATCH 16/27] chore: move release logic to a script --- .github/workflows/release.yml | 139 +++-------------------------- package.json | 1 - scripts/prepare-github-release.mjs | 117 ++++++++++++++++++++++++ scripts/version-alpha.mjs | 24 ----- 4 files changed, 127 insertions(+), 154 deletions(-) create mode 100644 scripts/prepare-github-release.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 19f349dfb8b..b55a057d446 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,7 @@ jobs: GITHUB_TOKEN: ${{ github.token }} uses: changesets/action@v1 with: - version: pnpm run version + version: node scripts/version-alpha.mjs - name: Build All Packages if: steps.pr.outputs.hasChangesets == 'false' @@ -60,144 +60,25 @@ jobs: pnpm publish --filter "./v-next/**" -r --no-git-checks --tag next --access public | tee -a $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - - name: Check if hardhat was published - id: hardhat + - name: Prepare GitHub release + id: release if: steps.pr.outputs.hasChangesets == 'false' env: - STDOUT: ${{ steps.publish.outputs.stdout }} - uses: actions/github-script@v7 - with: - script: | - const lines = process.env.STDOUT.split('\n') - - core.info('Checking if Hardhat was published') - - const line = lines.find(line => line.startsWith('+ hardhat@')) - - if (line === undefined) { - core.info('Hardhat was not published') - core.setOutput('version', '') - process.exit(0) - } - - const version = line.split('@')[1] - core.info(`Hardhat was published with version ${version}`) - core.setOutput('version', version) - - - name: Check the version of the published package - id: version - if: steps.hardhat.outputs.version != '' - env: - VERSION: ${{ steps.hardhat.outputs.version }} - uses: actions/github-script@v7 - with: - script: | - const version = process.env.VERSION - - core.info('Checking if Hardhat was published as a prerelease') - - // NOTE: This check would mark non-prerelease versions with - in the build tag as prereleases - const prerelease = version.includes('-') - if (prerelease) { - core.info('Hardhat was published as a prerelease') - core.setOutput('prerelease', true) - } else { - core.info('Hardhat was not published as a prerelease') - core.setOutput('prerelease', false) - } - - core.info('Checking if this is the latest version') - - if (prerelease) { - core.info('This is a prerelease, so this is not the latest version') - core.setOutput('latest', false) - process.exit(0) - } - - try { - const { data: release } = await github.rest.repos.getLatestRelease(context.repo) - - const latestVersion = release.tag_name.split('@')[1] - core.info(`Current latest version is ${latestVersion}`) - - const [major, minor, patch] = version.split('.') - const [latestMajor, latestMinor, latestPatch] = latestVersion.split('.') - - if (parseInt(major, 10) > parseInt(latestMajor, 10) || - parseInt(minor, 10) > parseInt(latestMinor, 10) || - parseInt(patch, 10) > parseInt(latestPatch, 10)) { - core.info('This is a new latest version') - core.setOutput('latest', true) - } else { - core.info('This is not a new latest version') - core.setOutput('latest', false) - } - } catch (error) { - if (error.status === 404) { - core.info('No existing latest release found, so this is the latest version') - core.setOutput('latest', true) - process.exit(0) - } - throw error - } - - - name: Find the relevant changelog entry - id: changelog - if: steps.hardhat.outputs.version != '' - env: - VERSION: ${{ steps.hardhat.outputs.version }} - uses: actions/github-script@v7 - with: - script: | - const fs = require('fs') - - const changelog = fs.readFileSync('./v-next/hardhat/CHANGELOG.md').toString() - core.debug(`Changelog: ${changelog}`) - - core.info('Parsing changelog...') - const lines = changelog.split('\n') - const headerIndex = lines.findIndex((line) => line == `## ${process.env.VERSION}`) - - if (headerIndex == -1) { - core.error(`Changelog entry for version ${process.env.VERSION} not found in ./v-next/hardhat/CHANGELOG.md`) - process.exit(1) - } - - const entryLines = []; - - for (const line of lines.slice(headerIndex + 1)) { - if (line.startsWith('## ')) { - break - } - entryLines.push(line) - } - - const entry = entryLines.join('\n').trim() - - core.debug(`Entry: ${entry}`) - core.setOutput('entry', entry) + STEPS_PUBLISH_OUTPUTS_STDOUT: ${{ steps.publish.outputs.stdout }} + run: node scripts/prepare-github-release.mjs - name: Create GitHub Release - if: steps.hardhat.outputs.version != '' + if: steps.release.outputs.published == 'true' env: GITHUB_TOKEN: ${{ github.token }} # NOTE: The action updates the release if it already exists uses: galargh/action-gh-release@571276229e7c9e6ea18f99bad24122a4c3ec813f # https://github.com/galargh/action-gh-release/pull/1 with: draft: false - tag_name: hardhat@${{ steps.hardhat.outputs.version }} + tag_name: hardhat@${{ steps.release.outputs.version }} generate_release_notes: false target_commitish: ${{ github.sha }} - make_latest: ${{ steps.version.outputs.latest == 'true' }} - prerelease: ${{ steps.version.outputs.prerelease == 'true' }} - body: | - # ${{ steps.hardhat.outputs.version }} - - ## Changes - - ${{ steps.changelog.outputs.entry }} - - --- - > 💡 **The Nomic Foundation is hiring! Check [our open positions](https://www.nomic.foundation/jobs).** - --- + make_latest: ${{ steps.release.outputs.latest == 'true' }} + prerelease: ${{ steps.release.outputs.prerelease == 'true' }} + body: ${{ steps.release.outputs.body }} token: ${{ github.token }} diff --git a/package.json b/package.json index 6b98b2e15e4..8ed954468a9 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,6 @@ "typescript": "~5.5.0" }, "scripts": { - "version": "node scripts/version.mjs", "build": "pnpm run --recursive --if-present build", "clean": "pnpm run --recursive --if-present clean", "test": "pnpm run --recursive --if-present test", diff --git a/scripts/prepare-github-release.mjs b/scripts/prepare-github-release.mjs new file mode 100644 index 00000000000..af15d285f28 --- /dev/null +++ b/scripts/prepare-github-release.mjs @@ -0,0 +1,117 @@ +import { appendFile } from "node:fs/promises"; + +// The regex was taken from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string +const semverRegex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; + +function wasHardhatPublished() { + if (process.env.STEPS_PUBLISH_OUTPUTS_STDOUT === undefined) { + throw new Error("STEPS_PUBLISH_OUTPUTS_STDOUT is not defined"); + } + + const lines = process.env.STEPS_PUBLISH_OUTPUTS_STDOUT.split("\n"); + + return lines.some((line) => line.startsWith("+ hardhat@")); +} + +async function getHardhatPackageVersion() { + const pkg = await readFile("v-next/hardhat/package.json", "utf-8"); + const { version } = JSON.parse(pkg); + return version; +} + +async function getHardhatNpmVersion() { + const url = `https://registry.npmjs.org/hardhat/latest`; + const response = await fetch(url); + if (response.status !== 200) { + throw new Error(`Failed to fetch ${url}: ${response.statusText}`); + } + const json = await response.json(); + return json.version; +} + +function isPrerelease(version) { + const [, , , , prerelease] = version.match(semverRegex); + return prerelease !== undefined; +} + +async function isLatest(version) { + const npm = await getHardhatNpmVersion(); + return version === npm; +} + +async function getHardhatChangelogEntry(version) { + const changelog = await readFile("v-next/hardhat/CHANGELOG.md", "utf-8"); + + const lines = changelog.split('\n') + const headerIndex = lines.findIndex((line) => line == `## ${version}`) + + if (headerIndex == -1) { + throw new Error(`Changelog entry for version ${version} not found in ./v-next/hardhat/CHANGELOG.md`) + } + + const entryLines = []; + + for (const line of lines.slice(headerIndex + 1)) { + if (line.startsWith('## ')) { + break + } + if (line === '### Patch Changes') { + entryLines.push('### Changes'); + } else { + entryLines.push(line) + } + } + + return entryLines.join('\n').trim() +} + +async function getReleaseBody(version) { + const lines = [ + "This Hardhat 3 Alpha release [short summary of the changes].", + "", + await getHardhatChangelogEntry(version), + "", + "---", + "> 💡 **The Nomic Foundation is hiring! Check [our open positions](https://www.nomic.foundation/jobs).**", + "---", + ]; + return lines.join("\n"); +} + +async function prepareGitHubRelease() { + if (process.env.GITHUB_OUTPUT === undefined) { + throw new Error("GITHUB_OUTPUT is not defined"); + } + + const published = wasHardhatPublished(); + console.log(`published: ${published}`); + + await appendFile(process.env.GITHUB_OUTPUT, `published=${published}\n`); + + if (!published) { + return; + } + + const version = await getHardhatPackageVersion(); + console.log(`version: ${version}`); + + const prerelease = isPrerelease(version); + console.log(`prerelease: ${prerelease}`); + + const latest = await isLatest(version); + console.log(`latest: ${latest}`); + + const releaseBody = await getReleaseBody(version); + console.log(`releaseBody: ${releaseBody}`); + + await appendFile(process.env.GITHUB_OUTPUT, `version=${version}\n`) + await appendFile(process.env.GITHUB_OUTPUT, `prerelease=${prerelease}\n`); + await appendFile(process.env.GITHUB_OUTPUT, `latest=${latest}\n`); + await appendFile(process.env.GITHUB_OUTPUT, [ + "body< 💡 **The Nomic Foundation is hiring! Check [our open positions](https://www.nomic.foundation/jobs).** ---- -`; -} - function generateChangesTextFrom(changesets) { return changesets .map(({ content, commitHash }) => From 5366fbf1800b481d0c9b93d6fb3243b4f8d3d2f9 Mon Sep 17 00:00:00 2001 From: galargh Date: Tue, 6 May 2025 16:44:14 +0100 Subject: [PATCH 17/27] ci: fetch base ref --- .github/workflows/check-changeset-added.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/check-changeset-added.yml b/.github/workflows/check-changeset-added.yml index 68178104c4e..e3329ec6ea2 100644 --- a/.github/workflows/check-changeset-added.yml +++ b/.github/workflows/check-changeset-added.yml @@ -24,6 +24,8 @@ jobs: - name: Checkout the repository uses: actions/checkout@v4 + - run: git fetch origin "$GITHUB_BASE_REF":"$GITHUB_BASE_REF" + - name: Check if merge group is valid if: github.event_name == 'merge_group' run: node scripts/validate-merge-group.mjs From d0382d586db0b39d0b156d5f0468091fe77d931a Mon Sep 17 00:00:00 2001 From: galargh Date: Tue, 6 May 2025 17:04:15 +0100 Subject: [PATCH 18/27] ci: fix the merge group validation --- scripts/validate-merge-group.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/validate-merge-group.mjs b/scripts/validate-merge-group.mjs index 3c8fc8295e5..0d1a943578d 100644 --- a/scripts/validate-merge-group.mjs +++ b/scripts/validate-merge-group.mjs @@ -30,7 +30,7 @@ function getPullNumber(headRef) { */ async function readAllReleasedPackages() { const allPackageNames = (await readdir(packagesDir)) - .filter(file => ['config', 'example-project', 'template-package'].includes(file)); + .filter(file => !['config', 'example-project', 'template-package', 'hardhat-test-utils'].includes(file)); const allPackages = allPackageNames .map(file => `./v-next/${file}/package.json`) From 4d9a91511e81e3c0d85abfd4e8e549a55d4e4126 Mon Sep 17 00:00:00 2001 From: galargh Date: Wed, 7 May 2025 09:36:53 +0100 Subject: [PATCH 19/27] ci: accept release token from secrets --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b55a057d446..41dee215814 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,7 +34,7 @@ jobs: id: pr env: # NOTE: If we use the GITHUB_TOKEN to create the release PR, the checks will not be triggered automatically - GITHUB_TOKEN: ${{ github.token }} + GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN || github.token }} uses: changesets/action@v1 with: version: node scripts/version-alpha.mjs From 5fe4b0ddeb3b3f6eb12ec0357551ff21e1e033a9 Mon Sep 17 00:00:00 2001 From: galargh Date: Wed, 7 May 2025 11:04:55 +0100 Subject: [PATCH 20/27] ci: create draft releases only --- .github/workflows/release.yml | 39 +++++++++-------- scripts/check-release.mjs | 20 +++++++++ scripts/lib/packages.mjs | 35 ++++++++++++++++ scripts/prepare-github-release.mjs | 67 +++++++----------------------- scripts/validate-merge-group.mjs | 29 ++----------- 5 files changed, 94 insertions(+), 96 deletions(-) create mode 100644 scripts/check-release.mjs create mode 100644 scripts/lib/packages.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 41dee215814..c4b022b2fe6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,8 @@ jobs: with: # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits fetch-depth: 0 + # NOTE: If we use the GITHUB_TOKEN to create the release PR, the checks on the release PR will not be triggered automatically + token: ${{ secrets.RELEASE_GITHUB_TOKEN || github.token }} - name: Set up the environment uses: ./.github/actions/setup-env @@ -30,51 +32,52 @@ jobs: - name: Install Dependencies run: pnpm install --frozen-lockfile --prefer-offline - - name: Create Release Pull Request + - name: Create release Pull Request id: pr env: - # NOTE: If we use the GITHUB_TOKEN to create the release PR, the checks will not be triggered automatically - GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN || github.token }} + GITHUB_TOKEN: ${{ github.token }} uses: changesets/action@v1 with: version: node scripts/version-alpha.mjs - - name: Build All Packages + - name: Check if release needs to be published + id: before if: steps.pr.outputs.hasChangesets == 'false' + run: node scripts/check-release.mjs + + - name: Build All Packages + if: steps.before.outputs.released == 'false' run: pnpm run --recursive -no-bail --filter './v-next/**' --if-present build - name: Publish All Packages (dry-run) - if: steps.pr.outputs.hasChangesets == 'false' + if: steps.before.outputs.released == 'false' run: pnpm publish --filter "./v-next/**" -r --no-git-checks --tag next --access public --dry-run - # NOTE: When running publish on an already published package we expect it to succeed but not to release anything and not include + hardhat@ in the output. - # If it does, however, include + hardhat@ in the output, it's OK, because we'll just update the release without moving the release tag forward. - name: Publish All Packages id: publish - if: steps.pr.outputs.hasChangesets == 'false' + if: steps.before.outputs.released == 'false' env: NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} NPM_CONFIG_PROVENANCE: true - run: | - echo "stdout<> $GITHUB_OUTPUT - pnpm publish --filter "./v-next/**" -r --no-git-checks --tag next --access public | tee -a $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + run: pnpm publish --filter "./v-next/**" -r --no-git-checks --tag next --access public + + - name: Check if release was published + id: after + if: steps.before.outputs.released == 'false' + run: node scripts/check-release.mjs - name: Prepare GitHub release id: release - if: steps.pr.outputs.hasChangesets == 'false' - env: - STEPS_PUBLISH_OUTPUTS_STDOUT: ${{ steps.publish.outputs.stdout }} + if: steps.before.outputs.released == 'false' && steps.after.outputs.released == 'true' run: node scripts/prepare-github-release.mjs - name: Create GitHub Release - if: steps.release.outputs.published == 'true' + if: steps.before.outputs.released == 'false' && steps.after.outputs.released == 'true' env: GITHUB_TOKEN: ${{ github.token }} - # NOTE: The action updates the release if it already exists uses: galargh/action-gh-release@571276229e7c9e6ea18f99bad24122a4c3ec813f # https://github.com/galargh/action-gh-release/pull/1 with: - draft: false + draft: true tag_name: hardhat@${{ steps.release.outputs.version }} generate_release_notes: false target_commitish: ${{ github.sha }} diff --git a/scripts/check-release.mjs b/scripts/check-release.mjs new file mode 100644 index 00000000000..f397f578926 --- /dev/null +++ b/scripts/check-release.mjs @@ -0,0 +1,20 @@ +// @ts-check + +import { appendFile } from "node:fs/promises"; + +import { readPackage, isPackageReleasedToNpm } from "./lib/packages.mjs"; + +async function checkRelease() { + if (process.env.GITHUB_OUTPUT === undefined) { + throw new Error("GITHUB_OUTPUT is not defined"); + } + + const hardhat = await readPackage("hardhat"); + const released = await isPackageReleasedToNpm(hardhat.name, hardhat.version); + + console.log(`released: ${released}`); + + await appendFile(process.env.GITHUB_OUTPUT, `released=${released}\n`); +} + +await checkRelease(); diff --git a/scripts/lib/packages.mjs b/scripts/lib/packages.mjs new file mode 100644 index 00000000000..a85fa307617 --- /dev/null +++ b/scripts/lib/packages.mjs @@ -0,0 +1,35 @@ +// @ts-check + +import { readdir, readFile } from "node:fs/promises"; + +const packagesDir = "v-next"; + +/** + * Read all the package.json files of the packages that we release to npm. + */ +export async function readAllReleasablePackages() { + const allPackageNames = (await readdir(packagesDir)) + .filter(file => !['config', 'example-project', 'template-package', 'hardhat-test-utils'].includes(file)); + + return Promise.all(allPackageNames.map(readPackage)); +} + +export async function readPackage(name) { + return JSON.parse(await readFile(`./v-next/${name}/package.json`, 'utf-8')); +} + +export async function getLatestPackageVersionFromNpm(name) { + const url = `https://registry.npmjs.org/${name}/latest`; + const response = await fetch(url); + if (response.status !== 200) { + throw new Error(`Failed to fetch ${url}: ${response.statusText}`); + } + const json = await response.json(); + return json.version; +} + +export async function isPackageReleasedToNpm(name, version) { + const url = `https://registry.npmjs.org/${name}/${version}`; + const response = await fetch(url); + return response.status === 200; +} diff --git a/scripts/prepare-github-release.mjs b/scripts/prepare-github-release.mjs index af15d285f28..ccf1d7aa6fc 100644 --- a/scripts/prepare-github-release.mjs +++ b/scripts/prepare-github-release.mjs @@ -1,42 +1,21 @@ +// @ts-check + import { appendFile } from "node:fs/promises"; +import { readFile } from "node:fs/promises"; + +import { getLatestPackageVersionFromNpm, readPackage } from "./lib/packages.mjs"; // The regex was taken from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string const semverRegex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; -function wasHardhatPublished() { - if (process.env.STEPS_PUBLISH_OUTPUTS_STDOUT === undefined) { - throw new Error("STEPS_PUBLISH_OUTPUTS_STDOUT is not defined"); - } - - const lines = process.env.STEPS_PUBLISH_OUTPUTS_STDOUT.split("\n"); - - return lines.some((line) => line.startsWith("+ hardhat@")); -} - -async function getHardhatPackageVersion() { - const pkg = await readFile("v-next/hardhat/package.json", "utf-8"); - const { version } = JSON.parse(pkg); - return version; -} - -async function getHardhatNpmVersion() { - const url = `https://registry.npmjs.org/hardhat/latest`; - const response = await fetch(url); - if (response.status !== 200) { - throw new Error(`Failed to fetch ${url}: ${response.statusText}`); - } - const json = await response.json(); - return json.version; -} - function isPrerelease(version) { const [, , , , prerelease] = version.match(semverRegex); return prerelease !== undefined; } async function isLatest(version) { - const npm = await getHardhatNpmVersion(); - return version === npm; + const latestVersion = await getLatestPackageVersionFromNpm("hardhat"); + return version === latestVersion; } async function getHardhatChangelogEntry(version) { @@ -83,35 +62,19 @@ async function prepareGitHubRelease() { throw new Error("GITHUB_OUTPUT is not defined"); } - const published = wasHardhatPublished(); - console.log(`published: ${published}`); - - await appendFile(process.env.GITHUB_OUTPUT, `published=${published}\n`); - - if (!published) { - return; - } - - const version = await getHardhatPackageVersion(); - console.log(`version: ${version}`); + const hardhat = await readPackage("hardhat"); - const prerelease = isPrerelease(version); + const prerelease = isPrerelease(hardhat.version); console.log(`prerelease: ${prerelease}`); + await appendFile(process.env.GITHUB_OUTPUT, `prerelease=${prerelease}\n`); - const latest = await isLatest(version); + const latest = await isLatest(hardhat.version); console.log(`latest: ${latest}`); - - const releaseBody = await getReleaseBody(version); - console.log(`releaseBody: ${releaseBody}`); - - await appendFile(process.env.GITHUB_OUTPUT, `version=${version}\n`) - await appendFile(process.env.GITHUB_OUTPUT, `prerelease=${prerelease}\n`); await appendFile(process.env.GITHUB_OUTPUT, `latest=${latest}\n`); - await appendFile(process.env.GITHUB_OUTPUT, [ - "body< !['config', 'example-project', 'template-package', 'hardhat-test-utils'].includes(file)); - - const allPackages = allPackageNames - .map(file => `./v-next/${file}/package.json`) - .map(async (path) => JSON.parse(await readFile(path, 'utf-8'))); - - return Promise.all(allPackages); -} - -async function isPackageReleasedToNpm(pkg) { - const url = `https://registry.npmjs.org/${pkg.name}/${pkg.version}`; - const response = await fetch(url); - return response.status === 200; -} - async function validateMergeGroup() { const changesets = await readAllNewChangsets(); @@ -53,10 +30,10 @@ async function validateMergeGroup() { return; } - const packages = await readAllReleasedPackages(); + const packages = await readAllReleasablePackages(); for (const pkg of packages) { - if (!(await isPackageReleasedToNpm(pkg))) { + if (!(await isPackageReleasedToNpm(pkg.name, pkg.version))) { throw new Error(`Package ${pkg.name} is not released to npm`); } } From 8d0921f9f84c2a232ad436950c5a1cfd83bcd810 Mon Sep 17 00:00:00 2001 From: galargh Date: Wed, 7 May 2025 11:11:46 +0100 Subject: [PATCH 21/27] ci: include version in the github release prepare output --- scripts/prepare-github-release.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/prepare-github-release.mjs b/scripts/prepare-github-release.mjs index ccf1d7aa6fc..bdd8442090a 100644 --- a/scripts/prepare-github-release.mjs +++ b/scripts/prepare-github-release.mjs @@ -64,6 +64,10 @@ async function prepareGitHubRelease() { const hardhat = await readPackage("hardhat"); + const version = hardhat.version; + console.log(`version: ${version}`); + await appendFile(process.env.GITHUB_OUTPUT, `version=${version}\n`); + const prerelease = isPrerelease(hardhat.version); console.log(`prerelease: ${prerelease}`); await appendFile(process.env.GITHUB_OUTPUT, `prerelease=${prerelease}\n`); From af53084ca8b4f2caa39fe571c248306566ec3727 Mon Sep 17 00:00:00 2001 From: galargh Date: Wed, 7 May 2025 11:25:19 +0100 Subject: [PATCH 22/27] ci: use github-api to create the pull request --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c4b022b2fe6..28c86000c1c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,8 +23,6 @@ jobs: with: # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits fetch-depth: 0 - # NOTE: If we use the GITHUB_TOKEN to create the release PR, the checks on the release PR will not be triggered automatically - token: ${{ secrets.RELEASE_GITHUB_TOKEN || github.token }} - name: Set up the environment uses: ./.github/actions/setup-env @@ -35,10 +33,12 @@ jobs: - name: Create release Pull Request id: pr env: - GITHUB_TOKEN: ${{ github.token }} + # NOTE: If we use the GITHUB_TOKEN to create the release PR, the checks on the release PR will not be triggered automatically + GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN || github.token }} uses: changesets/action@v1 with: version: node scripts/version-alpha.mjs + commitMode: ${{ secrets.RELEASE_GITHUB_TOKEN && 'github-api' || 'git-cli' }} - name: Check if release needs to be published id: before From 86d3f40ad9ec86ffc001caf7313f4aae0af4d933 Mon Sep 17 00:00:00 2001 From: galargh Date: Wed, 7 May 2025 11:39:47 +0100 Subject: [PATCH 23/27] ci: use pat in both places --- .github/workflows/release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 28c86000c1c..834891c62af 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,8 @@ jobs: with: # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits fetch-depth: 0 + # NOTE: If we use the GITHUB_TOKEN to create the release PR, the checks on the release PR will not be triggered automatically + token: ${{ secrets.RELEASE_GITHUB_TOKEN || github.token }} - name: Set up the environment uses: ./.github/actions/setup-env @@ -38,7 +40,6 @@ jobs: uses: changesets/action@v1 with: version: node scripts/version-alpha.mjs - commitMode: ${{ secrets.RELEASE_GITHUB_TOKEN && 'github-api' || 'git-cli' }} - name: Check if release needs to be published id: before From a3f98e55e793a6d1cd2413756b0489c78d99e697 Mon Sep 17 00:00:00 2001 From: galargh Date: Wed, 7 May 2025 11:51:35 +0100 Subject: [PATCH 24/27] ci: handle hardhat-toolbox-viem correctly --- .changeset/config.json | 7 +++++++ scripts/version-alpha.mjs | 14 +++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.changeset/config.json b/.changeset/config.json index 51b03815b4b..cc10ab5745e 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -30,8 +30,15 @@ "@nomicfoundation/hardhat-test-utils", "@nomicfoundation/hardhat-typechain", "@nomicfoundation/hardhat-utils", + "@nomicfoundation/hardhat-toolbox-mocha-ethers", "@nomicfoundation/hardhat-viem", "@nomicfoundation/hardhat-zod-utils" + ], + [ + "@nomicfoundation/hardhat-toolbox-viem" + ], + [ + "@nomicfoundation/hardhat-ethers" ] ], "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { diff --git a/scripts/version-alpha.mjs b/scripts/version-alpha.mjs index 9bd920fbca2..a39fe725170 100644 --- a/scripts/version-alpha.mjs +++ b/scripts/version-alpha.mjs @@ -38,8 +38,10 @@ async function versionAlpha() { validateChangesets(changesets); - if (shouldCreateHardhatEthersPackageChangeset(changesets)) { - await createHardhatEthersPackageChangeset(); + for (const packageName of ["@nomicfoundation/hardhat-ethers", "@nomicfoundation/hardhat-toolbox-viem"]) { + if (shouldCreateChangeset(changesets, packageName)) { + await createChangeset(packageName); + } } await executeChangesetVersion(); @@ -92,13 +94,13 @@ async function readHardhatVersion() { /** * Checks whether the hardhat-ethers package changeset should be created. */ -function shouldCreateHardhatEthersPackageChangeset(changesets) { +function shouldCreateChangeset(changesets, packageName) { if (changesets.length === 0) { return false; } for (const { frontMatter } of changesets) { - if (/"@nomicfoundation\/hardhat-ethers": patch$/.test(frontMatter)) { + if (frontMatter.split("\n").some((line) => line.startsWith(`"${packageName}": patch`))) { return false; } } @@ -109,14 +111,12 @@ function shouldCreateHardhatEthersPackageChangeset(changesets) { /** * Write a hardhat-ethers changeset file that has a patch entry for the package. */ -async function createHardhatEthersPackageChangeset() { +async function createChangeset(packageName) { const changesetPath = path.join( changesetDir, `${randomUUID()}.md` ); - const packageName = '@nomicfoundation/hardhat-ethers'; - const releaseChangesetContent = [ '---', `"${packageName}": patch`, From debec91a380f1ee09cfb90109636d9602726dbb6 Mon Sep 17 00:00:00 2001 From: Piotr Galar Date: Thu, 8 May 2025 15:59:29 +0100 Subject: [PATCH 25/27] docs: apply suggestions from code review --- .github/workflows/check-changeset-added.yml | 1 + .github/workflows/release.yml | 5 +++-- scripts/check-release.mjs | 4 ++++ scripts/prepare-github-release.mjs | 5 +++++ scripts/version-alpha.mjs | 11 +++++------ 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/.github/workflows/check-changeset-added.yml b/.github/workflows/check-changeset-added.yml index e3329ec6ea2..2b585cdf162 100644 --- a/.github/workflows/check-changeset-added.yml +++ b/.github/workflows/check-changeset-added.yml @@ -24,6 +24,7 @@ jobs: - name: Checkout the repository uses: actions/checkout@v4 + # We need to fetch the base ref because the pull request check inspects the diff between the head and the base ref - run: git fetch origin "$GITHUB_BASE_REF":"$GITHUB_BASE_REF" - name: Check if merge group is valid diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 834891c62af..3997a3db9be 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: with: # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits fetch-depth: 0 - # NOTE: If we use the GITHUB_TOKEN to create the release PR, the checks on the release PR will not be triggered automatically + # NOTE: If we use default GITHUB_TOKEN to create the release PR, the checks on the release PR will not be triggered automatically token: ${{ secrets.RELEASE_GITHUB_TOKEN || github.token }} - name: Set up the environment @@ -35,7 +35,7 @@ jobs: - name: Create release Pull Request id: pr env: - # NOTE: If we use the GITHUB_TOKEN to create the release PR, the checks on the release PR will not be triggered automatically + # NOTE: If we use the default GITHUB_TOKEN to create the release PR, the checks on the release PR will not be triggered automatically GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN || github.token }} uses: changesets/action@v1 with: @@ -78,6 +78,7 @@ jobs: GITHUB_TOKEN: ${{ github.token }} uses: galargh/action-gh-release@571276229e7c9e6ea18f99bad24122a4c3ec813f # https://github.com/galargh/action-gh-release/pull/1 with: + # We want the GitHub releases to be verified and published by a human because they are automatically pulled into the website draft: true tag_name: hardhat@${{ steps.release.outputs.version }} generate_release_notes: false diff --git a/scripts/check-release.mjs b/scripts/check-release.mjs index f397f578926..3c0df90aedd 100644 --- a/scripts/check-release.mjs +++ b/scripts/check-release.mjs @@ -4,6 +4,10 @@ import { appendFile } from "node:fs/promises"; import { readPackage, isPackageReleasedToNpm } from "./lib/packages.mjs"; +/** + * The function checks whether the version of hardhat from its' package.json is available in the NPM registry + * It appends this information to the GITHUB_OUTPUT file (this is an env variable available in the GitHub Actions environment) + */ async function checkRelease() { if (process.env.GITHUB_OUTPUT === undefined) { throw new Error("GITHUB_OUTPUT is not defined"); diff --git a/scripts/prepare-github-release.mjs b/scripts/prepare-github-release.mjs index bdd8442090a..28f7494de85 100644 --- a/scripts/prepare-github-release.mjs +++ b/scripts/prepare-github-release.mjs @@ -57,6 +57,11 @@ async function getReleaseBody(version) { return lines.join("\n"); } +/** + * The function calculates the information that we need to create a new GitHub Release. Namely, the hardhat version + * whether the release should be marked as prerelease and latest, and the body that should be used for the release. + * It appends this information to the GITHUB_OUTPUT file (this is an env variable available in the GitHub Actions environment) + */ async function prepareGitHubRelease() { if (process.env.GITHUB_OUTPUT === undefined) { throw new Error("GITHUB_OUTPUT is not defined"); diff --git a/scripts/version-alpha.mjs b/scripts/version-alpha.mjs index a39fe725170..193c47d6f63 100644 --- a/scripts/version-alpha.mjs +++ b/scripts/version-alpha.mjs @@ -22,10 +22,9 @@ const packagesDir = "v-next"; * It then combines the new changesets to create a new changelog * section for the new version. * - * The next step is to create a changeset for hardhat-ethers if it does not - * already exists. This is necessary because the hardhat-ethers package - * is one major version ahead of the rest of the packages so we cannot include - * it in the fixed packages set. + * The next step is to create a changeset for packages which are one major + * version ahead of all the others (v4 vs v3) if they don't exist yet. The major + * version mismatch is the reason why we cannot include them in the fixed set. * * The release changeset is then applied, bumping versions across * the packages (including the template packages). @@ -92,7 +91,7 @@ async function readHardhatVersion() { } /** - * Checks whether the hardhat-ethers package changeset should be created. + * Checks whether a new changeset should be created for the package. */ function shouldCreateChangeset(changesets, packageName) { if (changesets.length === 0) { @@ -109,7 +108,7 @@ function shouldCreateChangeset(changesets, packageName) { } /** - * Write a hardhat-ethers changeset file that has a patch entry for the package. + * Creates a new patch changeset file for the pacakge. */ async function createChangeset(packageName) { const changesetPath = path.join( From 644d4efb4e83512e060d9888af172c46209086ff Mon Sep 17 00:00:00 2001 From: galargh Date: Thu, 8 May 2025 16:08:40 +0100 Subject: [PATCH 26/27] chore: remove duplicate package json reading function --- scripts/version-alpha.mjs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/scripts/version-alpha.mjs b/scripts/version-alpha.mjs index 193c47d6f63..011e1e8a248 100644 --- a/scripts/version-alpha.mjs +++ b/scripts/version-alpha.mjs @@ -7,6 +7,7 @@ import { promisify } from "node:util"; import { randomUUID } from "node:crypto"; import { readAllNewChangsets } from "./lib/changesets.mjs"; +import { readPackage } from "./lib/packages.mjs"; const execAsync = promisify(exec); @@ -45,9 +46,9 @@ async function versionAlpha() { await executeChangesetVersion(); - const hardhatVersion = await readHardhatVersion(); + const hardhat = await readPackage("hardhat"); - await updateHardhatChangelog(hardhatVersion, changesets); + await updateHardhatChangelog(hardhat.version, changesets); } /** @@ -79,17 +80,6 @@ function validateChangesets(changesets) { } } -/** - * Read the current Alpha version based on the hardhat package.json - */ -async function readHardhatVersion() { - const hardhatPackageJson = JSON.parse( - (await readFile(path.join("v-next", "hardhat", "package.json"))).toString() - ); - - return hardhatPackageJson.version; -} - /** * Checks whether a new changeset should be created for the package. */ From 43693af6642f83a7af49b2cd91fa638dc22d2b2d Mon Sep 17 00:00:00 2001 From: Piotr Galar Date: Wed, 14 May 2025 16:04:06 +0200 Subject: [PATCH 27/27] Update scripts/validate-merge-group.mjs Co-authored-by: John Kane --- scripts/validate-merge-group.mjs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/scripts/validate-merge-group.mjs b/scripts/validate-merge-group.mjs index 33caa273bc5..7b8260d054c 100644 --- a/scripts/validate-merge-group.mjs +++ b/scripts/validate-merge-group.mjs @@ -1,11 +1,13 @@ // @ts-check /** - * A merge group is valid if it contains no new changesets or all the - * currently checked-in versions of the packages are released to npm. - * The former is an indicationthat the merge group consists of a release PR - * and/or PRs that don't needa changeset. The latter is an indication that the - * merge group does notcontain a release PR. + * A merge group can contain multiple PRs at once - to proceed the merge group should be either: + * 1. Only a release PR (+ no changeset changes PRs) + * 2. A collection of standard feature PRs (+ no changeset changes PRs) + * + * If the merge group has no changesets but there are unreleased packages (version locally is higher than NPM), we know we have only a Release PR. + * If the there are changesets, but all versions locally match NPM, then we have a collection of standard feature PRs - and no Release PR. + * We are only invalid if we have both changesets and unreleased packages. This indicates a Release PR is being unintentionally mixed with a new Feature PR - we throw on this case. */ import { readAllNewChangsets } from './lib/changesets.mjs';