Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
64 changes: 64 additions & 0 deletions .github/scripts/js-packages-create-and-commit-build.sh
Original file line number Diff line number Diff line change
@@ -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
47 changes: 47 additions & 0 deletions .github/workflows/js-packages-create-release.yml
Original file line number Diff line number Diff line change
@@ -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
109 changes: 109 additions & 0 deletions .github/workflows/js-packages-prepare-release.yml
Original file line number Diff line number Diff line change
@@ -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

Comment on lines +54 to +60

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The major-keywords input is intentionally omitted here so it falls back to the action default, which is "breaking", as defined in get-release-notes/action.yml. With that default, only the "Breaking Changes" heading matches the major level, so regular releases are not bumped to major.

- 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',
} );
127 changes: 127 additions & 0 deletions .github/workflows/js-packages-release.yml
Original file line number Diff line number Diff line change
@@ -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,
} );
}
Comment on lines +28 to +38

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The automated release is created with the default GITHUB_TOKEN, and events from GITHUB_TOKEN do not start a new workflow run, so the release.published path is skipped and only workflow_run runs. The release.published path fires only for manual releases. Triggering both would take a deliberate bypass (such as using a PAT), which operators would not do in normal use.


- 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',
} );
Comment on lines +52 to +57

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This step is gated by if: github.event.workflow_run.conclusion == 'success', and the "JS Packages - Create Release" workflow always ends by uploading the release artifact. So whenever the run concludes successfully, the artifact should be present, which means artifact would not be undefined here in practice.


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
19 changes: 18 additions & 1 deletion packages/js/jsdoc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,21 @@ To support
To support
```js
/* @param {SomeClass & {abc: 123}}
```
```

## 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.
Comment thread
eason9487 marked this conversation as resolved.
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.
Loading