-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
42 changed files
with
1,213 additions
and
774 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,50 @@ | ||
# Release tooling | ||
|
||
Release tooling for Redwood. See the [package.json](./package.json) scripts for the available commands. | ||
This repository contains release tooling for the [Redwood monorepo](https://github.com/redwoodjs/redwood). | ||
|
||
## Quick Start | ||
|
||
- In your shell start-up file (e.g. `~/.zshrc` or `~/.bashrc`), add the following environment variable: | ||
- In your shell start-up file (e.g. `~/.zshrc` or `~/.bashrc`) or a `.env` file (that you create) in this directory, add the following environment variable: | ||
|
||
```bash | ||
export RWFW_PATH='/path/to/redwoodjs/redwood' | ||
touch .env | ||
|
||
echo "export RWFW_PATH='/path/to/redwoodjs/redwood'" >> .env | ||
echo "export REDWOOD_GITHUB_TOKEN='...'" >> .env | ||
``` | ||
|
||
Where `RWFW_PATH` is the path to your local copy of the Redwood monorepo and `REDWOOD_GITHUB_TOKEN` is a personal access token with the `public_repo` scope. | ||
|
||
- Check out the `main` and `next` branches from the Redwood monorepo's remote. | ||
|
||
```bash | ||
cd $RWFW_PATH | ||
|
||
git fetch <your-redwood-remote> | ||
git switch main | ||
git switch next | ||
``` | ||
|
||
Where `RWFW_PATH` is the path to your local copy of the Redwood monorepo. | ||
- Run `yarn install` in this directory. It should fly by! This project uses Yarn's [Zero-installs](https://yarnpkg.com/features/caching#zero-installs). | ||
|
||
## Commands | ||
|
||
### Triage | ||
|
||
Redwood has a dual-branch strategy. | ||
Most of the work involves moving commits from main to next and then from next to a release branch. | ||
The triage command guides you through this process: | ||
|
||
``` | ||
yarn triage | ||
``` | ||
|
||
### Release | ||
|
||
- Make sure that you've checked out the `main` and `next` branches from the Redwood monorepo's remote and have the latest changes on your machine. | ||
When it's time to release, the release command walks you through the process: | ||
|
||
## Notes | ||
``` | ||
yarn release | ||
``` | ||
|
||
- This project uses Yarn's [Zero-installs](https://yarnpkg.com/features/caching#zero-installs) | ||
- The code for release is incomplete. Use the release script in the Redwood monorepo for the time being. | ||
Ensure you have access to the RedwoodJS organization on NPM first. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { describe, expect, it } from 'vitest' | ||
|
||
import { assertGitHubToken } from './assert_github_token.js' | ||
|
||
describe('assertGitHubToken', () => { | ||
it('works', () => { | ||
expect(process.env.REDWOOD_GITHUB_TOKEN).toBeDefined() | ||
expect(assertGitHubToken).not.toThrow() | ||
}) | ||
|
||
it("throws if `REDWOOD_GITHUB_TOKEN` isn't defined", () => { | ||
const originalToken = process.env.REDWOOD_GITHUB_TOKEN | ||
delete process.env.REDWOOD_GITHUB_TOKEN | ||
expect(process.env.REDWOOD_GITHUB_TOKEN).toBeUndefined() | ||
expect(assertGitHubToken).toThrow() | ||
process.env.REDWOOD_GITHUB_TOKEN = originalToken | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { chalk } from 'zx' | ||
|
||
import { CustomError } from './custom_error.js' | ||
|
||
export function assertGitHubToken() { | ||
if (process.env.REDWOOD_GITHUB_TOKEN) { | ||
return | ||
} | ||
|
||
throw new CustomError([ | ||
`The ${chalk.magenta('REDWOOD_GITHUB_TOKEN')} environment variable isn't set. Set it to a GitHub personal access token:`, | ||
'', | ||
` ${chalk.green("export REDWOOD_GITHUB_TOKEN='...'")}`, | ||
'', | ||
`in one of your shell start-up files (e.g. ${chalk.magenta('~/.bashrc')} or ${chalk.magenta('~/.zshrc')})`, | ||
'or in a .env file in this directory that you create.', | ||
'', | ||
`You can create a new personal access token at https://github.com/settings/tokens`, | ||
`All it needs is the ${chalk.magenta('public_repo')} scope` | ||
].join('\n')) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { describe, expect, it } from 'vitest' | ||
|
||
import { branchExists, getReleaseBranchesFromStdout } from './branches.js' | ||
|
||
describe('branchExists', () => { | ||
it('works', async () => { | ||
const exists = await branchExists('main') | ||
expect(exists).toBe(true) | ||
}) | ||
|
||
it("returns false if the branch doesn't exist", async () => { | ||
const exists = await branchExists('nonexistent-branch') | ||
expect(exists).toBe(false) | ||
}) | ||
}) | ||
|
||
describe('getReleaseBranches', () => { | ||
it('works', () => { | ||
const releaseBranches = getReleaseBranchesFromStdout('release/patch/v7.0.4\nrelease/minor/v7.1.0\nrelease/patch/v7.0.5') | ||
expect(releaseBranches).toEqual(["release/minor/v7.1.0", "release/patch/v7.0.5", "release/patch/v7.0.4"]) | ||
}) | ||
|
||
it("returns an empty array if there's no release branches", () => { | ||
const releaseBranches = getReleaseBranchesFromStdout('') | ||
expect(releaseBranches).toEqual([]) | ||
}) | ||
|
||
it('handles a release branch currently being checked out', () => { | ||
const releaseBranches = getReleaseBranchesFromStdout('release/patch/v7.0.4\n* release/patch/v7.0.5') | ||
expect(releaseBranches).toEqual(["release/patch/v7.0.5", "release/patch/v7.0.4"]) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import semver from 'semver' | ||
import { chalk, $ } from 'zx' | ||
|
||
import { CustomError } from './custom_error.js' | ||
import { unwrap } from './zx_helpers.js' | ||
|
||
export async function assertWorkTreeIsClean() { | ||
const workTreeIsClean = unwrap(await $`git status -s`) === '' | ||
if (!workTreeIsClean) { | ||
throw new CustomError( | ||
`The working tree at ${chalk.magenta(process.cwd())} isn't clean. Commit or stash your changes` | ||
); | ||
} | ||
console.log('✨ The working tree is clean') | ||
} | ||
|
||
export async function branchExists(branch: string) { | ||
return !!unwrap(await $`git branch --list ${branch}`) | ||
} | ||
|
||
export async function assertBranchExistsAndTracksRemote(branch: string, remote: string) { | ||
if (!(await branchExists(branch))) { | ||
throw new CustomError([ | ||
`The ${chalk.magenta(branch)} branch doesn't exist locally. Check it out from the Redwood remote:`, | ||
'', | ||
chalk.green(` git checkout -b ${branch} ${remote}/${branch}`), | ||
].join('\n')) | ||
} | ||
console.log(`🏠 The ${chalk.magenta(branch)} branch exists locally`) | ||
|
||
const trackingBranch = unwrap(await $`git rev-parse --abbrev-ref ${branch}@{upstream}`) | ||
if (trackingBranch === `${remote}/${branch}`) { | ||
console.log(`🆗 The ${chalk.magenta(branch)} branch tracks ${chalk.magenta(`${remote}/${branch}`)}`) | ||
return | ||
} | ||
|
||
throw new CustomError([ | ||
`The ${chalk.magenta(branch)} branch doesn't track ${chalk.magenta(`${remote}/${branch}`)}`, | ||
`It's currently tracking ${chalk.magenta(trackingBranch)}`, | ||
'', | ||
`Make it track the remote with:`, | ||
'', | ||
chalk.green(` git branch -u ${remote}/${branch}`), | ||
].join('\n')) | ||
} | ||
|
||
export async function pullBranch(branch: string, remote: string) { | ||
await $`git switch ${branch}` | ||
await $`git pull ${remote} ${branch}` | ||
} | ||
|
||
export async function pushBranch(branch: string, remote: string) { | ||
await $`git push ${remote} ${branch}` | ||
} | ||
|
||
export async function getReleaseBranches() { | ||
const stdout = unwrap(await $`git branch --list release/*`) | ||
return getReleaseBranchesFromStdout(stdout) | ||
} | ||
|
||
export function getReleaseBranchesFromStdout(stdout: string) { | ||
if (stdout === '') { | ||
return [] | ||
} | ||
|
||
const releaseBranches = stdout | ||
.split('\n') | ||
.map((branch) => branch.trim().replace('* ', '')) | ||
.sort((releaseBranchA, releaseBranchB) => { | ||
const [, , versionA] = releaseBranchA.split('/') | ||
const [, , versionB] = releaseBranchB.split('/') | ||
return semver.compare(versionA, versionB) | ||
}) | ||
|
||
return releaseBranches.reverse() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { describe, expect, it, test } from 'vitest' | ||
|
||
import { getCommitHash, getCommitMessage, getCommitNotes, getCommitPr } from './commits.js' | ||
|
||
describe('getCommitHash', () => { | ||
it('works', () => { | ||
const hash = getCommitHash("< 487548234b49bb93bb79ad89c7ac4a91ed6c0dc9 chore(deps): update dependency @playwright/test to v1.41.2 (#10040)") | ||
expect(hash).toEqual('487548234b49bb93bb79ad89c7ac4a91ed6c0dc9') | ||
}) | ||
|
||
it('throws if no hash is found', () => { | ||
expect(() => getCommitHash("|\\")).toThrowErrorMatchingInlineSnapshot(` | ||
[Error: Couldn't find a commit hash in the line "|\\" | ||
This most likely means that a line that's UI isn't being identified as such] | ||
`) | ||
}) | ||
|
||
it('works for non left-right lines', () => { | ||
const hash = getCommitHash("487548234b49bb93bb79ad89c7ac4a91ed6c0dc9 chore(deps): update dependency @playwright/test to v1.41.2 (#10040)") | ||
expect(hash).toEqual('487548234b49bb93bb79ad89c7ac4a91ed6c0dc9') | ||
|
||
}) | ||
}) | ||
|
||
test('getCommitMessage', async () => { | ||
const message = await getCommitMessage('4f4ad5989b794ddd0065d9c96c3091343c2a63c0') | ||
expect(message).toEqual('Initial commit') | ||
}) | ||
|
||
describe('getCommitNotes', () => { | ||
it('works', async () => { | ||
const notes = await getCommitNotes('4f4ad5989b794ddd0065d9c96c3091343c2a63c0') | ||
expect(notes).toEqual('(jtoar) hello') | ||
}) | ||
|
||
it("returns `undefined` there's no notes", async () => { | ||
const notes = await getCommitNotes('9fd38ae1b3ad6afc5b0c1b2acb627b2bed25abda') | ||
expect(notes).toBeUndefined() | ||
}) | ||
}) | ||
|
||
describe('getCommitPr', () => { | ||
it('works', () => { | ||
expect(getCommitPr("< 487548234b49bb93bb79ad89c7ac4a91ed6c0dc9 chore(deps): update dependency @playwright/test to v1.41.2 (#10040)")).toEqual('10040') | ||
}) | ||
|
||
it('works for reverts', () => { | ||
expect(getCommitPr("< 5f89c0176f517b894cb2c3f1ab9cee4c7c207393 Revert \"Revert `@testing-library/jest-dom` v6 upgrade (#9713)\" (#9719)")).toEqual('9719') | ||
}) | ||
|
||
it("returns `undefined` if it can't find a PR", () => { | ||
expect(getCommitPr("< 635d6dea677b28993661a2e46659ff8c987b7275 Merge branch 'release/major/v7.0.0'")).toBeUndefined() | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { $ } from 'zx' | ||
|
||
import { unwrap } from './zx_helpers.js' | ||
|
||
/** Square brackets (`[` or `]`) in commit messages need to be escaped */ | ||
function sanitizeMessage(message: string) { | ||
return message.replace('[', '\\[').replace(']', '\\]') | ||
} | ||
|
||
/** See if a commit has been cherry picked into a given ref */ | ||
export async function commitIsInRef(ref: string, message: string) { | ||
message = sanitizeMessage(message) | ||
return unwrap(await $`git log ${ref} --oneline --grep ${message}`) | ||
} | ||
|
||
export const commitRegExps = { | ||
hash: /(?<hash>\w{40})\s/, | ||
pr: /\(#(?<pr>\d+)\)$/, | ||
annotatedTag: /^v\d.\d.\d$/, | ||
} | ||
|
||
/** Get a commit's 40-character hash */ | ||
export function getCommitHash(line: string) { | ||
const match = line.match(commitRegExps.hash) | ||
|
||
if (!match?.groups) { | ||
throw new Error([ | ||
`Couldn't find a commit hash in the line "${line}"`, | ||
"This most likely means that a line that's UI isn't being identified as such", | ||
].join('\n')) | ||
} | ||
|
||
return match.groups.hash | ||
} | ||
|
||
/** Get a commit's message from its 40-character hash */ | ||
export async function getCommitMessage(hash: string) { | ||
return unwrap(await $`git log --format=%s -n 1 ${hash}`) | ||
} | ||
|
||
/** Get a commit's PR if it has one */ | ||
export function getCommitPr(message: string) { | ||
return message.match(commitRegExps.pr)?.groups?.pr | ||
} | ||
|
||
/** Get a commit's notes if it has any */ | ||
export async function getCommitNotes(hash: string) { | ||
try { | ||
const notes = unwrap(await $`git notes show ${hash}`) | ||
return notes | ||
} catch (error) { | ||
return undefined | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { describe, expect, it } from 'vitest' | ||
|
||
import { assertRwfwPathAndSetCwd } from './cwd.js' | ||
|
||
describe('assertRwfwPathAndSetCwd', () => { | ||
it('works', async () => { | ||
const originalCwd = process.cwd() | ||
const resetCwd = await assertRwfwPathAndSetCwd() | ||
expect(process.cwd()).toEqual(process.env.RWFW_PATH) | ||
resetCwd() | ||
expect(process.cwd()).toEqual(originalCwd) | ||
}) | ||
|
||
it("throws if RWFW_PATH isn't set", async () => { | ||
const originalRwfwPath = process.env.RWFW_PATH | ||
delete process.env.RWFW_PATH | ||
expect(process.env.RWFW_PATH).toBeUndefined() | ||
await expect(() => assertRwfwPathAndSetCwd()).rejects.toThrowError() | ||
process.env.RWFW_PATH = originalRwfwPath | ||
}) | ||
}) |
Oops, something went wrong.