diff --git a/.github/workflows/build-lint-test.yml b/.github/workflows/build-lint-test.yml index d1092052..66715b68 100644 --- a/.github/workflows/build-lint-test.yml +++ b/.github/workflows/build-lint-test.yml @@ -16,6 +16,12 @@ jobs: is-high-risk-environment: false - run: yarn lint + - name: Validate RC changelog + if: ${{ startsWith(github.head_ref, 'release/') }} + run: yarn lint:changelog --rc + - name: Validate changelog + if: ${{ !startsWith(github.head_ref, 'release/') }} + run: yarn lint:changelog - name: Require clean working directory shell: bash diff --git a/.github/workflows/create-release-pr.yml b/.github/workflows/create-release-pr.yml new file mode 100644 index 00000000..0f0bed9c --- /dev/null +++ b/.github/workflows/create-release-pr.yml @@ -0,0 +1,42 @@ +name: Create Release Pull Request + +on: + workflow_dispatch: + inputs: + base-branch: + description: 'The base branch for git operations and the pull request.' + default: 'main' + required: true + release-type: + description: 'A SemVer version diff, i.e. major, minor, or patch. Mutually exclusive with "release-version".' + required: false + release-version: + description: 'A specific version to bump to. Mutually exclusive with "release-type".' + required: false + +jobs: + create-release-pr: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout and setup environment + uses: MetaMask/action-checkout-and-setup@v2 + with: + is-high-risk-environment: true + + # This is to guarantee that the most recent tag is fetched. This can + # be configured to a more reasonable value by consumers. + fetch-depth: 0 + + # We check out the specified branch, which will be used as the base + # branch for all git operations and the release PR. + ref: ${{ github.event.inputs.base-branch }} + + - uses: MetaMask/action-create-release-pr@v4 + with: + release-type: ${{ github.event.inputs.release-type }} + release-version: ${{ github.event.inputs.release-version }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c6d7046d..3a889538 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -53,3 +53,28 @@ jobs: if [[ "$PASSED" != "true" ]]; then exit 1 fi + + is-release: + # Filtering by `push` events ensures that we only release from the `main` branch, which is a + # requirement for our npm publishing environment. + # The commit author should always be 'github-actions' for releases created by the + # 'create-release-pr' workflow, so we filter by that as well to prevent accidentally + # triggering a release. + if: github.event_name == 'push' && startsWith(github.event.head_commit.author.name, 'github-actions') + needs: all-jobs-pass + outputs: + IS_RELEASE: ${{ steps.is-release.outputs.IS_RELEASE }} + runs-on: ubuntu-latest + steps: + - uses: MetaMask/action-is-release@v1 + id: is-release + + publish-release: + needs: is-release + if: needs.is-release.outputs.IS_RELEASE == 'true' + name: Publish release + permissions: + contents: write + uses: ./.github/workflows/publish-release.yml + secrets: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 00000000..be4cdd34 --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,86 @@ +name: Publish Release + +on: + workflow_call: + inputs: + slack-icon-url: + required: false + type: string + default: 'https://raw.githubusercontent.com/MetaMask/action-npm-publish/main/robo.png' + slack-subteam: + required: false + type: string + default: S042S7RE4AE # @metamask-npm-publishers + slack-username: + required: false + type: string + default: 'MetaMask bot' + secrets: + SLACK_WEBHOOK_URL: + required: true + +jobs: + announce-release: + name: Announce release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - id: name-hash + name: Get Slack name and hash + shell: bash + if: inputs.slack-subteam != '' + run: | + NAME_VERSION_TEXT=$(jq --raw-output '.name + "@" + .version' package.json ) + NAME_VERSION_TEXT_STRIPPED="${NAME_VERSION_TEXT#@}" + echo "NAME_VERSION=$NAME_VERSION_TEXT_STRIPPED" >> "$GITHUB_OUTPUT" + - id: final-text + name: Get Slack final text + shell: bash + if: inputs.slack-subteam != '' + run: | + DEFAULT_TEXT="\`${{ steps.name-hash.outputs.NAME_VERSION }}\` is awaiting deployment :rocket: \n " + SUBTEAM_TEXT="${{ inputs.slack-subteam }}" + FINAL_TEXT="$DEFAULT_TEXT" + if [[ ! "$SUBTEAM_TEXT" == "" ]]; then + FINAL_TEXT=" $DEFAULT_TEXT" + fi + echo "FINAL_TEXT=$FINAL_TEXT" >> "$GITHUB_OUTPUT" + - name: Post to a Slack channel + if: inputs.slack-subteam != '' + uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844 + with: + payload: | + { + "text": "${{ steps.final-text.outputs.FINAL_TEXT }}", + "icon_url": "${{ inputs.slack-icon-url }}", + "username": "${{ inputs.slack-username }}" + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK + continue-on-error: true + + publish-release: + name: Publish release + environment: npm-publish + needs: announce-release + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + # This is to guarantee that the most recent tag is fetched, which we + # need for updating the shorthand major version tag. + fetch-depth: 0 + ref: ${{ github.sha }} + - name: Publish release + uses: MetaMask/action-publish-release@v3 + id: publish-release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Update shorthand major version tag + run: | + ./.github/workflows/scripts/update-major-version-tag.sh \ + ${{ steps.publish-release.outputs.release-version }} diff --git a/.github/workflows/scripts/update-major-version-tag.sh b/.github/workflows/scripts/update-major-version-tag.sh new file mode 100755 index 00000000..5a9e78ec --- /dev/null +++ b/.github/workflows/scripts/update-major-version-tag.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -x +set -e +set -o pipefail + +RELEASE_VERSION="${1}" + +if [[ -z $RELEASE_VERSION ]]; then + echo "Error: No release version specified." + exit 1 +fi + +MAJOR_VERSION_TAG="v${RELEASE_VERSION/\.*/}" + +git config user.name github-actions +git config user.email github-actions@github.com + +if git show-ref --tags "$MAJOR_VERSION_TAG" --quiet; then + echo "Tag \"${MAJOR_VERSION_TAG}\" exists, attempting to delete it." + git tag --delete "$MAJOR_VERSION_TAG" + git push --delete origin "$MAJOR_VERSION_TAG" +else + echo "Tag \"${MAJOR_VERSION_TAG}\" does not exist, creating it from scratch." +fi + +git tag "$MAJOR_VERSION_TAG" HEAD +git push --tags +echo "Updated shorthand major version tag." + +echo "MAJOR_VERSION_TAG=$MAJOR_VERSION_TAG" >> "$GITHUB_OUTPUT" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..b7230876 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/github-tools/ diff --git a/package.json b/package.json index 92c5aa6f..752d8cea 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,10 @@ "version": "0.0.0", "private": true, "description": "Tools for interacting with the GitHub API to do metrics gathering", + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/github-tools.git" + }, "scripts": { "changelog:check": "ts-node src/changelog-check.ts", "count-references-to-contributor-docs": "ts-node --swc src/scripts/count-references-to-contributor-docs/cli.ts", @@ -10,6 +14,7 @@ "get-review-metrics": "ts-node src/get-review-metrics.ts", "lint": "yarn lint:tsc && yarn lint:eslint && yarn lint:constraints && yarn lint:misc --check && yarn lint:dependencies --check", "lint:constraints": "yarn constraints", + "lint:changelog": "auto-changelog validate --prettier", "lint:dependencies": "depcheck && yarn dedupe", "lint:eslint": "eslint . --cache --ext js,ts", "lint:fix": "yarn lint:eslint --fix && yarn lint:constraints --fix && yarn lint:misc --write && yarn lint:dependencies",