-
Notifications
You must be signed in to change notification settings - Fork 1.7k
ci: automate the v-next release process using changesets #6565
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
890b5c3
c45cf55
32a8235
1b01042
5764c24
79e71bd
a95cb21
3b67142
43e9c5d
0ab8274
43d4625
6ab7aa3
7545b1b
269ef94
2c99e91
1384c36
90febd1
5366fbf
d0382d5
4d9a915
5fe4b0d
8d0921f
af53084
86d3f40
3c333bd
a3f98e5
debec91
644d4ef
6805975
71e94b4
43693af
7b58bd6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool lets do that as a follow up PR. |
||
| with: | ||
| # We want the GitHub releases to be verified and published by a human because they are automatically pulled into the website | ||
| draft: true | ||
galargh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 }} | ||
| 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() { | ||
galargh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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(); | ||
| 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; | ||
| } | ||
| } |
| 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; | ||
| } |
There was a problem hiding this comment.
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.