Skip to content

Commit 0b0eba3

Browse files
authored
Release workflow (#544)
This is a modified version of a local script i've been using for a few releases. Hasn't been tested as an action yet so we'll need a new release to validate. ### New workflows for release management: #### Prepare Release Workflow: * Added `.github/workflows/prepare-release.yml` to automate the creation of draft releases, release branches, and pull requests for new versions. Includes support for potential release/hotfix base branches in the case of a backport fix. To create a backport, we'll just need to create a branch for the target base version (release/v1.x) and set the base branch for the action. #### Publish Release Workflow: * Added `.github/workflows/publish-release.yml` to handle the publication of merged release pull requests, including publishing to npm, updating GitHub release notes #### Update Fixed Issues Workflow: * Added `.github/workflows/updated-fixed-issues.yml` to comment on fixed issues. --------- Signed-off-by: Paul Sachs <psachs@buf.build>
1 parent 328680d commit 0b0eba3

9 files changed

Lines changed: 234 additions & 53 deletions

File tree

.github/RELEASING.md

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,13 @@
1212

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

2823
## Release notes
2924

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: Prepare Release
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
version:
7+
description: "Version to release (e.g. 1.2.3)"
8+
required: true
9+
type: string
10+
11+
jobs:
12+
prepare-release:
13+
runs-on: ubuntu-latest
14+
permissions:
15+
contents: write
16+
pull-requests: write
17+
18+
steps:
19+
- name: Checkout repository
20+
uses: actions/checkout@v4
21+
with:
22+
ref: main
23+
fetch-depth: 0
24+
token: ${{ secrets.GITHUB_TOKEN }}
25+
26+
- name: Setup Node.js
27+
uses: actions/setup-node@v6
28+
with:
29+
node-version-file: ".nvmrc"
30+
31+
- name: Install dependencies
32+
run: npm ci
33+
34+
- name: Create release branch
35+
run: |
36+
git config --global user.name 'github-actions[bot]'
37+
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
38+
git checkout -b "release/prep-release-${{ inputs.version }}"
39+
40+
- name: Get current workspace version
41+
id: workspace_version
42+
run: |
43+
VERSION=$(npm run getversion --silent)
44+
echo "version=$VERSION" >> $GITHUB_OUTPUT
45+
46+
- name: Set version and run build
47+
run: |
48+
npm run setversion ${{ inputs.version }}
49+
50+
- name: Commit version changes
51+
run: |
52+
git add .
53+
git commit -s -m "Release ${{ inputs.version }}"
54+
git push --set-upstream origin "release/prep-release-${{ inputs.version }}"
55+
56+
- name: Get release notes
57+
id: release_notes
58+
run: |
59+
RELEASE_NOTES=$(
60+
gh api \
61+
--method POST \
62+
-H "Accept: application/vnd.github+json" \
63+
-H "X-GitHub-Api-Version: 2022-11-28" \
64+
/repos/${{ github.repository }}/releases/generate-notes \
65+
-f 'tag_name=v${{ inputs.version }}' -f 'target_commitish=${{ inputs.base_branch }}' -f 'previous_tag_name=v${{ steps.workspace_version.outputs.version }}' \
66+
--jq ".body" \
67+
)
68+
echo "notes<<EOF" >> $GITHUB_OUTPUT
69+
echo "$RELEASE_NOTES" >> $GITHUB_OUTPUT
70+
echo "EOF" >> $GITHUB_OUTPUT
71+
env:
72+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
73+
74+
- name: Create pull request
75+
run: |
76+
gh pr create \
77+
--title "Release ${{ inputs.version }}" \
78+
--body "${{ steps.release_notes.outputs.notes }}" \
79+
--base "${{ inputs.base_branch }}"
80+
env:
81+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: Publish Release
2+
3+
on:
4+
pull_request:
5+
types: [closed]
6+
branches:
7+
- main
8+
9+
jobs:
10+
publish-release:
11+
runs-on: ubuntu-latest
12+
# Only run if PR was merged and branch name starts with release/prep-release-
13+
if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/prep-release-')
14+
permissions:
15+
id-token: write # Required for OIDC
16+
contents: write
17+
pull-requests: write
18+
issues: write
19+
20+
steps:
21+
- name: Checkout base branch
22+
uses: actions/checkout@v4
23+
with:
24+
ref: ${{ github.event.pull_request.base.ref }}
25+
fetch-depth: 0
26+
token: ${{ secrets.GITHUB_TOKEN }}
27+
28+
- name: Setup Node.js
29+
uses: actions/setup-node@v6
30+
with:
31+
node-version-file: ".nvmrc"
32+
33+
- name: Install dependencies
34+
run: npm ci
35+
36+
- name: Get current workspace version
37+
id: workspace_version
38+
run: |
39+
VERSION=$(npm run getversion --silent)
40+
echo "version=$VERSION" >> $GITHUB_OUTPUT
41+
42+
- name: Get updated release notes from PR
43+
id: pr_notes
44+
run: |
45+
RELEASE_NOTES=$(gh pr view ${{ github.event.pull_request.number }} --json body | jq -r ".body")
46+
echo "notes<<EOF" >> $GITHUB_OUTPUT
47+
echo "$RELEASE_NOTES" >> $GITHUB_OUTPUT
48+
echo "EOF" >> $GITHUB_OUTPUT
49+
env:
50+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51+
52+
- name: Publish to npm
53+
run: npm run release
54+
55+
- name: Publish GitHub release
56+
run: |
57+
gh release create v${{ steps.workspace_version.outputs.version }} \
58+
--title "Release v${{ steps.workspace_version.outputs.version }}" \
59+
--notes "${{ steps.pr_notes.outputs.notes }}"
60+
# --discussion-category "Announcements" ## Enable if discussions are enabled
61+
env:
62+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

cspell.config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"oneof",
4141
"typesafe",
4242
"setversion",
43+
"getversion",
4344
"postsetversion",
4445
"postgenerate",
4546
"npmjs"

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"all": "turbo run --ui tui build format test lint attw license-header",
1414
"clean": "git clean -Xdf",
1515
"setversion": "node scripts/set-workspace-version.js",
16+
"getversion": "node scripts/find-workspace-version.js",
1617
"postsetversion": "npm run all",
1718
"release": "node scripts/release.js",
1819
"prerelease": "npm run all",

scripts/find-workspace-version.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2021-2023 The Connect Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import { findWorkspaceVersion } from "./utils.js";
16+
17+
process.stdout.write(`${findWorkspaceVersion("packages")}\n`);

scripts/release.js

Lines changed: 4 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,16 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import { readdirSync, readFileSync } from "fs";
16-
import { join } from "path";
17-
import { existsSync } from "node:fs";
1815
import { execSync } from "node:child_process";
16+
import { findWorkspaceVersion } from "./utils.js";
1917

2018
/*
2119
* Publish connect-query
2220
*
2321
* Recommended procedure:
24-
* 1. Set a new version with `npm run setversion 1.2.3`
25-
* 2. Commit and push all changes to a PR, wait for approval.
26-
* 3. Login with `npm login`
27-
* 4. Publish to npmjs.com with `npm run release`
28-
* 5. Merge PR and create a release on GitHub
22+
* 1. Trigger the prepare-release workflow with the version you want to release.
23+
* 2. Reviews release notes in the created PR, wait for approval.
24+
* 3. Merge the PR.
2925
*/
3026

3127
const tag = determinePublishTag(findWorkspaceVersion("packages"));
@@ -79,35 +75,3 @@ function determinePublishTag(version) {
7975
throw new Error(`Unable to determine publish tag from version ${version}`);
8076
}
8177
}
82-
83-
/**
84-
* @param {string} packagesDir
85-
* @returns {string}
86-
*/
87-
function findWorkspaceVersion(packagesDir) {
88-
let version = undefined;
89-
for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {
90-
if (!entry.isDirectory()) {
91-
continue;
92-
}
93-
const path = join(packagesDir, entry.name, "package.json");
94-
if (existsSync(path)) {
95-
const pkg = JSON.parse(readFileSync(path, "utf-8"));
96-
if (pkg.private === true) {
97-
continue;
98-
}
99-
if (!pkg.version) {
100-
throw new Error(`${path} is missing "version"`);
101-
}
102-
if (version === undefined) {
103-
version = pkg.version;
104-
} else if (version !== pkg.version) {
105-
throw new Error(`${path} has unexpected version ${pkg.version}`);
106-
}
107-
}
108-
}
109-
if (version === undefined) {
110-
throw new Error(`unable to find workspace version`);
111-
}
112-
return version;
113-
}

scripts/set-workspace-version.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@
1818
import { readFileSync, writeFileSync, existsSync, globSync } from "node:fs";
1919
import { dirname, join } from "node:path";
2020

21-
if (process.argv.length !== 3 || !/^\d+\.\d+\.\d+$/.test(process.argv[2])) {
21+
// Ensures that a valid semver version is provided
22+
// See https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
23+
const versionRegex =
24+
/^(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-]+)*))?$/;
25+
if (process.argv.length !== 3 || !versionRegex.test(process.argv[2])) {
2226
process.stderr.write(
2327
[
2428
`USAGE: ${process.argv[1]} <new-version>`,
@@ -28,6 +32,12 @@ if (process.argv.length !== 3 || !/^\d+\.\d+\.\d+$/.test(process.argv[2])) {
2832
"If a package depends on another package from the workspace, the",
2933
"dependency version is updated as well.",
3034
"",
35+
...(versionRegex.test(process.argv[2])
36+
? []
37+
: [
38+
"Version provided is not a valid semver version.",
39+
"Please provide a version in the format MAJOR.MINOR.PATCH[-PRERELEASE+BUILD].",
40+
]),
3141
].join("\n"),
3242
);
3343
process.exit(1);

scripts/utils.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2021-2023 The Connect Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import { readdirSync, readFileSync, existsSync } from "node:fs";
16+
import { join } from "node:path";
17+
18+
/**
19+
* Retrieves the workspace version from the package directory.
20+
*
21+
* @param {string} packagesDir
22+
* @returns {string}
23+
*/
24+
export function findWorkspaceVersion(packagesDir) {
25+
let version = undefined;
26+
for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {
27+
if (!entry.isDirectory()) {
28+
continue;
29+
}
30+
const path = join(packagesDir, entry.name, "package.json");
31+
if (existsSync(path)) {
32+
const pkg = JSON.parse(readFileSync(path, "utf-8"));
33+
if (pkg.private === true) {
34+
continue;
35+
}
36+
if (!pkg.version) {
37+
throw new Error(`${path} is missing "version"`);
38+
}
39+
if (version === undefined) {
40+
version = pkg.version;
41+
} else if (version !== pkg.version) {
42+
throw new Error(`${path} has unexpected version ${pkg.version}`);
43+
}
44+
}
45+
}
46+
if (version === undefined) {
47+
throw new Error(`unable to find workspace version`);
48+
}
49+
return version;
50+
}

0 commit comments

Comments
 (0)