Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
890b5c3
ci: automate the v-next release process using changesets
galargh Apr 10, 2025
c45cf55
chore: fix the hardhat-ethers version
galargh Apr 10, 2025
32a8235
fix: update lockfile after changeset version
galargh Apr 10, 2025
1b01042
fix: change the expected value of hasChangesets output
galargh Apr 10, 2025
5764c24
fix: authenticate with the npmjs registry
galargh Apr 10, 2025
79e71bd
chore: replace complex logic with js scripts
galargh Apr 11, 2025
a95cb21
fix: syntax and error handling in release scripts
galargh Apr 11, 2025
3b67142
fix: do not run dependency install tests on release PR merges
galargh Apr 11, 2025
43e9c5d
chore: revert the hardhat-ethers version change
galargh May 5, 2025
0ab8274
Merge remote-tracking branch 'origin/v-next' into v-next-release
galargh May 5, 2025
43d4625
chore: revert the hardhat-ethers version change
galargh May 5, 2025
6ab7aa3
ci: do not include hardhat-ether in the set of fixed packages
galargh May 5, 2025
7545b1b
ci: use version-alpha script as the versioning script
galargh May 5, 2025
269ef94
ci: update the changeset check
galargh May 5, 2025
2c99e91
fix: the changeset workflow
galargh May 5, 2025
1384c36
chore: rearrange the changeset check
galargh May 6, 2025
90febd1
chore: move release logic to a script
galargh May 6, 2025
5366fbf
ci: fetch base ref
galargh May 6, 2025
d0382d5
ci: fix the merge group validation
galargh May 6, 2025
4d9a915
ci: accept release token from secrets
galargh May 7, 2025
5fe4b0d
ci: create draft releases only
galargh May 7, 2025
8d0921f
ci: include version in the github release prepare output
galargh May 7, 2025
af53084
ci: use github-api to create the pull request
galargh May 7, 2025
86d3f40
ci: use pat in both places
galargh May 7, 2025
3c333bd
Merge remote-tracking branch 'origin/v-next' into v-next-release
galargh May 7, 2025
a3f98e5
ci: handle hardhat-toolbox-viem correctly
galargh May 7, 2025
debec91
docs: apply suggestions from code review
galargh May 8, 2025
644d4ef
chore: remove duplicate package json reading function
galargh May 8, 2025
6805975
Merge remote-tracking branch 'origin/v-next' into v-next-release
galargh May 8, 2025
71e94b4
Merge branch 'v-next' into v-next-release
kanej May 14, 2025
43693af
Update scripts/validate-merge-group.mjs
galargh May 14, 2025
7b58bd6
Merge branch 'v-next' into v-next-release
galargh May 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,40 @@
"baseBranch": "v-next",
"updateInternalDependencies": "patch",
"ignore": [
"@nomicfoundation/config",
"@nomicfoundation/example-project",
"@nomicfoundation/template-package",
"template-*"
],
"fixed": [
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we ever wanted to stop releasing all the packages together, we could group the ones linked through peer dependency relations here instead.

[
"hardhat",
"@nomicfoundation/hardhat-errors",
"@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-toolbox-mocha-ethers",
"@nomicfoundation/hardhat-viem",
"@nomicfoundation/hardhat-zod-utils"
],
[
"@nomicfoundation/hardhat-toolbox-viem"
],
[
"@nomicfoundation/hardhat-ethers"
]
],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true
}
Expand Down
5 changes: 3 additions & 2 deletions .github/actions/setup-env/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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://registry.npmjs.org
- id: pnpm
if: inputs.cache-save == 'false'
run: pnpm store path --silent | xargs -I {} -0 echo "path={}" | tee -a $GITHUB_OUTPUT
Expand Down
54 changes: 12 additions & 42 deletions .github/workflows/check-changeset-added.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,49 +20,19 @@ 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";
- name: Checkout the repository
uses: actions/checkout@v4

// Merge group context
if (isMergeGroup) {
console.log("Ignore changeset check for merge group.");
return;
}
# 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"

// Single PR context
const pullNumber = context.issue.number;
- name: Check if merge group is valid
if: github.event_name == 'merge_group'
run: node scripts/validate-merge-group.mjs

const { data: files } = await github.rest.pulls.listFiles({
...context.issue,
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"');
return;
}

console.log('The PR is not labeled as "no changeset needed"');

process.exit(1);
- name: Check if PR has a changeset
if: github.event_name == 'pull_request'
env:
GITHUB_EVENT_PULL_REQUEST_LABELS: ${{ toJson(github.event.pull_request.labels) }}
run: node scripts/validate-pull-request.mjs
71 changes: 65 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,30 +1,89 @@
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
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 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 }}

- 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
- name: Create release Pull Request
id: pr
env:
# 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:
version: node scripts/version-alpha.mjs

- 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.before.outputs.released == '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.before.outputs.released == 'false'
env:
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
NPM_CONFIG_PROVENANCE: true
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.before.outputs.released == 'false' && steps.after.outputs.released == 'true'
run: node scripts/prepare-github-release.mjs

- name: Create GitHub Release
if: steps.before.outputs.released == 'false' && steps.after.outputs.released == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ github.token }}
uses: galargh/action-gh-release@571276229e7c9e6ea18f99bad24122a4c3ec813f # https://github.com/galargh/action-gh-release/pull/1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we able to push this to Nomic to keep us self-contained?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we should be able to use the https://github.com/softprops/action-gh-release version of the action instead of my fork now as both my PRs got merged there. I would leave it as a TODO for later though as I would like to carefully check what other things they introduced in their action.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. What is it that drives pulling in an external task here? Is the GitHub API around releases just really cumbersome here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I think about it, we should be able to use the API directly in our case. This action is great for a little more advanced setups. Especially those where a draft gets updated over and over again. However, in our setup, we only ever create the draft once and that's it.

I'm going to update it to use the GitHub API directly. Thanks for pointing this out. I might not be able to finish it before our meeting though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool lets do that as a follow up PR.
I will mark this as approved.

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
target_commitish: ${{ github.sha }}
make_latest: ${{ steps.release.outputs.latest == 'true' }}
prerelease: ${{ steps.release.outputs.prerelease == 'true' }}
body: ${{ steps.release.outputs.body }}
token: ${{ github.token }}
24 changes: 24 additions & 0 deletions scripts/check-release.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// @ts-check

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");
}

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();
71 changes: 71 additions & 0 deletions scripts/lib/changesets.mjs
Original file line number Diff line number Diff line change
@@ -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;
}
}
35 changes: 35 additions & 0 deletions scripts/lib/packages.mjs
Original file line number Diff line number Diff line change
@@ -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;
}
Loading
Loading