diff --git a/.github/scripts/js-packages-create-and-commit-build.sh b/.github/scripts/js-packages-create-and-commit-build.sh new file mode 100755 index 00000000..7707ea95 --- /dev/null +++ b/.github/scripts/js-packages-create-and-commit-build.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_URL=$1 +TAG_NAME=$2 +PACKAGE_DIR=$3 +PACKAGE_NAME=$(basename "$PACKAGE_DIR") +SOURCE_SHA=$(git rev-parse HEAD) + +TMP_BRANCH="tmp-js-pkg-release-build-${PACKAGE_NAME}" +TMP_BRANCH_PUSHED=false + +cleanup() { + if [ "$TMP_BRANCH_PUSHED" = true ]; then + git push -d origin "$TMP_BRANCH" 2>/dev/null || true + fi +} +trap cleanup EXIT + +# Use the github-actions bot account to commit. +# https://api.github.com/users/github-actions%5Bbot%5D +git config user.name github-actions[bot] +git config user.email 41898282+github-actions[bot]@users.noreply.github.com + +# To move the package to the top directory: +## 1. Delete all files from version control system. +git rm -r . +git commit -q -m "Create the ${TAG_NAME} release build for the \`${PACKAGE_NAME}\` package." + +## 2. Get the package files back. +git checkout HEAD^ -- "./${PACKAGE_DIR}" +git restore --staged . + +## 3. Remove files not needed in the release build. +## This includes release-notes-config.yml and all dotfiles (e.g., .jsdocrc.dev.json, .npmrc). +rm -f "./${PACKAGE_DIR}/release-notes-config.yml" +find "./${PACKAGE_DIR}" -maxdepth 1 -name ".*" -not -name "." -exec rm -rf {} + + +## 4. Move the package contents to the top directory. +## The glob * does not match dotfiles, which is fine since step 3 already removed them. +git add "./${PACKAGE_DIR}" +git mv "./${PACKAGE_DIR}"/* ./ + +## 5. Create the README to point to the source revision of this build. +tee README.md << END +# ${PACKAGE_NAME} +### This is the release build of version \`${TAG_NAME}\`. +### Please visit [here to view the source code of this version](${REPO_URL}/tree/${SOURCE_SHA}/${PACKAGE_DIR}). +END +git add README.md + +## 6. Complete the build for release. +git commit -q --amend -C HEAD + +# The temporary branch is only for pushing to the remote repo. +# Tagging it with a version tag will be proceeded with a separate step. +git push origin "HEAD:refs/heads/$TMP_BRANCH" +TMP_BRANCH_PUSHED=true + +# Deleting the temporary branch is cleanup, so a failure here should not fail an +# otherwise successful release. Leave it best-effort and let the EXIT trap retry. +if git push -d origin "$TMP_BRANCH"; then + TMP_BRANCH_PUSHED=false +fi diff --git a/.github/workflows/js-packages-create-release.yml b/.github/workflows/js-packages-create-release.yml new file mode 100644 index 00000000..014e6b0b --- /dev/null +++ b/.github/workflows/js-packages-create-release.yml @@ -0,0 +1,47 @@ +name: JS Packages - Create Release + +on: + pull_request_review: + types: + - submitted + +jobs: + CreateRelease: + name: Create release for JS package + runs-on: ubuntu-latest + if: ${{ (github.event.pull_request.head.ref == 'release/jsdoc' || github.event.pull_request.head.ref == 'release/tracking-jsdoc') && github.event.review.state == 'approved' }} + steps: + - name: Derive package config + id: config + run: | + PACKAGE_NAME="${{ github.event.pull_request.head.ref }}" + PACKAGE_NAME="${PACKAGE_NAME#release/}" + PACKAGE_DIR="packages/js/${PACKAGE_NAME}" + TAG_PREFIX="${PACKAGE_NAME}-v" + echo "package-name=${PACKAGE_NAME}" >> $GITHUB_OUTPUT + echo "package-dir=${PACKAGE_DIR}" >> $GITHUB_OUTPUT + echo "tag-prefix=${TAG_PREFIX}" >> $GITHUB_OUTPUT + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create release + uses: actions/github-script@v7 + with: + script: | + const workspace = '${{ github.workspace }}'; + const { default: script } = await import( `${ workspace }/.github/scripts/create-release.mjs` ); + await script( { + github, + context, + outputJsonPath: '/tmp/release.json', + packageDir: '${{ steps.config.outputs.package-dir }}', + packageName: '${{ steps.config.outputs.package-name }}', + tagPrefix: '${{ steps.config.outputs.tag-prefix }}', + } ); + + - name: Upload release artifact + uses: actions/upload-artifact@v4 + with: + name: release + path: /tmp/release.json diff --git a/.github/workflows/js-packages-prepare-release.yml b/.github/workflows/js-packages-prepare-release.yml new file mode 100644 index 00000000..b9d698dc --- /dev/null +++ b/.github/workflows/js-packages-prepare-release.yml @@ -0,0 +1,109 @@ +name: JS Packages - Prepare New Release + +on: + push: + branches: + - release/jsdoc + - release/tracking-jsdoc + +jobs: + CheckCreatedBranch: + name: Check Created Branch + runs-on: ubuntu-latest + steps: + - name: Check created release branch + uses: actions/github-script@v7 + with: + script: | + if ( ! context.payload.created ) { + await github.rest.actions.cancelWorkflowRun( { + ...context.repo, + run_id: context.runId, + } ); + } + + PrepareRelease: + name: Prepare Release + runs-on: ubuntu-latest + needs: CheckCreatedBranch + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Derive package config + id: config + run: | + PACKAGE_NAME="${{ github.ref_name }}" + PACKAGE_NAME="${PACKAGE_NAME#release/}" + PACKAGE_DIR="packages/js/${PACKAGE_NAME}" + TAG_TEMPLATE="${PACKAGE_NAME}-v{version}" + CONFIG_PATH="${PACKAGE_DIR}/release-notes-config.yml" + HAS_LOCK_FILE="false" + if [ -f "${PACKAGE_DIR}/package-lock.json" ]; then + HAS_LOCK_FILE="true" + fi + echo "package-name=${PACKAGE_NAME}" >> $GITHUB_OUTPUT + echo "package-dir=${PACKAGE_DIR}" >> $GITHUB_OUTPUT + echo "tag-template=${TAG_TEMPLATE}" >> $GITHUB_OUTPUT + echo "config-path=${CONFIG_PATH}" >> $GITHUB_OUTPUT + echo "has-lock-file=${HAS_LOCK_FILE}" >> $GITHUB_OUTPUT + + - name: Get release notes + id: get-notes + uses: woocommerce/grow/get-release-notes@actions-v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + package-dir: ${{ steps.config.outputs.package-dir }} + config-path: ${{ steps.config.outputs.config-path }} + tag-template: ${{ steps.config.outputs.tag-template }} + minor-keywords: feature, update, enhancement + + - name: Prepare release commits + run: | + PACKAGE_DIR="${{ steps.config.outputs.package-dir }}" + cd "./${PACKAGE_DIR}" + + TODAY=$(date '+%Y-%m-%d') + NEXT_VER="${{ steps.get-notes.outputs.next-version }}" + CHANGELOG='${{ steps.get-notes.outputs.release-changelog-shell }}' + CHANGELOG=$(echo "$CHANGELOG" | sed -E 's/\.? by @[^ ]+ in (https:\/\/github\.com\/.+)/. (\1)/') + + sed -i "/# Changelog/r"<( + printf "\n## ${TODAY} (${NEXT_VER})\n${CHANGELOG}\n" + ) CHANGELOG.md + + jq ".version=\"${NEXT_VER}\"" package.json > package.json.tmp + mv package.json.tmp package.json + + if [ "${{ steps.config.outputs.has-lock-file }}" = "true" ]; then + jq ".version=\"${NEXT_VER}\" | .packages.\"\".version=\"${NEXT_VER}\"" package-lock.json > package-lock.json.tmp + mv package-lock.json.tmp package-lock.json + fi + + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + git add CHANGELOG.md + git add package.json + if [ "${{ steps.config.outputs.has-lock-file }}" = "true" ]; then + git add package-lock.json + fi + cd - + git commit -q -m "Update changelog and package version for the ${{ steps.get-notes.outputs.next-tag }} release of ${{ steps.config.outputs.package-name }}." + git push + + - name: Create a pull request for release + uses: actions/github-script@v7 + with: + script: | + const workspace = '${{ github.workspace }}'; + const { default: script } = await import( `${ workspace }/.github/scripts/create-pr-for-release.mjs` ); + await script( { + github, + context, + refName: '${{ github.ref_name }}', + version: '${{ steps.get-notes.outputs.next-version }}', + packageDir: '${{ steps.config.outputs.package-dir }}', + packageName: '${{ steps.config.outputs.package-name }}', + createReleaseWorkflow: 'js-packages-create-release.yml', + releaseWorkflow: 'js-packages-release.yml', + } ); diff --git a/.github/workflows/js-packages-release.yml b/.github/workflows/js-packages-release.yml new file mode 100644 index 00000000..f4723021 --- /dev/null +++ b/.github/workflows/js-packages-release.yml @@ -0,0 +1,127 @@ +name: JS Packages - Release + +on: + release: + types: + - published + + workflow_run: + workflows: + - JS Packages - Create Release + types: + - completed + branches: + - release/jsdoc + - release/tracking-jsdoc + +jobs: + Setup: + name: Setup and Checks + runs-on: ubuntu-latest + outputs: + release: ${{ steps.set-result.outputs.release }} + steps: + - name: Check tag name or workflow_run conclusion + uses: actions/github-script@v7 + with: + script: | + const { payload, eventName } = context; + const tagReg = /^(jsdoc|tracking-jsdoc)-v(0|[1-9]\d*)(\.(0|[1-9]\d*)){2}(-pre)?$/; + const failedWorkflowRun = eventName === 'workflow_run' && payload.workflow_run.conclusion !== 'success'; + const mismatchedTagName = eventName === 'release' && ! tagReg.test( payload.release.tag_name ); + + if ( failedWorkflowRun || mismatchedTagName ) { + await github.rest.actions.cancelWorkflowRun( { + ...context.repo, + run_id: context.runId, + } ); + } + + - name: Get release artifact + id: set-result + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: actions/github-script@v7 + with: + script: | + const fs = require( 'fs' ); + const { data: { artifacts } } = await github.rest.actions.listWorkflowRunArtifacts( { + ...context.repo, + run_id: context.payload.workflow_run.id, + } ); + + const artifact = artifacts.find( ( el ) => el.name === 'release' ); + const download = await github.rest.actions.downloadArtifact( { + ...context.repo, + artifact_id: artifact.id, + archive_format: 'zip', + } ); + + fs.writeFileSync( `/tmp/release.zip`, Buffer.from( download.data ) ); + await exec.exec( 'unzip', [ '/tmp/release.zip', '-d', '/tmp' ] ); + + const release = fs.readFileSync( `/tmp/release.json`, 'utf8' ); + core.setOutput( 'release', release ); + + UpdateTags: + name: Create Release Build and Update Version Tags + runs-on: ubuntu-latest + needs: Setup + steps: + - name: Resolve tag name + id: resolve-tag + run: | + TAG_NAME="${{ github.event.release.tag_name }}" + if [ "$TAG_NAME" = '' ]; then + TAG_NAME="${{ fromJSON(needs.Setup.outputs.release || '{}').tag_name }}" + fi + echo "tag_name=${TAG_NAME}" >> $GITHUB_OUTPUT + + - name: Derive package config + id: config + run: | + TAG_NAME="${{ steps.resolve-tag.outputs.tag_name }}" + PACKAGE_NAME=$(echo "$TAG_NAME" | sed 's/-v[0-9].*//') + PACKAGE_DIR="packages/js/${PACKAGE_NAME}" + echo "package-name=${PACKAGE_NAME}" >> $GITHUB_OUTPUT + echo "package-dir=${PACKAGE_DIR}" >> $GITHUB_OUTPUT + + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ steps.resolve-tag.outputs.tag_name }} + + - name: Create and commit release build + id: commit-build + run: | + REPO_URL="${{ github.server_url }}/${{ github.repository }}" + TAG_NAME="${{ steps.resolve-tag.outputs.tag_name }}" + PACKAGE_DIR="${{ steps.config.outputs.package-dir }}" + + .github/scripts/js-packages-create-and-commit-build.sh "$REPO_URL" "$TAG_NAME" "$PACKAGE_DIR" + + echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + + - name: Update version tags + uses: woocommerce/grow/update-version-tags@actions-v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + sha: ${{ steps.commit-build.outputs.sha }} + release: ${{ needs.Setup.outputs.release }} + + MergeReleasePR: + name: Merge Release PR + if: ${{ github.event_name == 'workflow_run' }} + needs: UpdateTags + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Merge the release PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr merge "${{ github.event.workflow_run.head_branch }}" \ + --repo "${{ github.repository }}" \ + --merge \ + --delete-branch diff --git a/packages/js/jsdoc/README.md b/packages/js/jsdoc/README.md index 51b33b2e..6a07ef0b 100644 --- a/packages/js/jsdoc/README.md +++ b/packages/js/jsdoc/README.md @@ -88,4 +88,21 @@ To support To support ```js /* @param {SomeClass & {abc: 123}} -``` \ No newline at end of file +``` + +## Release + +### Official release process + +1. Create the branch `release/jsdoc` from the target revision on `trunk` branch. +1. When the branch is created, [the prepare workflow](https://github.com/woocommerce/grow/actions/workflows/js-packages-prepare-release.yml) will prepend changelog, update the version in package.json and package-lock.json, and create a release PR. +1. Check if the new changelog content and updated version are correct. + - If something needs to be revised, append the changes in the release PR. +1. Approve the release PR to trigger [the create release workflow](https://github.com/woocommerce/grow/actions/workflows/js-packages-create-release.yml). +1. After the new release is created, [the release workflow](https://github.com/woocommerce/grow/actions/workflows/js-packages-release.yml) will create the release build, update the version tags, and merge the release PR automatically. + +### Testing the release process + +1. Create a new release with a prerelease version tag. For example `jsdoc-vX.Y.Z-pre`. +1. Check if the "JS Packages - Release" workflow runs successfully. +1. Delete the testing releases and tags once they are no longer in use. diff --git a/packages/js/jsdoc/release-notes-config.yml b/packages/js/jsdoc/release-notes-config.yml new file mode 100644 index 00000000..72a886b8 --- /dev/null +++ b/packages/js/jsdoc/release-notes-config.yml @@ -0,0 +1,35 @@ +changelog: + exclude: + authors: + - github-actions + - github-actions[bot] + labels: + - "changelog: none" + + categories: + # Major level + - title: Breaking Changes 🚨 + labels: + - "[jsdoc] changelog: breaking" + + # Minor level + - title: New Features 🎉 + labels: + - "[jsdoc] changelog: add" + + - title: Updated ✨ + labels: + - "[jsdoc] changelog: update" + + # Patch level + - title: Bug Fixes 🐛 + labels: + - "[jsdoc] changelog: fix" + + - title: Tweaked 🔧 + labels: + - "[jsdoc] changelog: tweak" + + - title: Documentation 📚 + labels: + - "[jsdoc] changelog: docs" diff --git a/packages/js/tracking-jsdoc/README.md b/packages/js/tracking-jsdoc/README.md index a4107897..370eb0f1 100644 --- a/packages/js/tracking-jsdoc/README.md +++ b/packages/js/tracking-jsdoc/README.md @@ -48,4 +48,21 @@ If you would like to add some descriptions to `@fires` or `@emits` tags, for exa "woocommerce-grow-tracking-jsdoc/fires-description" ], // … -``` \ No newline at end of file +``` + +## Release + +### Official release process + +1. Create the branch `release/tracking-jsdoc` from the target revision on `trunk` branch. +1. When the branch is created, [the prepare workflow](https://github.com/woocommerce/grow/actions/workflows/js-packages-prepare-release.yml) will prepend changelog, update the version in package.json, and create a release PR. +1. Check if the new changelog content and updated version are correct. + - If something needs to be revised, append the changes in the release PR. +1. Approve the release PR to trigger [the create release workflow](https://github.com/woocommerce/grow/actions/workflows/js-packages-create-release.yml). +1. After the new release is created, [the release workflow](https://github.com/woocommerce/grow/actions/workflows/js-packages-release.yml) will create the release build, update the version tags, and merge the release PR automatically. + +### Testing the release process + +1. Create a new release with a prerelease version tag. For example `tracking-jsdoc-vX.Y.Z-pre`. +1. Check if the "JS Packages - Release" workflow runs successfully. +1. Delete the testing releases and tags once they are no longer in use. diff --git a/packages/js/tracking-jsdoc/release-notes-config.yml b/packages/js/tracking-jsdoc/release-notes-config.yml new file mode 100644 index 00000000..494a1cdb --- /dev/null +++ b/packages/js/tracking-jsdoc/release-notes-config.yml @@ -0,0 +1,35 @@ +changelog: + exclude: + authors: + - github-actions + - github-actions[bot] + labels: + - "changelog: none" + + categories: + # Major level + - title: Breaking Changes 🚨 + labels: + - "[tracking-jsdoc] changelog: breaking" + + # Minor level + - title: New Features 🎉 + labels: + - "[tracking-jsdoc] changelog: add" + + - title: Updated ✨ + labels: + - "[tracking-jsdoc] changelog: update" + + # Patch level + - title: Bug Fixes 🐛 + labels: + - "[tracking-jsdoc] changelog: fix" + + - title: Tweaked 🔧 + labels: + - "[tracking-jsdoc] changelog: tweak" + + - title: Documentation 📚 + labels: + - "[tracking-jsdoc] changelog: docs"