Skip to content
Open
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
19 changes: 7 additions & 12 deletions .github/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,13 @@

1. Choose a new version (e.g. 1.2.3), making sure to follow semver. Note that all
packages in this repository use the same version number.
2. Make sure you are on the latest main, and create a new git branch.
3. Set the new version across all packages within the monorepo with the following
command: `npm run setversion 1.2.3`
4. Commit, push, and open a pull request with the title "Release 1.2.3".
5. Edit the PR description with release notes. See the section below for details.
6. Make sure CI passed on your PR and ask a maintainer for review.
7. After approval, run the following command to publish to npmjs.com: `npm run release`.
8. Merge your PR.
9. Create a new release in the GitHub UI
- Choose "v1.2.3" as a tag and as the release title.
- Copy and paste the release notes from the PR description.
- Check the checkbox “Create a discussion for this release”.
2. Trigger the prepare-release workflow that will create a release PR.

- Note: If releasing for a hotfix of a major version that is behind the current main branch, make sure to create an appropriate branch (e.g. release/v1.x) before running the workflow with the branch name set as the base_branch.

3. Edit the PR description with release notes. See the section below for details.
4. Make sure CI passed on your PR and ask a maintainer for review.
5. After approval, merge your PR.

## Release notes

Expand Down
81 changes: 81 additions & 0 deletions .github/workflows/prepare-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: Prepare Release

on:
workflow_dispatch:
inputs:
version:
description: "Version to release (e.g. 1.2.3)"
required: true
type: string

jobs:
prepare-release:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"

- name: Install dependencies
run: npm ci

- name: Create release branch
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
git checkout -b "release/prep-release-${{ inputs.version }}"

- name: Get current workspace version
id: workspace_version
run: |
VERSION=$(npm run getversion --silent)
echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Set version and run build
run: |
npm run setversion ${{ inputs.version }}

- name: Commit version changes
run: |
git add .
git commit -s -m "Release ${{ inputs.version }}"
git push --set-upstream origin "release/prep-release-${{ inputs.version }}"

- name: Get release notes
Copy link
Member

Choose a reason for hiding this comment

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

Is this the reason to draft a release? Then I suggest that we create a draft, get the generated release notes, and delete the draft.

Can you add a comment that we should switch to an API call to generate release notes without creating a release? https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#generate-release-notes-content-for-a-release

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated script to use api directly, much easier to understand with less side effects 🚀

Copy link

Choose a reason for hiding this comment

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

Why not kick the action off by manually creating a draft release and editing the release notes up front? The prepare-release action could trigger on that event, pick up the release tag, target branch, and release notes, create the release branch and PR, and delete the draft release.

It wouldn't be very different from what you have, but it would make use of the "new release" UI rather than the more esoteric UI to trigger a workflow.

One downside I see is that you could accidentally create a non-draft release in the UI... but this could maybe be handled by permissions or even detected and cleaned up by the prepare-release workflow?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The original idea was to leverage a review process, which is more central to this current version (everything pivots off the PR). Creating a release doesn't really have associated workflows so we'd need to do cleanup. But both would obviously work.

Copy link

Choose a reason for hiding this comment

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

Yeah, totally get why you're using a PR for review, and I wasn't suggesting changing that. I was just suggesting using the draft release creation to kick off the flow which would seem marginally more GitHub-idiomatic to me, even though you still wouldn't be publishing the draft release actually created since it would just be tagged from main (or from a branch for a backport).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah i think i now understand what you meant. So instead of triggering via workflow run, we'd trigger on a draft release creation. Yeah, has a little more cleanup but i can see why that might be a little more natural.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@timostamm do you have thoughts on this? Using a draft release as the trigger feels a little more intuitive vs an arbitrary workflow.

Copy link
Member

Choose a reason for hiding this comment

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

Can draft releases trigger a workflow?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm, not clear from the docs. It says that a draft release doesn't trigger a created/edited/deleted, which implies published but i don't know how you would trigger a published event on a draft release (i guess when it moves from draft to release). So yeah, that would probably be a blocker to using drafts.

id: release_notes
run: |
RELEASE_NOTES=$(
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
/repos/${{ github.repository }}/releases/generate-notes \
-f 'tag_name=v${{ inputs.version }}' -f 'target_commitish=${{ inputs.base_branch }}' -f 'previous_tag_name=v${{ steps.workspace_version.outputs.version }}' \
--jq ".body" \
)
echo "notes<<EOF" >> $GITHUB_OUTPUT
echo "$RELEASE_NOTES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Create pull request
run: |
gh pr create \
--title "Release ${{ inputs.version }}" \
--body "${{ steps.release_notes.outputs.notes }}" \
--base "${{ inputs.base_branch }}"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
62 changes: 62 additions & 0 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Publish Release

on:
pull_request:
types: [closed]
branches:
- main

jobs:
publish-release:
runs-on: ubuntu-latest
# Only run if PR was merged and branch name starts with release/prep-release-
if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/prep-release-')
permissions:
id-token: write # Required for OIDC
contents: write
pull-requests: write
issues: write

steps:
- name: Checkout base branch
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.ref }}
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"

- name: Install dependencies
run: npm ci

- name: Get current workspace version
id: workspace_version
run: |
VERSION=$(npm run getversion --silent)
echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Get updated release notes from PR
id: pr_notes
run: |
RELEASE_NOTES=$(gh pr view ${{ github.event.pull_request.number }} --json body | jq -r ".body")
echo "notes<<EOF" >> $GITHUB_OUTPUT
echo "$RELEASE_NOTES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Publish to npm
run: npm run release

- name: Publish GitHub release
run: |
gh release create v${{ steps.workspace_version.outputs.version }} \
--title "Release v${{ steps.workspace_version.outputs.version }}" \
--notes "${{ steps.pr_notes.outputs.notes }}"
# --discussion-category "Announcements" ## Enable if discussions are enabled
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1 change: 1 addition & 0 deletions cspell.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"oneof",
"typesafe",
"setversion",
"getversion",
"postsetversion",
"postgenerate",
"npmjs"
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"all": "turbo run --ui tui build format test lint attw license-header",
"clean": "git clean -Xdf",
"setversion": "node scripts/set-workspace-version.js",
"getversion": "node scripts/find-workspace-version.js",
"postsetversion": "npm run all",
"release": "node scripts/release.js",
"prerelease": "npm run all",
Expand Down
17 changes: 17 additions & 0 deletions scripts/find-workspace-version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2021-2023 The Connect Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { findWorkspaceVersion } from "./utils.js";

process.stdout.write(`${findWorkspaceVersion("packages")}\n`);
44 changes: 4 additions & 40 deletions scripts/release.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { readdirSync, readFileSync } from "fs";
import { join } from "path";
import { existsSync } from "node:fs";
import { execSync } from "node:child_process";
import { findWorkspaceVersion } from "./utils.js";

/*
* Publish connect-query
*
* Recommended procedure:
* 1. Set a new version with `npm run setversion 1.2.3`
* 2. Commit and push all changes to a PR, wait for approval.
* 3. Login with `npm login`
* 4. Publish to npmjs.com with `npm run release`
* 5. Merge PR and create a release on GitHub
* 1. Trigger the prepare-release workflow with the version you want to release.
* 2. Reviews release notes in the created PR, wait for approval.
* 3. Merge the PR.
*/

const tag = determinePublishTag(findWorkspaceVersion("packages"));
Expand Down Expand Up @@ -79,35 +75,3 @@ function determinePublishTag(version) {
throw new Error(`Unable to determine publish tag from version ${version}`);
}
}

/**
* @param {string} packagesDir
* @returns {string}
*/
function findWorkspaceVersion(packagesDir) {
let version = undefined;
for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
const path = join(packagesDir, entry.name, "package.json");
if (existsSync(path)) {
const pkg = JSON.parse(readFileSync(path, "utf-8"));
if (pkg.private === true) {
continue;
}
if (!pkg.version) {
throw new Error(`${path} is missing "version"`);
}
if (version === undefined) {
version = pkg.version;
} else if (version !== pkg.version) {
throw new Error(`${path} has unexpected version ${pkg.version}`);
}
}
}
if (version === undefined) {
throw new Error(`unable to find workspace version`);
}
return version;
}
12 changes: 11 additions & 1 deletion scripts/set-workspace-version.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
import { readFileSync, writeFileSync, existsSync, globSync } from "node:fs";
import { dirname, join } from "node:path";

if (process.argv.length !== 3 || !/^\d+\.\d+\.\d+$/.test(process.argv[2])) {
// Ensures that a valid semver version is provided
// See https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
const versionRegex =
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
if (process.argv.length !== 3 || !versionRegex.test(process.argv[2])) {
process.stderr.write(
[
`USAGE: ${process.argv[1]} <new-version>`,
Expand All @@ -28,6 +32,12 @@ if (process.argv.length !== 3 || !/^\d+\.\d+\.\d+$/.test(process.argv[2])) {
"If a package depends on another package from the workspace, the",
"dependency version is updated as well.",
"",
...(versionRegex.test(process.argv[2])
? []
: [
"Version provided is not a valid semver version.",
"Please provide a version in the format MAJOR.MINOR.PATCH[-PRERELEASE+BUILD].",
]),
].join("\n"),
);
process.exit(1);
Expand Down
50 changes: 50 additions & 0 deletions scripts/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2021-2023 The Connect Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { readdirSync, readFileSync, existsSync } from "node:fs";
import { join } from "node:path";

/**
* Retrieves the workspace version from the package directory.
*
* @param {string} packagesDir
* @returns {string}
*/
export function findWorkspaceVersion(packagesDir) {
let version = undefined;
for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
const path = join(packagesDir, entry.name, "package.json");
if (existsSync(path)) {
const pkg = JSON.parse(readFileSync(path, "utf-8"));
if (pkg.private === true) {
continue;
}
if (!pkg.version) {
throw new Error(`${path} is missing "version"`);
}
if (version === undefined) {
version = pkg.version;
} else if (version !== pkg.version) {
throw new Error(`${path} has unexpected version ${pkg.version}`);
}
}
}
if (version === undefined) {
throw new Error(`unable to find workspace version`);
}
return version;
}