Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release tooling: Add label all patch PRs on stable promotions, fix sync of version files #23491

Merged
merged 12 commits into from
Jul 24, 2023
40 changes: 30 additions & 10 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,13 @@ jobs:
if: github.ref_name == 'latest-release'
run: git fetch --tags origin

# when this is a patch release from main, label any patch PRs included in the release
# when this is a stable release from next, label ALL patch PRs found, as they will per definition be "patched" now
Copy link
Contributor Author

@JReinhold JReinhold Jul 18, 2023

Choose a reason for hiding this comment

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

This is technically not true.

If we're doing a stable release from next, and while that is being done and frozen, a new PR is merged to next with the patch label, that PR will not be part of the release, but it will be labeled as "patch:done" here, which is incorrect.

The only way to get around that would be to check that the merge commit of each patch PR found is also found in the git log on next-release. This is not trivial to do, but doable.

WDYT @kasperpeulen @shilman ?

- name: Label patch PRs as picked
if: github.ref_name == 'latest-release'
if: github.ref_name == 'latest-release' || (steps.publish-needed.outputs.published == 'false' && steps.target.outputs.target == 'next' && !steps.is-prerelease.outputs.prerelease)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: yarn release:label-patches
run: yarn release:label-patches ${{ steps.target.outputs.target == 'next' && '--all' || '' }}

- name: Create GitHub Release
if: steps.publish-needed.outputs.published == 'false'
Expand Down Expand Up @@ -161,25 +163,43 @@ jobs:
git commit -m "Update CHANGELOG.md for v${{ steps.version.outputs.current-version }} [skip ci]"
git push origin next

- name: Sync versions/next.json from `next` to `main`
- name: Sync version JSONs from `next-release` to `main`
if: github.ref_name == 'next-release'
working-directory: .
run: |
VERSION_FILE="./docs/versions/${{ steps.is-prerelease.outputs.prerelease == 'true' && 'next' || 'latest' }}.json"
git fetch origin main
git checkout main
git pull
git checkout origin/next ./docs/versions/next.json
git add ./docs/versions/next.json
git commit -m "Update versions/next.json for v${{ steps.version.outputs.current-version }}"
git checkout origin/next-release $VERSION_FILE
git add $VERSION_FILE
git commit -m "Update $VERSION_FILE for v${{ steps.version.outputs.current-version }}"
git push origin main

# Force push from next to main if it is not a prerelease, and this release is from next-release
# TODO: this is currently disabled, because we may have a better strategy that we want to try out manually first before comitting to it:
# - create a branch "release-<VERSION>" from HEAD of main
# - git push --force origin ${{ steps.target.outputs.target }}:main
# - ... this will keep the "main" history in the new release branch, and then overwrite main's history with next's

# Sync next-release to main if it is not a prerelease, and this release is from next-release
# This happens when eg. next has been tracking 7.1.0-alpha.X, and now we want to release 7.1.0
# This will keep release-next, next and main all tracking v7.1.0
# - name: Force push ${{ steps.target.outputs.target }} to main
# This will keep next-release, next and main all tracking v7.1.0
# See "Alternative merge strategies" in https://stackoverflow.com/a/36321787
# - name: Sync next-release to main
# if: steps.publish-needed.outputs.published == 'false' && steps.target.outputs.target == 'next' && !steps.is-prerelease.outputs.prerelease
# working-directory: .
# run: |
# git push --force origin ${{ steps.target.outputs.target }}:main
# git fetch origin next-release
# git checkout next-release
# git pull
# git fetch origin main
# git checkout main
# git pull
# git merge --no-commit -s ours next-release
# git rm -rf .
# git checkout next-release -- .
# git commit -m "Sync next-release to main"
# git push origin main

- name: Report job failure to Discord
if: failure()
Expand Down
4 changes: 4 additions & 0 deletions CONTRIBUTING/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,10 @@ It's possible and valid to push manual changes directly on the release branch wh

It's recommended to use the automated process as much as possible to ensure that the information in GitHub is the single source of truth, and that pull requests and changelogs are in sync.

> **Warning**
> If you make manual changes to the changelog, you also need to make those changes in either [`./docs/versions/latest.json`](../docs/versions/latest.json) or [`./docs/versions/next.json`](../docs/versions/next.json). The `"plain"` property should match the changelog entry, **without the heading** and with all new lines replaces with `\n`.
> This is common for custom release notes when releasing majors and minors.

### 6. Merge

When the pull request was frozen, a CI run was triggered on the branch. If it's green, it's time to merge the pull request. If CI is failing for some reason, consult with the rest of the core team. These release pull requests are almost exact copies of `next|main` so CI should only fail if they fail too.
Expand Down
8 changes: 8 additions & 0 deletions scripts/release/__tests__/generate-pr-description.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ For each pull request below, you need to either manually cherry pick it, or disc

If you\\'ve made any changes doing the above QA (change PR titles, revert PRs), manually trigger a re-generation of this PR with [this workflow](https://github.com/storybookjs/storybook/actions/workflows/prepare-prerelease.yml) and wait for it to finish. It will wipe your progress in this to do, which is expected.

Feel free to manually commit any changes necessary to this branch **after** you\\'ve done the last re-generation, following the [Make Manual Changes](https://github.com/storybookjs/storybook/blob/next/CONTRIBUTING/RELEASING.md#5-make-manual-changes) section in the docs, *especially* if you\\'re making changes to the changelog.

When everything above is done:
- Merge this PR
- [Follow the run of the publish action](https://github.com/storybookjs/storybook/actions/workflows/publish.yml)
Expand Down Expand Up @@ -273,6 +275,8 @@ For each pull request below, you need to either manually cherry pick it, or disc

If you\\'ve made any changes (change PR titles, revert PRs), manually trigger a re-generation of this PR with [this workflow](https://github.com/storybookjs/storybook/actions/workflows/prepare-patch-release.yml) and wait for it to finish.

Feel free to manually commit any changes necessary to this branch **after** you\\'ve done the last re-generation, following the [Make Manual Changes](https://github.com/storybookjs/storybook/blob/next/CONTRIBUTING/RELEASING.md#5-make-manual-changes) section in the docs.

When everything above is done:
- Merge this PR
- [Follow the run of the publish action](https://github.com/storybookjs/storybook/actions/workflows/publish.yml)"
Expand Down Expand Up @@ -338,6 +342,8 @@ For each pull request below, you need to either manually cherry pick it, or disc

If you\\'ve made any changes doing the above QA (change PR titles, revert PRs), manually trigger a re-generation of this PR with [this workflow](https://github.com/storybookjs/storybook/actions/workflows/prepare-prerelease.yml) and wait for it to finish. It will wipe your progress in this to do, which is expected.

Feel free to manually commit any changes necessary to this branch **after** you\\'ve done the last re-generation, following the [Make Manual Changes](https://github.com/storybookjs/storybook/blob/next/CONTRIBUTING/RELEASING.md#5-make-manual-changes) section in the docs, *especially* if you\\'re making changes to the changelog.

When everything above is done:
- Merge this PR
- [Follow the run of the publish action](https://github.com/storybookjs/storybook/actions/workflows/publish.yml)
Expand Down Expand Up @@ -391,6 +397,8 @@ For each pull request below, you need to either manually cherry pick it, or disc

If you\\'ve made any changes (change PR titles, revert PRs), manually trigger a re-generation of this PR with [this workflow](https://github.com/storybookjs/storybook/actions/workflows/prepare-patch-release.yml) and wait for it to finish.

Feel free to manually commit any changes necessary to this branch **after** you\\'ve done the last re-generation, following the [Make Manual Changes](https://github.com/storybookjs/storybook/blob/next/CONTRIBUTING/RELEASING.md#5-make-manual-changes) section in the docs.

When everything above is done:
- Merge this PR
- [Follow the run of the publish action](https://github.com/storybookjs/storybook/actions/workflows/publish.yml)"
Expand Down
93 changes: 92 additions & 1 deletion scripts/release/__tests__/label-patches.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,22 @@ beforeEach(() => {
gitClient.git.getRemotes.mockResolvedValue(remoteMock);
githubInfo.getPullInfoFromCommit.mockResolvedValue(pullInfoMock);
github.getLabelIds.mockResolvedValue({ 'patch:done': 'pick-id' });
github.getUnpickedPRs.mockResolvedValue([
{
number: 42,
id: 'some-id',
branch: 'some-patching-branch',
title: 'Fix: Patch this PR',
mergeCommit: 'abcd1234',
},
{
number: 44,
id: 'other-id',
branch: 'other-patching-branch',
title: 'Fix: Also patch this PR',
mergeCommit: 'abcd1234',
},
]);
});

test('it should fail early when no GH_TOKEN is set', async () => {
Expand Down Expand Up @@ -130,8 +146,83 @@ test('it should label the PR associated with cheery picks in the current branch'
"Found latest tag: v7.2.1",
"Looking at cherry pick commits since v7.2.1",
"Found the following picks : Commit: 930b47f011f750c44a1782267d698ccdd3c04da3 PR: [#55](https://github.com/storybookjs/storybook/pull/55)",
"Labeling the PRs with the patch:done label...",
"Labeling 1 PRs with the patch:done label...",
"Successfully labeled all PRs with the patch:done label.",
]
`);
});

test('it should label all PRs when the --all flag is passed', async () => {
process.env.GH_TOKEN = 'MY_SECRET';

// clear the git log, it shouldn't depend on it in --all mode
gitClient.git.log.mockResolvedValue({
all: [],
latest: null!,
total: 0,
});

const writeStderr = jest.spyOn(process.stderr, 'write').mockImplementation();

await run({ all: true });
expect(github.githubGraphQlClient.mock.calls).toMatchInlineSnapshot(`
[
[
"
mutation ($input: AddLabelsToLabelableInput!) {
addLabelsToLabelable(input: $input) {
clientMutationId
}
}
",
{
"input": {
"clientMutationId": "39cffd21-7933-56e4-9d9c-1afeda9d5906",
"labelIds": [
"pick-id",
],
"labelableId": "some-id",
},
},
],
[
"
mutation ($input: AddLabelsToLabelableInput!) {
addLabelsToLabelable(input: $input) {
clientMutationId
}
}
",
{
"input": {
"clientMutationId": "cc31033b-5da7-5c9e-adf2-80a2963e19a8",
"labelIds": [
"pick-id",
],
"labelableId": "other-id",
},
},
],
]
`);

const stderrCalls = writeStderr.mock.calls
.map(([text]) =>
typeof text === 'string'
? text
.replace(ansiRegex(), '')
.replace(/[^\x20-\x7E]/g, '')
.replaceAll('-', '')
.trim()
: text
)
.filter((it) => it !== '');

expect(stderrCalls).toMatchInlineSnapshot(`
[
"Labeling 2 PRs with the patch:done label...",
"Successfully labeled all PRs with the patch:done label.",
]
`);
expect(github.getUnpickedPRs).toHaveBeenCalledTimes(1);
});
4 changes: 4 additions & 0 deletions scripts/release/generate-pr-description.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ export const generateReleaseDescription = ({

If you've made any changes doing the above QA (change PR titles, revert PRs), manually trigger a re-generation of this PR with [this workflow](${workflowUrl}) and wait for it to finish. It will wipe your progress in this to do, which is expected.

Feel free to manually commit any changes necessary to this branch **after** you've done the last re-generation, following the [Make Manual Changes](https://github.com/storybookjs/storybook/blob/next/CONTRIBUTING/RELEASING.md#5-make-manual-changes) section in the docs, *especially* if you're making changes to the changelog.

When everything above is done:
- Merge this PR
- [Follow the run of the publish action](https://github.com/storybookjs/storybook/actions/workflows/publish.yml)
Expand Down Expand Up @@ -215,6 +217,8 @@ export const generateNonReleaseDescription = (

If you've made any changes (change PR titles, revert PRs), manually trigger a re-generation of this PR with [this workflow](https://github.com/storybookjs/storybook/actions/workflows/prepare-patch-release.yml) and wait for it to finish.

Feel free to manually commit any changes necessary to this branch **after** you've done the last re-generation, following the [Make Manual Changes](https://github.com/storybookjs/storybook/blob/next/CONTRIBUTING/RELEASING.md#5-make-manual-changes) section in the docs.

When everything above is done:
- Merge this PR
- [Follow the run of the publish action](https://github.com/storybookjs/storybook/actions/workflows/publish.yml)`
Expand Down
47 changes: 33 additions & 14 deletions scripts/release/label-patches.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import program from 'commander';
import { v4 as uuidv4 } from 'uuid';
import ora from 'ora';
import { getLabelIds, githubGraphQlClient } from './utils/github-client';
import { getLabelIds, githubGraphQlClient, getUnpickedPRs } from './utils/github-client';
import { getPullInfoFromCommits, getRepo } from './utils/get-changes';
import { getLatestTag, git } from './utils/git-client';

program
.name('label-patches')
.description('Label all patches applied in current branch up to the latest release tag.');
.description('Label all patches applied in current branch up to the latest release tag.')
.option(
'-A, --all',
'Label all pull requests pending patches, iregardless if they are in the git log or not',
false
);

async function labelPR(id: string, labelId: string) {
await githubGraphQlClient(
Expand All @@ -22,11 +27,7 @@ async function labelPR(id: string, labelId: string) {
);
}

export const run = async (_: unknown) => {
if (!process.env.GH_TOKEN) {
throw new Error('GH_TOKEN environment variable must be set, exiting.');
}

async function getPullRequestsFromLog({ repo }: { repo: string }) {
const spinner = ora('Looking for latest tag').start();
const latestTag = await getLatestTag();
spinner.succeed(`Found latest tag: ${latestTag}`);
Expand All @@ -41,10 +42,8 @@ export const run = async (_: unknown) => {

if (cherryPicked.length === 0) {
spinner2.fail('No cherry pick commits found to label.');
return;
return [];
}

const repo = await getRepo();
const pullRequests = (
await getPullInfoFromCommits({
repo,
Expand All @@ -56,17 +55,37 @@ export const run = async (_: unknown) => {
spinner2.fail(
`Found picks: ${cherryPicked.join(', ')}, but no associated pull request found to label.`
);
return;
return pullRequests;
}

const commitWithPr = pullRequests.map((pr) => `Commit: ${pr.commit}\n PR: ${pr.links.pull}`);

spinner2.succeed(`Found the following picks 🍒:\n ${commitWithPr.join('\n')}`);

const spinner3 = ora(`Labeling the PRs with the patch:done label...`).start();
return pullRequests;
}

export const run = async (options: unknown) => {
if (!process.env.GH_TOKEN) {
throw new Error('GH_TOKEN environment variable must be set, exiting.');
}

const repo = await getRepo();
const labelAll = typeof options === 'object' && 'all' in options && Boolean(options.all);

const pullRequestsToLabel = labelAll
? await getUnpickedPRs('next')
: await getPullRequestsFromLog({ repo });
if (pullRequestsToLabel.length === 0) {
return;
}

const spinner3 = ora(
`Labeling ${pullRequestsToLabel.length} PRs with the patch:done label...`
).start();
try {
const labelToId = await getLabelIds({ repo, labelNames: ['patch:done'] });
await Promise.all(pullRequests.map((pr) => labelPR(pr.id, labelToId['patch:done'])));
await Promise.all(pullRequestsToLabel.map((pr) => labelPR(pr.id, labelToId['patch:done'])));
spinner3.succeed(`Successfully labeled all PRs with the patch:done label.`);
} catch (e) {
spinner3.fail(`Something went wrong when labelling the PRs.`);
Expand All @@ -75,7 +94,7 @@ export const run = async (_: unknown) => {
};

if (require.main === module) {
const options = program.parse(process.argv);
const options = program.parse().opts();
run(options).catch((err) => {
console.error(err);
process.exit(1);
Expand Down
Loading