mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-08 06:23:00 +08:00
add release scripts
This commit is contained in:
parent
32d2fafa8d
commit
529afae088
@ -12,6 +12,18 @@
|
||||
"lint:js:cmd": "cross-env NODE_ENV=production eslint --cache --cache-location=../.cache/eslint --ext .js,.jsx,.json,.html,.ts,.tsx,.mjs --report-unused-disable-directives",
|
||||
"lint:package": "sort-package-json",
|
||||
"migrate-docs": "node --require esbuild-register ./ts-to-ts49.ts",
|
||||
"release:generate-pr-description": "ts-node --swc ./release/generate-pr-description.ts",
|
||||
"release:get-changelog-from-file": "ts-node --swc ./release/get-changelog-from-file.ts",
|
||||
"release:get-current-version": "ts-node --swc ./release/get-current-version.ts",
|
||||
"release:get-version-changelog": "ts-node --swc ./release/get-version-changelog.ts",
|
||||
"release:is-pr-frozen": "ts-node --swc ./release/is-pr-frozen.ts",
|
||||
"release:is-prerelease": "ts-node --swc ./release/is-prerelease.ts",
|
||||
"release:is-version-published": "ts-node --swc ./release/is-version-published.ts",
|
||||
"release:pick-patches": "ts-node --swc ./release/pick-patches.ts",
|
||||
"release:publish": "ts-node --swc ./release/publish.ts",
|
||||
"release:unreleased-changes-exists": "ts-node --swc ./release/unreleased-changes-exists.ts",
|
||||
"release:version": "ts-node --swc ./release/version.ts",
|
||||
"release:write-changelog": "ts-node --swc ./release/write-changelog.ts",
|
||||
"strict-ts": "node --require esbuild-register ./strict-ts.ts",
|
||||
"task": "ts-node --swc ./task.ts",
|
||||
"test": "jest --config ./jest.config.js",
|
||||
|
391
scripts/release/__tests__/generate-pr-description.test.ts
Normal file
391
scripts/release/__tests__/generate-pr-description.test.ts
Normal file
@ -0,0 +1,391 @@
|
||||
import {
|
||||
generateReleaseDescription,
|
||||
generateNonReleaseDescription,
|
||||
mapToChangelist,
|
||||
mapCherryPicksToTodo,
|
||||
} from '../generate-pr-description';
|
||||
import type { Change } from '../utils/get-changes';
|
||||
|
||||
describe('Generate PR Description', () => {
|
||||
const changes: Change[] = [
|
||||
{
|
||||
user: 'JReinhold',
|
||||
title: 'Some PR title for a bug',
|
||||
labels: ['bug', 'build', 'other label', 'patch'],
|
||||
commit: 'abc123',
|
||||
pull: '42',
|
||||
links: {
|
||||
commit: '[abc123](https://github.com/storybookjs/storybook/commit/abc123)',
|
||||
pull: '[#42](https://github.com/storybookjs/storybook/pull/42)',
|
||||
user: '[@JReinhold](https://github.com/JReinhold)',
|
||||
},
|
||||
},
|
||||
{
|
||||
// this Bump version commit should be ignored
|
||||
user: 'github-actions[bot]',
|
||||
pull: null,
|
||||
commit: '012b58140c3606efeacbe99c0c410624b0a1ed1f',
|
||||
title: 'Bump version on `next`: preminor (alpha) from 7.2.0 to 7.3.0-alpha.0',
|
||||
labels: null,
|
||||
links: {
|
||||
commit:
|
||||
'[`012b58140c3606efeacbe99c0c410624b0a1ed1f`](https://github.com/storybookjs/monorepo-release-tooling-prototype/commit/012b58140c3606efeacbe99c0c410624b0a1ed1f)',
|
||||
pull: null,
|
||||
user: '[@github-actions[bot]](https://github.com/github-actions%5Bbot%5D)',
|
||||
},
|
||||
},
|
||||
{
|
||||
user: 'shilman',
|
||||
title: 'Some title for a "direct commit"',
|
||||
labels: null,
|
||||
commit: '22bb11',
|
||||
pull: null,
|
||||
links: {
|
||||
commit: '[22bb11](https://github.com/storybookjs/storybook/commit/22bb11)',
|
||||
pull: null,
|
||||
user: '[@shilman](https://github.com/shilman)',
|
||||
},
|
||||
},
|
||||
{
|
||||
user: 'shilman',
|
||||
title: 'Another PR `title` for docs',
|
||||
labels: ['another label', 'documentation', 'patch'],
|
||||
commit: 'ddd222',
|
||||
pull: '11',
|
||||
links: {
|
||||
commit: '[ddd222](https://github.com/storybookjs/storybook/commit/ddd222)',
|
||||
pull: '[#11](https://github.com/storybookjs/storybook/pull/11)',
|
||||
user: '[@shilman](https://github.com/shilman)',
|
||||
},
|
||||
},
|
||||
{
|
||||
user: 'JReinhold',
|
||||
title: "Some PR title for a 'new' feature",
|
||||
labels: ['feature request', 'other label'],
|
||||
commit: 'wow1337',
|
||||
pull: '48',
|
||||
links: {
|
||||
commit: '[wow1337](https://github.com/storybookjs/storybook/commit/wow1337)',
|
||||
pull: '[#48](https://github.com/storybookjs/storybook/pull/48)',
|
||||
user: '[@JReinhold](https://github.com/JReinhold)',
|
||||
},
|
||||
},
|
||||
{
|
||||
user: 'JReinhold',
|
||||
title: 'Some PR title with a missing label',
|
||||
labels: ['incorrect label', 'other label'],
|
||||
commit: 'bad999',
|
||||
pull: '77',
|
||||
links: {
|
||||
commit: '[bad999](https://github.com/storybookjs/storybook/commit/bad999)',
|
||||
pull: '[#77](https://github.com/storybookjs/storybook/pull/77)',
|
||||
user: '[@JReinhold](https://github.com/JReinhold)',
|
||||
},
|
||||
},
|
||||
];
|
||||
describe('mapToChangelist', () => {
|
||||
it('should return a correct string for releases', () => {
|
||||
expect(mapToChangelist({ changes, isRelease: true })).toMatchInlineSnapshot(`
|
||||
"- **🐛 Bug**: Some PR title for a bug [#42](https://github.com/storybookjs/storybook/pull/42)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- [ ] The PR is labeled correctly
|
||||
- [ ] The PR title is correct
|
||||
- **⚠️ Direct commit**: Some title for a "direct commit" [22bb11](https://github.com/storybookjs/storybook/commit/22bb11)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- **📝 Documentation**: Another PR \`title\` for docs [#11](https://github.com/storybookjs/storybook/pull/11)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- [ ] The PR is labeled correctly
|
||||
- [ ] The PR title is correct
|
||||
- **✨ Feature Request**: Some PR title for a 'new' feature [#48](https://github.com/storybookjs/storybook/pull/48)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- [ ] The PR is labeled correctly
|
||||
- [ ] The PR title is correct
|
||||
- **❔ Missing Label**: Some PR title with a missing label [#77](https://github.com/storybookjs/storybook/pull/77)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- [ ] The PR is labeled correctly
|
||||
- [ ] The PR title is correct"
|
||||
`);
|
||||
});
|
||||
it('should return a correct string for non-releases', () => {
|
||||
expect(mapToChangelist({ changes, isRelease: false })).toMatchInlineSnapshot(`
|
||||
"- **🐛 Bug**: Some PR title for a bug [#42](https://github.com/storybookjs/storybook/pull/42)
|
||||
- **⚠️ Direct commit**: Some title for a "direct commit" [22bb11](https://github.com/storybookjs/storybook/commit/22bb11)
|
||||
- **📝 Documentation**: Another PR \`title\` for docs [#11](https://github.com/storybookjs/storybook/pull/11)
|
||||
- **✨ Feature Request**: Some PR title for a 'new' feature [#48](https://github.com/storybookjs/storybook/pull/48)
|
||||
- **❔ Missing Label**: Some PR title with a missing label [#77](https://github.com/storybookjs/storybook/pull/77)"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapCherryPicksToTodo', () => {
|
||||
it('should return a correct string for releases', () => {
|
||||
expect(mapCherryPicksToTodo({ changes, commits: ['abc123'] })).toMatchInlineSnapshot(`
|
||||
"## 🍒 Manual cherry picking needed!
|
||||
|
||||
The following pull requests could not be cherry-picked automatically because it resulted in merge conflicts.
|
||||
For each pull request below, you need to either manually cherry pick it, or discard it by removing the "patch" label from the PR and re-generate this PR.
|
||||
|
||||
- [ ] [#42](https://github.com/storybookjs/storybook/pull/42): \`git cherry-pick -m1 -x abc123\`"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('description generator', () => {
|
||||
const changeList = `- **🐛 Bug**: Some PR title for a bug [#42](https://github.com/storybookjs/storybook/pull/42)
|
||||
\t- [ ] The change is appropriate for the version bump
|
||||
\t- [ ] The PR is labeled correctly
|
||||
\t- [ ] The PR title is correct
|
||||
- **⚠️ Direct commit**: Some title for a \\"direct commit\\" [22bb11](https://github.com/storybookjs/storybook/commit/22bb11)
|
||||
\t- [ ] The change is appropriate for the version bump
|
||||
- **📝 Documentation**: Another PR \\\`title\\\` for docs [#11](https://github.com/storybookjs/storybook/pull/11)
|
||||
\t- [ ] The change is appropriate for the version bump
|
||||
\t- [ ] The PR is labeled correctly
|
||||
\t- [ ] The PR title is correct
|
||||
- **✨ Feature Request**: Some PR title for a \\'new\\' feature [#48](https://github.com/storybookjs/storybook/pull/48)
|
||||
\t- [ ] The change is appropriate for the version bump
|
||||
\t- [ ] The PR is labeled correctly
|
||||
\t- [ ] The PR title is correct
|
||||
- **⚠️ Missing Label**: Some PR title with a missing label [#77](https://github.com/storybookjs/storybook/pull/77)
|
||||
\t- [ ] The change is appropriate for the version bump
|
||||
\t- [ ] The PR is labeled correctly
|
||||
\t- [ ] The PR title is correct`;
|
||||
|
||||
const manualCherryPicks = `## 🍒 Manual cherry picking needed!
|
||||
|
||||
The following pull requests could not be cherry-picked automatically because it resulted in merge conflicts.
|
||||
For each pull request below, you need to either manually cherry pick it, or discard it by removing the "patch" label from the PR and re-generate this PR.
|
||||
|
||||
- [ ] [#42](https://github.com/storybookjs/storybook/pull/42): \`git cherry-pick -m1 -x abc123\``;
|
||||
|
||||
it('should return a correct string with cherry picks for releases', () => {
|
||||
const changelogText = `## 7.1.0-alpha.11
|
||||
|
||||
- Some PR \`title\` for a bug [#42](https://github.com/storybookjs/storybook/pull/42), thanks [@JReinhold](https://github.com/JReinhold)
|
||||
- Some PR 'title' for a feature request [#48](https://github.com/storybookjs/storybook/pull/48), thanks [@JReinhold](https://github.com/JReinhold)
|
||||
- Antoher PR "title" for maintainance [#49](https://github.com/storybookjs/storybook/pull/49), thanks [@JReinhold](https://github.com/JReinhold)`;
|
||||
expect(
|
||||
generateReleaseDescription({
|
||||
currentVersion: '7.1.0-alpha.10',
|
||||
nextVersion: '7.1.0-alpha.11',
|
||||
changeList,
|
||||
changelogText,
|
||||
manualCherryPicks,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
"This is an automated pull request that bumps the version from \\\`7.1.0-alpha.10\\\` to \\\`7.1.0-alpha.11\\\`.
|
||||
Once this pull request is merged, it will trigger a new release of version \\\`7.1.0-alpha.11\\\`.
|
||||
If you\\'re not a core maintainer with permissions to release you can ignore this pull request.
|
||||
|
||||
## To do
|
||||
|
||||
Before merging the PR, there are a few QA steps to go through:
|
||||
|
||||
- [ ] Add the \\"freeze\\" label to this PR, to ensure it doesn\\'t get automatically forced pushed by new changes.
|
||||
|
||||
And for each change below:
|
||||
|
||||
1. Ensure the change is appropriate for the version bump. E.g. patch release should only contain patches, not new or de-stabilizing features. If a change is not appropriate, revert the PR.
|
||||
2. Ensure the PR is labeled correctly with \\"BREAKING CHANGE\\", \\"feature request\\", \\"maintainance\\", \\"bug\\", \\"build\\" or \\"documentation\\".
|
||||
3. Ensure the PR title is correct, and follows the format \\"[Area]: [Summary]\\", e.g. *\\"React: Fix hooks in CSF3 render functions\\"*. If it is not correct, change the title in the PR.
|
||||
- Areas include: React, Vue, Core, Docs, Controls, etc.
|
||||
- First word of summary indicates the type: “Add”, “Fix”, “Upgrade”, etc.
|
||||
- The entire title should fit on a line
|
||||
|
||||
This is a list of all the PRs merged and commits pushed directly to \\\`next\\\`, that will be part of this release:
|
||||
|
||||
- **🐛 Bug**: Some PR title for a bug [#42](https://github.com/storybookjs/storybook/pull/42)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- [ ] The PR is labeled correctly
|
||||
- [ ] The PR title is correct
|
||||
- **⚠️ Direct commit**: Some title for a \\\\"direct commit\\\\" [22bb11](https://github.com/storybookjs/storybook/commit/22bb11)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- **📝 Documentation**: Another PR \\\\\`title\\\\\` for docs [#11](https://github.com/storybookjs/storybook/pull/11)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- [ ] The PR is labeled correctly
|
||||
- [ ] The PR title is correct
|
||||
- **✨ Feature Request**: Some PR title for a \\\\'new\\\\' feature [#48](https://github.com/storybookjs/storybook/pull/48)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- [ ] The PR is labeled correctly
|
||||
- [ ] The PR title is correct
|
||||
- **⚠️ Missing Label**: Some PR title with a missing label [#77](https://github.com/storybookjs/storybook/pull/77)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- [ ] The PR is labeled correctly
|
||||
- [ ] The PR title is correct
|
||||
|
||||
## 🍒 Manual cherry picking needed!
|
||||
|
||||
The following pull requests could not be cherry-picked automatically because it resulted in merge conflicts.
|
||||
For each pull request below, you need to either manually cherry pick it, or discard it by removing the \\"patch\\" label from the PR and re-generate this PR.
|
||||
|
||||
- [ ] [#42](https://github.com/storybookjs/storybook/pull/42): \\\`git cherry-pick -m1 -x abc123\\\`
|
||||
|
||||
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/monorepo-release-tooling-prototype/actions/workflows/prepare-prerelease.yml) and wait for it to finish. It will wipe your progress in this to do, which is expected.
|
||||
|
||||
When everything above is done:
|
||||
- [ ] Merge this PR
|
||||
- [ ] [Follow the publish workflow run and see it finishes succesfully](https://github.com/storybookjs/monorepo-release-tooling-prototype/actions/workflows/publish.yml)
|
||||
|
||||
---
|
||||
|
||||
# Generated changelog
|
||||
|
||||
## 7.1.0-alpha.11
|
||||
|
||||
- Some PR \\\`title\\\` for a bug [#42](https://github.com/storybookjs/storybook/pull/42), thanks [@ JReinhold](https://github.com/JReinhold)
|
||||
- Some PR \\'title\\' for a feature request [#48](https://github.com/storybookjs/storybook/pull/48), thanks [@ JReinhold](https://github.com/JReinhold)
|
||||
- Antoher PR \\"title\\" for maintainance [#49](https://github.com/storybookjs/storybook/pull/49), thanks [@ JReinhold](https://github.com/JReinhold)"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should return a correct string for non-releases with cherry picks', () => {
|
||||
expect(generateNonReleaseDescription(changeList, manualCherryPicks)).toMatchInlineSnapshot(`
|
||||
"This is an automated pull request. None of the changes requires a version bump, they are only internal or documentation related. Merging this PR will not trigger a new release, but documentation will be updated.
|
||||
If you\\'re not a core maintainer with permissions to release you can ignore this pull request.
|
||||
|
||||
This is a list of all the PRs merged and commits pushed directly to \\\`next\\\` since the last release:
|
||||
|
||||
- **🐛 Bug**: Some PR title for a bug [#42](https://github.com/storybookjs/storybook/pull/42)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- [ ] The PR is labeled correctly
|
||||
- [ ] The PR title is correct
|
||||
- **⚠️ Direct commit**: Some title for a \\\\"direct commit\\\\" [22bb11](https://github.com/storybookjs/storybook/commit/22bb11)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- **📝 Documentation**: Another PR \\\\\`title\\\\\` for docs [#11](https://github.com/storybookjs/storybook/pull/11)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- [ ] The PR is labeled correctly
|
||||
- [ ] The PR title is correct
|
||||
- **✨ Feature Request**: Some PR title for a \\\\'new\\\\' feature [#48](https://github.com/storybookjs/storybook/pull/48)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- [ ] The PR is labeled correctly
|
||||
- [ ] The PR title is correct
|
||||
- **⚠️ Missing Label**: Some PR title with a missing label [#77](https://github.com/storybookjs/storybook/pull/77)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- [ ] The PR is labeled correctly
|
||||
- [ ] The PR title is correct
|
||||
|
||||
## 🍒 Manual cherry picking needed!
|
||||
|
||||
The following pull requests could not be cherry-picked automatically because it resulted in merge conflicts.
|
||||
For each pull request below, you need to either manually cherry pick it, or discard it by removing the \\"patch\\" label from the PR and re-generate this PR.
|
||||
|
||||
- [ ] [#42](https://github.com/storybookjs/storybook/pull/42): \\\`git cherry-pick -m1 -x abc123\\\`
|
||||
|
||||
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/monorepo-release-tooling-prototype/actions/workflows/prepare-prerelease.yml) and wait for it to finish.
|
||||
|
||||
When everything above is done:
|
||||
- [ ] Merge this PR
|
||||
- [ ] [Approve the publish workflow run](https://github.com/storybookjs/monorepo-release-tooling-prototype/actions/workflows/publish.yml)"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should return a correct string without cherry picks for releases', () => {
|
||||
const changelogText = `## 7.1.0-alpha.11
|
||||
|
||||
- Some PR \`title\` for a bug [#42](https://github.com/storybookjs/storybook/pull/42), thanks [@JReinhold](https://github.com/JReinhold)
|
||||
- Some PR 'title' for a feature request [#48](https://github.com/storybookjs/storybook/pull/48), thanks [@JReinhold](https://github.com/JReinhold)
|
||||
- Antoher PR "title" for maintainance [#49](https://github.com/storybookjs/storybook/pull/49), thanks [@JReinhold](https://github.com/JReinhold)`;
|
||||
expect(
|
||||
generateReleaseDescription({
|
||||
currentVersion: '7.1.0-alpha.10',
|
||||
nextVersion: '7.1.0-alpha.11',
|
||||
changeList,
|
||||
changelogText,
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
"This is an automated pull request that bumps the version from \\\`7.1.0-alpha.10\\\` to \\\`7.1.0-alpha.11\\\`.
|
||||
Once this pull request is merged, it will trigger a new release of version \\\`7.1.0-alpha.11\\\`.
|
||||
If you\\'re not a core maintainer with permissions to release you can ignore this pull request.
|
||||
|
||||
## To do
|
||||
|
||||
Before merging the PR, there are a few QA steps to go through:
|
||||
|
||||
- [ ] Add the \\"freeze\\" label to this PR, to ensure it doesn\\'t get automatically forced pushed by new changes.
|
||||
|
||||
And for each change below:
|
||||
|
||||
1. Ensure the change is appropriate for the version bump. E.g. patch release should only contain patches, not new or de-stabilizing features. If a change is not appropriate, revert the PR.
|
||||
2. Ensure the PR is labeled correctly with \\"BREAKING CHANGE\\", \\"feature request\\", \\"maintainance\\", \\"bug\\", \\"build\\" or \\"documentation\\".
|
||||
3. Ensure the PR title is correct, and follows the format \\"[Area]: [Summary]\\", e.g. *\\"React: Fix hooks in CSF3 render functions\\"*. If it is not correct, change the title in the PR.
|
||||
- Areas include: React, Vue, Core, Docs, Controls, etc.
|
||||
- First word of summary indicates the type: “Add”, “Fix”, “Upgrade”, etc.
|
||||
- The entire title should fit on a line
|
||||
|
||||
This is a list of all the PRs merged and commits pushed directly to \\\`next\\\`, that will be part of this release:
|
||||
|
||||
- **🐛 Bug**: Some PR title for a bug [#42](https://github.com/storybookjs/storybook/pull/42)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- [ ] The PR is labeled correctly
|
||||
- [ ] The PR title is correct
|
||||
- **⚠️ Direct commit**: Some title for a \\\\"direct commit\\\\" [22bb11](https://github.com/storybookjs/storybook/commit/22bb11)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- **📝 Documentation**: Another PR \\\\\`title\\\\\` for docs [#11](https://github.com/storybookjs/storybook/pull/11)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- [ ] The PR is labeled correctly
|
||||
- [ ] The PR title is correct
|
||||
- **✨ Feature Request**: Some PR title for a \\\\'new\\\\' feature [#48](https://github.com/storybookjs/storybook/pull/48)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- [ ] The PR is labeled correctly
|
||||
- [ ] The PR title is correct
|
||||
- **⚠️ Missing Label**: Some PR title with a missing label [#77](https://github.com/storybookjs/storybook/pull/77)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- [ ] The PR is labeled correctly
|
||||
- [ ] The PR title is correct
|
||||
|
||||
|
||||
|
||||
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/monorepo-release-tooling-prototype/actions/workflows/prepare-prerelease.yml) and wait for it to finish. It will wipe your progress in this to do, which is expected.
|
||||
|
||||
When everything above is done:
|
||||
- [ ] Merge this PR
|
||||
- [ ] [Follow the publish workflow run and see it finishes succesfully](https://github.com/storybookjs/monorepo-release-tooling-prototype/actions/workflows/publish.yml)
|
||||
|
||||
---
|
||||
|
||||
# Generated changelog
|
||||
|
||||
## 7.1.0-alpha.11
|
||||
|
||||
- Some PR \\\`title\\\` for a bug [#42](https://github.com/storybookjs/storybook/pull/42), thanks [@ JReinhold](https://github.com/JReinhold)
|
||||
- Some PR \\'title\\' for a feature request [#48](https://github.com/storybookjs/storybook/pull/48), thanks [@ JReinhold](https://github.com/JReinhold)
|
||||
- Antoher PR \\"title\\" for maintainance [#49](https://github.com/storybookjs/storybook/pull/49), thanks [@ JReinhold](https://github.com/JReinhold)"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should return a correct string for non-releases without cherry picks', () => {
|
||||
expect(generateNonReleaseDescription(changeList)).toMatchInlineSnapshot(`
|
||||
"This is an automated pull request. None of the changes requires a version bump, they are only internal or documentation related. Merging this PR will not trigger a new release, but documentation will be updated.
|
||||
If you\\'re not a core maintainer with permissions to release you can ignore this pull request.
|
||||
|
||||
This is a list of all the PRs merged and commits pushed directly to \\\`next\\\` since the last release:
|
||||
|
||||
- **🐛 Bug**: Some PR title for a bug [#42](https://github.com/storybookjs/storybook/pull/42)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- [ ] The PR is labeled correctly
|
||||
- [ ] The PR title is correct
|
||||
- **⚠️ Direct commit**: Some title for a \\\\"direct commit\\\\" [22bb11](https://github.com/storybookjs/storybook/commit/22bb11)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- **📝 Documentation**: Another PR \\\\\`title\\\\\` for docs [#11](https://github.com/storybookjs/storybook/pull/11)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- [ ] The PR is labeled correctly
|
||||
- [ ] The PR title is correct
|
||||
- **✨ Feature Request**: Some PR title for a \\\\'new\\\\' feature [#48](https://github.com/storybookjs/storybook/pull/48)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- [ ] The PR is labeled correctly
|
||||
- [ ] The PR title is correct
|
||||
- **⚠️ Missing Label**: Some PR title with a missing label [#77](https://github.com/storybookjs/storybook/pull/77)
|
||||
- [ ] The change is appropriate for the version bump
|
||||
- [ ] The PR is labeled correctly
|
||||
- [ ] The PR title is correct
|
||||
|
||||
|
||||
|
||||
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/monorepo-release-tooling-prototype/actions/workflows/prepare-prerelease.yml) and wait for it to finish.
|
||||
|
||||
When everything above is done:
|
||||
- [ ] Merge this PR
|
||||
- [ ] [Approve the publish workflow run](https://github.com/storybookjs/monorepo-release-tooling-prototype/actions/workflows/publish.yml)"
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
55
scripts/release/__tests__/is-pr-frozen.test.ts
Normal file
55
scripts/release/__tests__/is-pr-frozen.test.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import path from "path";
|
||||
import { run as isPrFrozen } from "../is-pr-frozen";
|
||||
|
||||
jest.mock('fs-extra', () => require('../../../code/__mocks__/fs-extra'));
|
||||
jest.mock('../utils/get-github-info');
|
||||
|
||||
const fsExtra = require('fs-extra');
|
||||
const simpleGit = require('simple-git');
|
||||
const { getPullInfoFromCommit } = require('../utils/get-github-info');
|
||||
|
||||
const CODE_DIR_PATH = path.join(__dirname, '..', '..', '..', 'code');
|
||||
const CODE_PACKAGE_JSON_PATH = path.join(CODE_DIR_PATH, 'package.json');
|
||||
|
||||
fsExtra.__setMockFiles({
|
||||
[CODE_PACKAGE_JSON_PATH]: JSON.stringify({ version: '1.0.0' }),
|
||||
});
|
||||
|
||||
describe('isPrFrozen', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return true when PR is frozen', async () => {
|
||||
getPullInfoFromCommit.mockResolvedValue({
|
||||
labels: ['freeze']
|
||||
});
|
||||
await expect(isPrFrozen({patch: false})).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when PR is not frozen', async () => {
|
||||
getPullInfoFromCommit.mockResolvedValue({
|
||||
labels: []
|
||||
});
|
||||
await expect(isPrFrozen({patch: false})).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('should look for patch PRs when patch is true', async () => {
|
||||
getPullInfoFromCommit.mockResolvedValue({
|
||||
labels: []
|
||||
});
|
||||
await isPrFrozen({patch: true});
|
||||
|
||||
expect(simpleGit.__fetch).toHaveBeenCalledWith('origin', 'version-from-patch-1.0.0', {'--depth': 1});
|
||||
});
|
||||
|
||||
it('should look for prerelease PRs when patch is false', async () => {
|
||||
getPullInfoFromCommit.mockResolvedValue({
|
||||
labels: []
|
||||
});
|
||||
await isPrFrozen({patch: false});
|
||||
|
||||
expect(simpleGit.__fetch).toHaveBeenCalledWith('origin', 'version-from-prerelease-1.0.0', {'--depth': 1});
|
||||
});
|
||||
});
|
180
scripts/release/__tests__/version.test.ts
Normal file
180
scripts/release/__tests__/version.test.ts
Normal file
@ -0,0 +1,180 @@
|
||||
/* eslint-disable global-require */
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import path from 'path';
|
||||
import { run as version } from '../version';
|
||||
|
||||
// eslint-disable-next-line jest/no-mocks-import
|
||||
jest.mock('fs-extra', () => require('../../../code/__mocks__/fs-extra'));
|
||||
const fsExtra = require('fs-extra');
|
||||
|
||||
jest.mock('../../utils/exec');
|
||||
const { execaCommand } = require('../../utils/exec');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('Version', () => {
|
||||
const CODE_DIR_PATH = path.join(__dirname, '..', '..', '..', 'code');
|
||||
const CODE_PACKAGE_JSON_PATH = path.join(CODE_DIR_PATH, 'package.json');
|
||||
const MANAGER_API_VERSION_PATH = path.join(
|
||||
CODE_DIR_PATH,
|
||||
'lib',
|
||||
'manager-api',
|
||||
'src',
|
||||
'version.ts'
|
||||
);
|
||||
const VERSIONS_PATH = path.join(CODE_DIR_PATH, 'lib', 'cli', 'src', 'versions.ts');
|
||||
|
||||
it('should throw when release type is invalid', async () => {
|
||||
fsExtra.__setMockFiles({
|
||||
[CODE_PACKAGE_JSON_PATH]: JSON.stringify({ version: '1.0.0' }),
|
||||
[MANAGER_API_VERSION_PATH]: `export const version = "1.0.0";`,
|
||||
[VERSIONS_PATH]: `export default { "@junk-temporary-prototypes/addon-a11y": "1.0.0" };`,
|
||||
});
|
||||
|
||||
await expect(version({ releaseType: 'invalid' })).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"[
|
||||
{
|
||||
"received": "invalid",
|
||||
"code": "invalid_enum_value",
|
||||
"options": [
|
||||
"major",
|
||||
"minor",
|
||||
"patch",
|
||||
"prerelease",
|
||||
"premajor",
|
||||
"preminor",
|
||||
"prepatch"
|
||||
],
|
||||
"path": [
|
||||
"releaseType"
|
||||
],
|
||||
"message": "Invalid enum value. Expected 'major' | 'minor' | 'patch' | 'prerelease' | 'premajor' | 'preminor' | 'prepatch', received 'invalid'"
|
||||
}
|
||||
]"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should throw when prerelease identifier is combined with non-pre release type', async () => {
|
||||
fsExtra.__setMockFiles({
|
||||
[CODE_PACKAGE_JSON_PATH]: JSON.stringify({ version: '1.0.0' }),
|
||||
[MANAGER_API_VERSION_PATH]: `export const version = "1.0.0";`,
|
||||
[VERSIONS_PATH]: `export default { "@junk-temporary-prototypes/addon-a11y": "1.0.0" };`,
|
||||
});
|
||||
|
||||
await expect(version({ releaseType: 'major', preId: 'alpha' })).rejects
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"[
|
||||
{
|
||||
"code": "custom",
|
||||
"message": "Using prerelease identifier requires one of release types: premajor, preminor, prepatch, prerelease",
|
||||
"path": []
|
||||
}
|
||||
]"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should throw when exact is combined with release type', async () => {
|
||||
fsExtra.__setMockFiles({
|
||||
[CODE_PACKAGE_JSON_PATH]: JSON.stringify({ version: '1.0.0' }),
|
||||
[MANAGER_API_VERSION_PATH]: `export const version = "1.0.0";`,
|
||||
[VERSIONS_PATH]: `export default { "@junk-temporary-prototypes/addon-a11y": "1.0.0" };`,
|
||||
});
|
||||
|
||||
await expect(version({ releaseType: 'major', exact: '1.0.0' })).rejects
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"[
|
||||
{
|
||||
"code": "custom",
|
||||
"message": "Combining --exact with --release-type is invalid, but having one of them is required",
|
||||
"path": []
|
||||
}
|
||||
]"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should throw when exact is invalid semver', async () => {
|
||||
fsExtra.__setMockFiles({
|
||||
[CODE_PACKAGE_JSON_PATH]: JSON.stringify({ version: '1.0.0' }),
|
||||
[MANAGER_API_VERSION_PATH]: `export const version = "1.0.0";`,
|
||||
[VERSIONS_PATH]: `export default { "@junk-temporary-prototypes/addon-a11y": "1.0.0" };`,
|
||||
});
|
||||
|
||||
await expect(version({ exact: 'not-semver' })).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"[
|
||||
{
|
||||
"code": "custom",
|
||||
"message": "--exact version has to be a valid semver string",
|
||||
"path": [
|
||||
"exact"
|
||||
]
|
||||
}
|
||||
]"
|
||||
`);
|
||||
});
|
||||
|
||||
it.each([
|
||||
// prettier-ignore
|
||||
{ releaseType: 'major', currentVersion: '1.1.1', expectedVersion: '2.0.0' },
|
||||
// prettier-ignore
|
||||
{ releaseType: 'minor', currentVersion: '1.1.1', expectedVersion: '1.2.0' },
|
||||
// prettier-ignore
|
||||
{ releaseType: 'patch', currentVersion: '1.1.1', expectedVersion: '1.1.2' },
|
||||
// prettier-ignore
|
||||
{ releaseType: 'premajor', preId: 'alpha', currentVersion: '1.1.1', expectedVersion: '2.0.0-alpha.0' },
|
||||
// prettier-ignore
|
||||
{ releaseType: 'preminor', preId: 'alpha', currentVersion: '1.1.1', expectedVersion: '1.2.0-alpha.0' },
|
||||
// prettier-ignore
|
||||
{ releaseType: 'prepatch', preId: 'alpha', currentVersion: '1.1.1', expectedVersion: '1.1.2-alpha.0' },
|
||||
// prettier-ignore
|
||||
{ releaseType: 'prerelease', currentVersion: '1.1.1-alpha.5', expectedVersion: '1.1.1-alpha.6' },
|
||||
// prettier-ignore
|
||||
{ releaseType: 'prerelease', preId: 'alpha', currentVersion: '1.1.1-alpha.5', expectedVersion: '1.1.1-alpha.6' },
|
||||
// prettier-ignore
|
||||
{ releaseType: 'prerelease', preId: 'beta', currentVersion: '1.1.1-alpha.10', expectedVersion: '1.1.1-beta.0' },
|
||||
// prettier-ignore
|
||||
{ releaseType: 'major', currentVersion: '1.1.1-rc.10', expectedVersion: '2.0.0' },
|
||||
// prettier-ignore
|
||||
{ releaseType: 'minor', currentVersion: '1.1.1-rc.10', expectedVersion: '1.2.0' },
|
||||
// prettier-ignore
|
||||
{ releaseType: 'patch', currentVersion: '1.1.1-rc.10', expectedVersion: '1.1.1' },
|
||||
// prettier-ignore
|
||||
{ exact: '4.2.0-canary.69', currentVersion: '1.1.1-rc.10', expectedVersion: '4.2.0-canary.69' },
|
||||
])(
|
||||
'bump with type: "$releaseType", pre id "$preId" or exact "$exact", from: $currentVersion, to: $expectedVersion',
|
||||
async ({ releaseType, preId, exact, currentVersion, expectedVersion }) => {
|
||||
fsExtra.__setMockFiles({
|
||||
[CODE_PACKAGE_JSON_PATH]: JSON.stringify({ version: currentVersion }),
|
||||
[MANAGER_API_VERSION_PATH]: `export const version = "${currentVersion}";`,
|
||||
[VERSIONS_PATH]: `export default { "@junk-temporary-prototypes/addon-a11y": "${currentVersion}" };`,
|
||||
});
|
||||
|
||||
await version({ releaseType, preId, exact });
|
||||
|
||||
expect(fsExtra.writeJson).toHaveBeenCalledWith(
|
||||
CODE_PACKAGE_JSON_PATH,
|
||||
{ version: expectedVersion },
|
||||
{ spaces: 2 }
|
||||
);
|
||||
expect(fsExtra.writeFile).toHaveBeenCalledWith(
|
||||
path.join(
|
||||
CODE_DIR_PATH,
|
||||
'.yarn',
|
||||
'versions',
|
||||
'generated-by-versions-script.yml'
|
||||
),
|
||||
expect.stringContaining(expectedVersion)
|
||||
);
|
||||
expect(execaCommand).toHaveBeenCalledWith('yarn version apply --all', { cwd: CODE_DIR_PATH });
|
||||
expect(fsExtra.writeFile).toHaveBeenCalledWith(
|
||||
MANAGER_API_VERSION_PATH,
|
||||
expect.stringContaining(expectedVersion)
|
||||
);
|
||||
expect(fsExtra.writeFile).toHaveBeenCalledWith(
|
||||
VERSIONS_PATH,
|
||||
expect.stringContaining(expectedVersion)
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
291
scripts/release/generate-pr-description.ts
Normal file
291
scripts/release/generate-pr-description.ts
Normal file
@ -0,0 +1,291 @@
|
||||
/* eslint-disable no-continue */
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
/* eslint-disable no-console */
|
||||
import chalk from 'chalk';
|
||||
import program from 'commander';
|
||||
import { z } from 'zod';
|
||||
import dedent from 'ts-dedent';
|
||||
import { setOutput } from '@actions/core';
|
||||
import type { Change } from './utils/get-changes';
|
||||
import { getChanges } from './utils/get-changes';
|
||||
import { getCurrentVersion } from './get-current-version';
|
||||
|
||||
program
|
||||
.name('generate-pr-description')
|
||||
.description('generate a PR description for a release')
|
||||
.option(
|
||||
'-C, --current-version <version>',
|
||||
'Which version to generate changelog from, eg. "7.0.7". Defaults to the version at code/package.json'
|
||||
)
|
||||
.option('-N, --next-version <version>', 'Which version to generate changelog to, eg. "7.0.8"')
|
||||
.option('-P, --unpicked-patches', 'Set to only consider PRs labeled with "patch" label')
|
||||
.option(
|
||||
'-M, --manual-cherry-picks <commits>',
|
||||
'A stringified JSON array of commit hashes, of patch PRs that needs to be cherry-picked manually'
|
||||
)
|
||||
.option('-V, --verbose', 'Enable verbose logging', false);
|
||||
|
||||
const optionsSchema = z.object({
|
||||
currentVersion: z.string().optional(),
|
||||
nextVersion: z.string().optional(),
|
||||
unpickedPatches: z.boolean().optional(),
|
||||
manualCherryPicks: z
|
||||
.string()
|
||||
.default('[]')
|
||||
.transform((val) => JSON.parse(val))
|
||||
.refine((val) => Array.isArray(val)),
|
||||
verbose: z.boolean().optional(),
|
||||
});
|
||||
|
||||
type Options = {
|
||||
currentVersion?: string;
|
||||
nextVersion?: string;
|
||||
unpickedPatches?: boolean;
|
||||
manualCherryPicks?: string[];
|
||||
verbose: boolean;
|
||||
};
|
||||
|
||||
const LABELS_BY_IMPORTANCE = {
|
||||
'BREAKING CHANGE': '❗ Breaking Change',
|
||||
'feature request': '✨ Feature Request',
|
||||
bug: '🐛 Bug',
|
||||
maintenance: '🔧 Maintenance',
|
||||
documentation: '📝 Documentation',
|
||||
build: '🏗️ Build',
|
||||
unknown: '❔ Missing Label',
|
||||
} as const;
|
||||
|
||||
const CHANGE_TITLES_TO_IGNORE = [/^bump version on.*/i, /^merge branch.*/i];
|
||||
|
||||
export const mapToChangelist = ({
|
||||
changes,
|
||||
isRelease,
|
||||
}: {
|
||||
changes: Change[];
|
||||
isRelease: boolean;
|
||||
}): string => {
|
||||
return changes
|
||||
.filter((change) => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const titleToIgnore of CHANGE_TITLES_TO_IGNORE) {
|
||||
if (change.title.match(titleToIgnore)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((change) => {
|
||||
const lines: string[] = [];
|
||||
if (!change.pull) {
|
||||
lines.push(`- **⚠️ Direct commit**: ${change.title} ${change.links.commit}`);
|
||||
if (isRelease) {
|
||||
lines.push('\t- [ ] The change is appropriate for the version bump');
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
const label = (change.labels
|
||||
?.filter((l) => Object.keys(LABELS_BY_IMPORTANCE).includes(l))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Object.keys(LABELS_BY_IMPORTANCE).indexOf(a) -
|
||||
Object.keys(LABELS_BY_IMPORTANCE).indexOf(b)
|
||||
)[0] || 'unknown') as keyof typeof LABELS_BY_IMPORTANCE;
|
||||
|
||||
lines.push(`- **${LABELS_BY_IMPORTANCE[label]}**: ${change.title} ${change.links.pull}`);
|
||||
|
||||
if (isRelease) {
|
||||
lines.push('\t- [ ] The change is appropriate for the version bump');
|
||||
lines.push('\t- [ ] The PR is labeled correctly');
|
||||
lines.push('\t- [ ] The PR title is correct');
|
||||
}
|
||||
return lines.join('\n');
|
||||
})
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
export const mapCherryPicksToTodo = ({
|
||||
commits,
|
||||
changes,
|
||||
verbose,
|
||||
}: {
|
||||
commits: string[];
|
||||
changes: Change[];
|
||||
verbose?: boolean;
|
||||
}): string => {
|
||||
const list = commits
|
||||
.map((commit) => {
|
||||
const change = changes.find((change) => change.commit === commit.substring(0, 7));
|
||||
if (!change) {
|
||||
throw new Error(
|
||||
`Cherry pick commit "${commit}" not found in changes, this should not happen?!`
|
||||
);
|
||||
}
|
||||
return `- [ ] ${change.links.pull}: \`git cherry-pick -m1 -x ${commit}\``;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
if (verbose) {
|
||||
console.log(`🍒 Cherry pick list:\n${list}`);
|
||||
}
|
||||
return dedent`## 🍒 Manual cherry picking needed!
|
||||
|
||||
The following pull requests could not be cherry-picked automatically because it resulted in merge conflicts.
|
||||
For each pull request below, you need to either manually cherry pick it, or discard it by removing the "patch" label from the PR and re-generate this PR.
|
||||
|
||||
${list}`;
|
||||
};
|
||||
|
||||
export const generateReleaseDescription = ({
|
||||
currentVersion,
|
||||
nextVersion,
|
||||
changeList,
|
||||
changelogText,
|
||||
manualCherryPicks,
|
||||
}: {
|
||||
currentVersion: string;
|
||||
nextVersion: string;
|
||||
changeList: string;
|
||||
changelogText: string;
|
||||
manualCherryPicks?: string;
|
||||
}): string => {
|
||||
return (
|
||||
dedent`This is an automated pull request that bumps the version from \`${currentVersion}\` to \`${nextVersion}\`.
|
||||
Once this pull request is merged, it will trigger a new release of version \`${nextVersion}\`.
|
||||
If you're not a core maintainer with permissions to release you can ignore this pull request.
|
||||
|
||||
## To do
|
||||
|
||||
Before merging the PR, there are a few QA steps to go through:
|
||||
|
||||
- [ ] Add the "freeze" label to this PR, to ensure it doesn't get automatically forced pushed by new changes.
|
||||
|
||||
And for each change below:
|
||||
|
||||
1. Ensure the change is appropriate for the version bump. E.g. patch release should only contain patches, not new or de-stabilizing features. If a change is not appropriate, revert the PR.
|
||||
2. Ensure the PR is labeled correctly with "BREAKING CHANGE", "feature request", "maintainance", "bug", "build" or "documentation".
|
||||
3. Ensure the PR title is correct, and follows the format "[Area]: [Summary]", e.g. *"React: Fix hooks in CSF3 render functions"*. If it is not correct, change the title in the PR.
|
||||
- Areas include: React, Vue, Core, Docs, Controls, etc.
|
||||
- First word of summary indicates the type: “Add”, “Fix”, “Upgrade”, etc.
|
||||
- The entire title should fit on a line
|
||||
|
||||
This is a list of all the PRs merged and commits pushed directly to \`next\`, that will be part of this release:
|
||||
|
||||
${changeList}
|
||||
|
||||
${manualCherryPicks ? manualCherryPicks : ''}
|
||||
|
||||
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/monorepo-release-tooling-prototype/actions/workflows/prepare-prerelease.yml) and wait for it to finish. It will wipe your progress in this to do, which is expected.
|
||||
|
||||
When everything above is done:
|
||||
- [ ] Merge this PR
|
||||
- [ ] [Follow the publish workflow run and see it finishes succesfully](https://github.com/storybookjs/monorepo-release-tooling-prototype/actions/workflows/publish.yml)
|
||||
|
||||
---
|
||||
|
||||
# Generated changelog
|
||||
|
||||
${changelogText}`
|
||||
// don't mention contributors in the release PR, to avoid spamming them
|
||||
.replaceAll('[@', '[@ ')
|
||||
.replaceAll('"', '\\"')
|
||||
.replaceAll('`', '\\`')
|
||||
.replaceAll("'", "\\'")
|
||||
);
|
||||
};
|
||||
|
||||
export const generateNonReleaseDescription = (
|
||||
changeList: string,
|
||||
manualCherryPicks?: string
|
||||
): string => {
|
||||
return (
|
||||
dedent`This is an automated pull request. None of the changes requires a version bump, they are only internal or documentation related. Merging this PR will not trigger a new release, but documentation will be updated.
|
||||
If you're not a core maintainer with permissions to release you can ignore this pull request.
|
||||
|
||||
This is a list of all the PRs merged and commits pushed directly to \`next\` since the last release:
|
||||
|
||||
${changeList}
|
||||
|
||||
${manualCherryPicks ? manualCherryPicks : ''}
|
||||
|
||||
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/monorepo-release-tooling-prototype/actions/workflows/prepare-prerelease.yml) and wait for it to finish.
|
||||
|
||||
When everything above is done:
|
||||
- [ ] Merge this PR
|
||||
- [ ] [Approve the publish workflow run](https://github.com/storybookjs/monorepo-release-tooling-prototype/actions/workflows/publish.yml)`
|
||||
// don't mention contributors in the release PR, to avoid spamming them
|
||||
.replaceAll('[@', '[@ ')
|
||||
.replaceAll('"', '\\"')
|
||||
.replaceAll('`', '\\`')
|
||||
.replaceAll("'", "\\'")
|
||||
);
|
||||
};
|
||||
|
||||
export const run = async (rawOptions: unknown) => {
|
||||
const { nextVersion, unpickedPatches, verbose, manualCherryPicks, ...options } =
|
||||
optionsSchema.parse(rawOptions) as Options;
|
||||
|
||||
if (!nextVersion) {
|
||||
console.log(
|
||||
'🚨 --next-version option not specified, generating PR description assuming no release is needed'
|
||||
);
|
||||
}
|
||||
|
||||
const currentVersion = options.currentVersion || (await getCurrentVersion());
|
||||
|
||||
console.log(
|
||||
`💬 Generating PR description for ${chalk.blue(nextVersion)} between ${chalk.green(
|
||||
currentVersion
|
||||
)} and ${chalk.green('HEAD')}`
|
||||
);
|
||||
|
||||
const { changes, changelogText } = await getChanges({
|
||||
version: nextVersion,
|
||||
from: `v${currentVersion}`,
|
||||
to: 'HEAD',
|
||||
unpickedPatches,
|
||||
verbose,
|
||||
});
|
||||
|
||||
const hasCherryPicks = manualCherryPicks?.length > 0;
|
||||
|
||||
const description = nextVersion
|
||||
? generateReleaseDescription({
|
||||
currentVersion,
|
||||
nextVersion,
|
||||
changeList: mapToChangelist({ changes, isRelease: true }),
|
||||
changelogText,
|
||||
...(hasCherryPicks && {
|
||||
manualCherryPicks: mapCherryPicksToTodo({
|
||||
commits: manualCherryPicks,
|
||||
changes,
|
||||
verbose,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
: generateNonReleaseDescription(
|
||||
mapToChangelist({ changes, isRelease: false }),
|
||||
hasCherryPicks ? mapCherryPicksToTodo({
|
||||
commits: manualCherryPicks,
|
||||
changes,
|
||||
verbose,
|
||||
}) : undefined
|
||||
);
|
||||
|
||||
if (process.env.GITHUB_ACTIONS === 'true') {
|
||||
setOutput('description', description);
|
||||
}
|
||||
console.log(`✅ Generated PR description for ${chalk.blue(nextVersion)}`);
|
||||
if (verbose) {
|
||||
console.log(description);
|
||||
}
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
const parsed = program.parse();
|
||||
run(parsed.opts()).catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
52
scripts/release/get-changelog-from-file.ts
Normal file
52
scripts/release/get-changelog-from-file.ts
Normal file
@ -0,0 +1,52 @@
|
||||
/* eslint-disable no-console */
|
||||
import { setOutput } from '@actions/core';
|
||||
import chalk from 'chalk';
|
||||
import { program } from 'commander';
|
||||
import { readFile } from 'fs-extra';
|
||||
import path from 'path';
|
||||
import semver from 'semver';
|
||||
import dedent from 'ts-dedent';
|
||||
import { getCurrentVersion } from './get-current-version';
|
||||
|
||||
program
|
||||
.name('get-changelog-from-file')
|
||||
.description(
|
||||
'get changelog entry for specific version. If not version argument specified it will use the current version in code/package.json'
|
||||
)
|
||||
.arguments('[version]')
|
||||
.option('-V, --verbose', 'Enable verbose logging', false);
|
||||
|
||||
export const getChangelogFromFile = async (args: { version?: string; verbose?: boolean }) => {
|
||||
const version = args.version || (await getCurrentVersion());
|
||||
const isPrerelease = semver.prerelease(version) !== null;
|
||||
const changelogFilename = isPrerelease ? 'CHANGELOG.prerelease.md' : 'CHANGELOG.md';
|
||||
const changelogPath = path.join(__dirname, '..', '..', changelogFilename);
|
||||
|
||||
console.log(`📝 Getting changelog from ${chalk.blue(changelogPath)}`);
|
||||
|
||||
const fullChangelog = await readFile(changelogPath, 'utf-8');
|
||||
const changelogForVersion = fullChangelog.split(/(^|\n)## /).find((v) => v.startsWith(version));
|
||||
if (!changelogForVersion) {
|
||||
throw new Error(
|
||||
`Could not find changelog entry for version ${chalk.blue(version)} in ${chalk.green(
|
||||
changelogPath
|
||||
)}`
|
||||
);
|
||||
}
|
||||
const result = `## ${changelogForVersion}`;
|
||||
|
||||
console.log(dedent`📝 Changelog entry found:
|
||||
${result}`);
|
||||
if (process.env.GITHUB_ACTIONS === 'true') {
|
||||
setOutput('changelog', result);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
const parsed = program.parse();
|
||||
getChangelogFromFile({ version: parsed.args[0], verbose: parsed.opts().verbose }).catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
28
scripts/release/get-current-version.ts
Normal file
28
scripts/release/get-current-version.ts
Normal file
@ -0,0 +1,28 @@
|
||||
/* eslint-disable no-continue */
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
/* eslint-disable no-console */
|
||||
import chalk from 'chalk';
|
||||
import { setOutput } from '@actions/core';
|
||||
import path from 'path';
|
||||
import { readJson } from 'fs-extra';
|
||||
|
||||
const CODE_DIR_PATH = path.join(__dirname, '..', '..', 'code');
|
||||
const CODE_PACKAGE_JSON_PATH = path.join(CODE_DIR_PATH, 'package.json');
|
||||
|
||||
export const getCurrentVersion = async () => {
|
||||
console.log(`📐 Reading current version of Storybook...`);
|
||||
const { version } = (await readJson(CODE_PACKAGE_JSON_PATH)) as { version: string };
|
||||
if (process.env.GITHUB_ACTIONS === 'true') {
|
||||
setOutput('current-version', version);
|
||||
}
|
||||
console.log(`📦 Current version is ${chalk.green(version)}`);
|
||||
return version;
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
getCurrentVersion().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
35
scripts/release/get-version-changelog.ts
Normal file
35
scripts/release/get-version-changelog.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/* eslint-disable no-console */
|
||||
import { setOutput } from '@actions/core';
|
||||
import chalk from 'chalk';
|
||||
import { program } from 'commander';
|
||||
import { getCurrentVersion } from './get-current-version';
|
||||
import { getChanges } from './utils/get-changes';
|
||||
|
||||
program
|
||||
.name('get-version-changelog')
|
||||
.description(
|
||||
'get changelog for specific version. If no version argument specified it will use the current version in code/package.json'
|
||||
)
|
||||
.arguments('[version]')
|
||||
.option('-V, --verbose', 'Enable verbose logging', false);
|
||||
|
||||
export const getVersionChangelog = async (args: { version?: string; verbose?: boolean }) => {
|
||||
const version = args.version || (await getCurrentVersion());
|
||||
|
||||
console.log(`📝 Getting changelog for version ${chalk.blue(version)}`);
|
||||
|
||||
const { changelogText } = await getChanges({ from: version, version, verbose: args.verbose });
|
||||
|
||||
if (process.env.GITHUB_ACTIONS === 'true') {
|
||||
setOutput('changelog', changelogText);
|
||||
}
|
||||
return changelogText;
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
const parsed = program.parse();
|
||||
getVersionChangelog({ version: parsed.args[0], verbose: parsed.opts().verbose }).catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
104
scripts/release/is-pr-frozen.ts
Normal file
104
scripts/release/is-pr-frozen.ts
Normal file
@ -0,0 +1,104 @@
|
||||
/* eslint-disable no-continue */
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
/* eslint-disable no-console */
|
||||
import chalk from 'chalk';
|
||||
import program from 'commander';
|
||||
import { simpleGit } from 'simple-git';
|
||||
import { setOutput } from '@actions/core';
|
||||
import path from 'path';
|
||||
import { readJson } from 'fs-extra';
|
||||
import { getPullInfoFromCommit } from './utils/get-github-info';
|
||||
|
||||
program
|
||||
.name('is-pr-frozen')
|
||||
.description(
|
||||
'returns true if the versioning pull request associated with the current branch has the "freeze" label'
|
||||
)
|
||||
.option('-P, --patch', 'Look for patch PR instead of prerelease PR', false)
|
||||
.option('-V, --verbose', 'Enable verbose logging', false);
|
||||
|
||||
const git = simpleGit();
|
||||
|
||||
const CODE_DIR_PATH = path.join(__dirname, '..', '..', 'code');
|
||||
const CODE_PACKAGE_JSON_PATH = path.join(CODE_DIR_PATH, 'package.json');
|
||||
|
||||
const getCurrentVersion = async () => {
|
||||
console.log(`📐 Reading current version of Storybook...`);
|
||||
const { version } = await readJson(CODE_PACKAGE_JSON_PATH);
|
||||
return version;
|
||||
};
|
||||
|
||||
const getRepo = async (verbose?: boolean): Promise<string> => {
|
||||
const remotes = await git.getRemotes(true);
|
||||
const originRemote = remotes.find((remote) => remote.name === 'origin');
|
||||
if (!originRemote) {
|
||||
console.error(
|
||||
'Could not determine repository URL because no remote named "origin" was found. Remotes found:'
|
||||
);
|
||||
console.dir(remotes, { depth: null, colors: true });
|
||||
throw new Error('No remote named "origin" found');
|
||||
}
|
||||
const pushUrl = originRemote.refs.push;
|
||||
const repo = pushUrl.replace(/\.git$/, '').replace(/.*:(\/\/github\.com\/)*/, '');
|
||||
if (verbose) {
|
||||
console.log(`📦 Extracted repo: ${chalk.blue(repo)}`);
|
||||
}
|
||||
return repo;
|
||||
};
|
||||
|
||||
export const run = async (options: unknown) => {
|
||||
const { verbose, patch } = options as { verbose?: boolean; patch?: boolean };
|
||||
|
||||
const version = await getCurrentVersion();
|
||||
const branch = `version-from-${patch ? 'patch' : 'prerelease'}-${version}`;
|
||||
|
||||
console.log(`💬 Determining if pull request from branch '${chalk.blue(branch)}' is frozen`);
|
||||
|
||||
console.log(`⬇️ Fetching remote 'origin/${branch}'...`);
|
||||
try {
|
||||
await git.fetch('origin', branch, { '--depth': 1 });
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`❗ Could not fetch remote 'origin/${branch}', it probably does not exist yet, which is okay`
|
||||
);
|
||||
console.warn(error);
|
||||
console.log(`💧 Pull request doesn't exist yet! 😎`);
|
||||
if (process.env.GITHUB_ACTIONS === 'true') {
|
||||
setOutput('frozen', false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const commit = await git.revparse(`origin/${branch}`);
|
||||
console.log(`🔍 Found commit: ${commit}`);
|
||||
|
||||
const repo = await getRepo(verbose);
|
||||
|
||||
const pullRequest = await getPullInfoFromCommit({ repo, commit }).catch((err) => {
|
||||
console.error(`🚨 Could not get pull requests from commit: ${commit}`);
|
||||
console.error(err);
|
||||
throw err;
|
||||
});
|
||||
console.log(`🔍 Found pull request:
|
||||
${JSON.stringify(pullRequest, null, 2)}`);
|
||||
|
||||
const isFrozen = pullRequest.labels?.includes('freeze');
|
||||
if (process.env.GITHUB_ACTIONS === 'true') {
|
||||
setOutput('frozen', isFrozen);
|
||||
}
|
||||
if (isFrozen) {
|
||||
console.log(`🧊 Pull request is frozen! 🥶`);
|
||||
} else {
|
||||
console.log(`🔥 Pull request is on fire! 🥵`);
|
||||
}
|
||||
return isFrozen;
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
const parsed = program.parse();
|
||||
run(parsed.opts()).catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
37
scripts/release/is-prerelease.ts
Normal file
37
scripts/release/is-prerelease.ts
Normal file
@ -0,0 +1,37 @@
|
||||
/* eslint-disable no-continue */
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
/* eslint-disable no-console */
|
||||
import chalk from 'chalk';
|
||||
import program from 'commander';
|
||||
import { setOutput } from '@actions/core';
|
||||
import semver from 'semver';
|
||||
import { getCurrentVersion } from './get-current-version';
|
||||
|
||||
program
|
||||
.name('is-prerelease')
|
||||
.description('returns true if the current version is a prerelease')
|
||||
.option('-V, --verbose', 'Enable verbose logging', false);
|
||||
|
||||
export const isPrerelease = async (versionArg?: string) => {
|
||||
const version = versionArg || (await getCurrentVersion());
|
||||
const result = semver.prerelease(version) !== null;
|
||||
|
||||
if (process.env.GITHUB_ACTIONS === 'true') {
|
||||
setOutput('prerelease', result);
|
||||
}
|
||||
console.log(
|
||||
`📦 Current version ${chalk.green(version)} ${
|
||||
result ? chalk.blue('IS') : chalk.red('IS NOT')
|
||||
} a prerelease`
|
||||
);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
isPrerelease().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
89
scripts/release/is-version-published.ts
Normal file
89
scripts/release/is-version-published.ts
Normal file
@ -0,0 +1,89 @@
|
||||
/* eslint-disable no-continue */
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
/* eslint-disable no-console */
|
||||
import chalk from 'chalk';
|
||||
import program from 'commander';
|
||||
import { setOutput } from '@actions/core';
|
||||
import fetch from 'node-fetch';
|
||||
import { getCurrentVersion } from './get-current-version';
|
||||
|
||||
program
|
||||
.name('is-prerelease [version]')
|
||||
.description('returns true if the current version is a prerelease')
|
||||
.arguments('[version]');
|
||||
|
||||
const isVersionPublished = async ({
|
||||
packageName,
|
||||
version,
|
||||
verbose,
|
||||
}: {
|
||||
packageName: string;
|
||||
version: string;
|
||||
verbose?: boolean;
|
||||
}) => {
|
||||
const prettyPackage = `${chalk.blue(packageName)}@${chalk.green(version)}`;
|
||||
console.log(`⛅ Checking if ${prettyPackage} is published...`);
|
||||
|
||||
if (verbose) {
|
||||
console.log(`Fetching from npm:`);
|
||||
console.log(`https://registry.npmjs.org/${chalk.blue(packageName)}/${chalk.green(version)}`);
|
||||
}
|
||||
const response = await fetch(`https://registry.npmjs.org/${packageName}/${version}`);
|
||||
if (response.status === 404) {
|
||||
console.log(`🌤️ ${prettyPackage} is not published`);
|
||||
return false;
|
||||
}
|
||||
if (response.status !== 200) {
|
||||
console.error(
|
||||
`Unexpected status code when checking the current version on npm: ${response.status}`
|
||||
);
|
||||
console.error(await response.text());
|
||||
throw new Error(
|
||||
`Unexpected status code when checking the current version on npm: ${response.status}`
|
||||
);
|
||||
}
|
||||
const data = await response.json();
|
||||
if (verbose) {
|
||||
console.log(`Response from npm:`);
|
||||
console.log(data);
|
||||
}
|
||||
if (data.version !== version) {
|
||||
// this should never happen
|
||||
console.error(
|
||||
`Unexpected version received when checking the current version on npm: ${data.version}`
|
||||
);
|
||||
console.error(JSON.stringify(data, null, 2));
|
||||
throw new Error(
|
||||
`Unexpected version received when checking the current version on npm: ${data.version}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`⛈️ ${prettyPackage} is published`);
|
||||
return true;
|
||||
};
|
||||
|
||||
export const run = async (args: unknown[], options: unknown) => {
|
||||
const { verbose } = options as { verbose?: boolean };
|
||||
|
||||
const version = (args[0] as string) || (await getCurrentVersion());
|
||||
|
||||
const isAlreadyPublished = await isVersionPublished({
|
||||
version,
|
||||
packageName: '@junk-temporary-prototypes/manager-api',
|
||||
verbose,
|
||||
});
|
||||
|
||||
if (process.env.GITHUB_ACTIONS === 'true') {
|
||||
setOutput('published', isAlreadyPublished);
|
||||
}
|
||||
return isAlreadyPublished;
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
const parsed = program.parse();
|
||||
run(parsed.args, parsed.opts()).catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
152
scripts/release/pick-patches.ts
Normal file
152
scripts/release/pick-patches.ts
Normal file
@ -0,0 +1,152 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import program from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { GraphQlQueryResponseData } from '@octokit/graphql';
|
||||
import ora from 'ora';
|
||||
import { simpleGit } from 'simple-git';
|
||||
import { getUnpickedPRs } from './utils/get-unpicked-prs';
|
||||
import { setOutput } from '@actions/core';
|
||||
import { githubGraphQlClient } from './utils/github-client';
|
||||
|
||||
program.name('pick-patches').description('Cherry pick patch PRs back to main');
|
||||
|
||||
const logger = console;
|
||||
|
||||
const OWNER = 'storybookjs';
|
||||
const REPO = 'monorepo-release-tooling-prototype';
|
||||
const SOURCE_BRANCH = 'next-v2';
|
||||
|
||||
const git = simpleGit();
|
||||
|
||||
interface PR {
|
||||
number: number;
|
||||
id: string;
|
||||
branch: string;
|
||||
title: string;
|
||||
mergeCommit: string;
|
||||
}
|
||||
|
||||
const LABEL = {
|
||||
PATCH: 'patch',
|
||||
PICKED: 'picked',
|
||||
DOCUMENTATION: 'documentation',
|
||||
} as const;
|
||||
|
||||
function formatPR(pr: PR): string {
|
||||
return `https://github.com/${OWNER}/${REPO}/pull/${pr.number} "${pr.title}" ${chalk.yellow(
|
||||
pr.mergeCommit
|
||||
)}`;
|
||||
}
|
||||
|
||||
// @ts-expect-error not used atm
|
||||
async function getLabelIds(labelNames: string[]) {
|
||||
const query = labelNames.join('+');
|
||||
const result = await githubGraphQlClient<GraphQlQueryResponseData>(
|
||||
`
|
||||
query ($owner: String!, $repo: String!, $q: String!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
labels(query: $q, first: 10) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
owner: OWNER,
|
||||
repo: REPO,
|
||||
q: query,
|
||||
}
|
||||
);
|
||||
|
||||
const { labels } = result.repository;
|
||||
const labelToId = {} as Record<string, string>;
|
||||
labels.nodes.forEach((label: { name: string; id: string }) => {
|
||||
labelToId[label.name] = label.id;
|
||||
});
|
||||
return labelToId;
|
||||
}
|
||||
|
||||
// @ts-expect-error not used atm
|
||||
async function labelPR(id: string, labelToId: Record<string, string>) {
|
||||
await githubGraphQlClient(
|
||||
`
|
||||
mutation ($input: AddLabelsToLabelableInput!) {
|
||||
addLabelsToLabelable(input: $input) {
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
input: {
|
||||
labelIds: [labelToId[LABEL.PICKED]],
|
||||
labelableId: id,
|
||||
clientMutationId: uuidv4(),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export const run = async (_: unknown) => {
|
||||
if (!process.env.GH_TOKEN) {
|
||||
logger.error('GH_TOKEN environment variable must be set, exiting.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const sourceBranch = SOURCE_BRANCH;
|
||||
|
||||
const spinner = ora('Searching for patch PRs to cherry-pick').start();
|
||||
|
||||
// const labelToId = await getLabelIds(Object.values(LABEL));
|
||||
const patchPRs = await getUnpickedPRs(sourceBranch);
|
||||
|
||||
if (patchPRs.length > 0) {
|
||||
spinner.succeed(`Found ${patchPRs.length} PRs to cherry-pick to main.`);
|
||||
} else {
|
||||
spinner.warn('No PRs found.');
|
||||
}
|
||||
|
||||
const failedCherryPicks: string[] = [];
|
||||
|
||||
for (const pr of patchPRs) {
|
||||
const spinner = ora(`Cherry picking #${pr.number}`).start();
|
||||
|
||||
try {
|
||||
await git.raw(['cherry-pick', '-m', '1', '-x', pr.mergeCommit]);
|
||||
spinner.succeed(`Picked: ${formatPR(pr)}`);
|
||||
} catch (pickError) {
|
||||
spinner.fail(`Failed to automatically pick: ${formatPR(pr)}`);
|
||||
logger.error(pickError.message);
|
||||
const abort = ora(`Aborting cherry pick for merge commit: ${pr.mergeCommit}`).start();
|
||||
try {
|
||||
await git.raw(['cherry-pick', '--abort']);
|
||||
abort.stop();
|
||||
} catch (abortError) {
|
||||
abort.warn(`Failed to abort cherry pick (${pr.mergeCommit})`);
|
||||
logger.error(pickError.message);
|
||||
}
|
||||
failedCherryPicks.push(pr.mergeCommit);
|
||||
spinner.info(
|
||||
`This PR can be picked manually with: ${chalk.grey(
|
||||
`git cherry-pick -m1 -x ${pr.mergeCommit}`
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.GITHUB_ACTIONS === 'true') {
|
||||
setOutput('failed-cherry-picks', JSON.stringify(failedCherryPicks));
|
||||
}
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
const options = program.parse(process.argv);
|
||||
run(options).catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
209
scripts/release/publish.ts
Normal file
209
scripts/release/publish.ts
Normal file
@ -0,0 +1,209 @@
|
||||
/* eslint-disable no-continue */
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
/* eslint-disable no-console */
|
||||
import chalk from 'chalk';
|
||||
import path from 'path';
|
||||
import program from 'commander';
|
||||
import semver from 'semver';
|
||||
import { z } from 'zod';
|
||||
import { readJson } from 'fs-extra';
|
||||
import fetch from 'node-fetch';
|
||||
import dedent from 'ts-dedent';
|
||||
import { execaCommand } from '../utils/exec';
|
||||
|
||||
program
|
||||
.name('publish')
|
||||
.description('publish all packages')
|
||||
.requiredOption(
|
||||
'-T, --tag <tag>',
|
||||
'Specify which distribution tag to set for the version being published. Required, since leaving it undefined would publish with the "latest" tag'
|
||||
)
|
||||
.option('-D, --dry-run', 'Do not publish, only output to shell', false)
|
||||
.option('-V, --verbose', 'Enable verbose logging', false);
|
||||
|
||||
const optionsSchema = z
|
||||
.object({
|
||||
tag: z.string(),
|
||||
verbose: z.boolean().optional(),
|
||||
dryRun: z.boolean().optional(),
|
||||
})
|
||||
.refine((schema) => (schema.tag ? !semver.valid(schema.tag) : true), {
|
||||
message:
|
||||
'The tag can not be a valid semver version, it must be a plain string like "next" or "latest"',
|
||||
});
|
||||
|
||||
type Options = {
|
||||
tag: string;
|
||||
verbose: boolean;
|
||||
dryRun?: boolean;
|
||||
};
|
||||
|
||||
const CODE_DIR_PATH = path.join(__dirname, '..', '..', 'code');
|
||||
const CODE_PACKAGE_JSON_PATH = path.join(CODE_DIR_PATH, 'package.json');
|
||||
|
||||
const validateOptions = (options: { [key: string]: any }): options is Options => {
|
||||
optionsSchema.parse(options);
|
||||
return true;
|
||||
};
|
||||
|
||||
const getCurrentVersion = async (verbose?: boolean) => {
|
||||
if (verbose) {
|
||||
console.log(`📐 Reading current version of Storybook...`);
|
||||
}
|
||||
const { version } = await readJson(CODE_PACKAGE_JSON_PATH);
|
||||
console.log(`📐 Current version of Storybook is ${chalk.green(version)}`);
|
||||
return version;
|
||||
};
|
||||
|
||||
const isCurrentVersionPublished = async ({
|
||||
packageName,
|
||||
currentVersion,
|
||||
verbose,
|
||||
}: {
|
||||
packageName: string;
|
||||
currentVersion: string;
|
||||
verbose?: boolean;
|
||||
}) => {
|
||||
const prettyPackage = `${chalk.blue(packageName)}@${chalk.green(currentVersion)}`;
|
||||
console.log(`⛅ Checking if ${prettyPackage} is published...`);
|
||||
|
||||
if (verbose) {
|
||||
console.log(`Fetching from npm:`);
|
||||
console.log(
|
||||
`https://registry.npmjs.org/${chalk.blue(packageName)}/${chalk.green(currentVersion)}`
|
||||
);
|
||||
}
|
||||
const response = await fetch(`https://registry.npmjs.org/${packageName}/${currentVersion}`);
|
||||
if (response.status === 404) {
|
||||
console.log(`🌤️ ${prettyPackage} is not published`);
|
||||
return false;
|
||||
}
|
||||
if (response.status !== 200) {
|
||||
console.error(
|
||||
`Unexpected status code when checking the current version on npm: ${response.status}`
|
||||
);
|
||||
console.error(await response.text());
|
||||
throw new Error(
|
||||
`Unexpected status code when checking the current version on npm: ${response.status}`
|
||||
);
|
||||
}
|
||||
const data = await response.json();
|
||||
if (verbose) {
|
||||
console.log(`Response from npm:`);
|
||||
console.log(data);
|
||||
}
|
||||
if (data.version !== currentVersion) {
|
||||
// this should never happen
|
||||
console.error(
|
||||
`Unexpected version received when checking the current version on npm: ${data.version}`
|
||||
);
|
||||
console.error(JSON.stringify(data, null, 2));
|
||||
throw new Error(
|
||||
`Unexpected version received when checking the current version on npm: ${data.version}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`⛈️ ${prettyPackage} is published`);
|
||||
return true;
|
||||
};
|
||||
|
||||
const buildAllPackages = async () => {
|
||||
console.log(`🏗️ Building all packages...`);
|
||||
await execaCommand('yarn task --task=compile --start-from=compile --no-link', {
|
||||
stdio: 'inherit',
|
||||
cwd: CODE_DIR_PATH,
|
||||
});
|
||||
console.log(`🏗️ Packages successfully built`);
|
||||
};
|
||||
|
||||
const publishAllPackages = async ({
|
||||
tag,
|
||||
verbose,
|
||||
dryRun,
|
||||
}: {
|
||||
tag: string;
|
||||
verbose?: boolean;
|
||||
dryRun?: boolean;
|
||||
}) => {
|
||||
console.log(`📦 Publishing all packages...`);
|
||||
const command = `yarn workspaces foreach --parallel --no-private --verbose npm publish --tolerate-republish --tag ${tag}`;
|
||||
if (verbose) {
|
||||
console.log(`📦 Executing: ${command}`);
|
||||
}
|
||||
if (dryRun) {
|
||||
console.log(`📦 Dry run, skipping publish. Would have executed:
|
||||
${chalk.blue(command)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Note this is to fool `ts-node` into not turning the `import()` into a `require()`.
|
||||
// See: https://github.com/TypeStrong/ts-node/discussions/1290
|
||||
// prettier-ignore
|
||||
const pRetry = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
||||
(await new Function('specifier', 'return import(specifier)')(
|
||||
'p-retry'
|
||||
)) as typeof import('p-retry')
|
||||
).default;
|
||||
/**
|
||||
* 'yarn npm publish' will fail if just one package fails to publish.
|
||||
* But it will continue through with all the other packages, and --tolerate-republish makes it okay to publish the same version again.
|
||||
* So we can safely retry the whole publishing process if it fails.
|
||||
* It's not uncommon for the registry to fail often, which Yarn catches by checking the registry after a package has been published.
|
||||
*/
|
||||
await pRetry(
|
||||
() =>
|
||||
execaCommand(command, {
|
||||
stdio: 'inherit',
|
||||
cwd: CODE_DIR_PATH,
|
||||
}),
|
||||
{
|
||||
retries: 4,
|
||||
onFailedAttempt: (error) =>
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
dedent`❗One or more packages failed to publish, retrying...
|
||||
This was attempt number ${error.attemptNumber}, there are ${error.retriesLeft} retries left. 🤞`
|
||||
)
|
||||
),
|
||||
}
|
||||
);
|
||||
console.log(`📦 Packages successfully published`);
|
||||
};
|
||||
|
||||
export const run = async (options: unknown) => {
|
||||
if (!validateOptions(options)) {
|
||||
return;
|
||||
}
|
||||
const { tag, dryRun, verbose } = options;
|
||||
|
||||
// Get the current version from code/package.json
|
||||
const currentVersion = await getCurrentVersion(verbose);
|
||||
const isAlreadyPublished = await isCurrentVersionPublished({
|
||||
currentVersion,
|
||||
packageName: '@junk-temporary-prototypes/manager-api',
|
||||
verbose,
|
||||
});
|
||||
if (isAlreadyPublished) {
|
||||
throw new Error(
|
||||
`⛔ Current version (${chalk.green(currentVersion)}) is already published, aborting.`
|
||||
);
|
||||
}
|
||||
await buildAllPackages();
|
||||
await publishAllPackages({ tag, verbose, dryRun });
|
||||
|
||||
console.log(
|
||||
`✅ Published all packages with version ${chalk.green(currentVersion)}${
|
||||
tag ? ` at tag ${chalk.blue(tag)}` : ''
|
||||
}`
|
||||
);
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
const parsed = program.parse();
|
||||
run(parsed.opts()).catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
92
scripts/release/unreleased-changes-exists.ts
Normal file
92
scripts/release/unreleased-changes-exists.ts
Normal file
@ -0,0 +1,92 @@
|
||||
/* eslint-disable no-continue */
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
/* eslint-disable no-console */
|
||||
import chalk from 'chalk';
|
||||
import program from 'commander';
|
||||
import { z } from 'zod';
|
||||
import { setOutput } from '@actions/core';
|
||||
import { intersection } from 'lodash';
|
||||
import type { Change } from './utils/get-changes';
|
||||
import { getChanges } from './utils/get-changes';
|
||||
import { getCurrentVersion } from './get-current-version';
|
||||
|
||||
program
|
||||
.name('are-changes-unreleased')
|
||||
.description('check if any changes since a release should be released')
|
||||
.option(
|
||||
'-F, --from <version>',
|
||||
'Which version/tag/commit to go back and check changes from. Defaults to latest release tag'
|
||||
)
|
||||
.option('-P, --unpicked-patches', 'Set to only consider PRs labeled with "patch" label')
|
||||
.option('-V, --verbose', 'Enable verbose logging', false);
|
||||
|
||||
const optionsSchema = z.object({
|
||||
from: z.string().optional(),
|
||||
unpickedPatches: z.boolean().optional(),
|
||||
verbose: z.boolean().optional(),
|
||||
});
|
||||
|
||||
type Options = {
|
||||
from?: string;
|
||||
unpickedPatches?: boolean;
|
||||
verbose: boolean;
|
||||
};
|
||||
|
||||
const validateOptions = (options: { [key: string]: any }): options is Options => {
|
||||
optionsSchema.parse(options);
|
||||
return true;
|
||||
};
|
||||
|
||||
const LABELS_TO_RELEASE = ['BREAKING CHANGE', 'feature request', 'bug', 'maintenance'] as const;
|
||||
|
||||
export const run = async (
|
||||
options: unknown
|
||||
): Promise<{ changesToRelease: Change[]; hasChangesToRelease: boolean }> => {
|
||||
if (!validateOptions(options)) {
|
||||
// this will never return because the validator throws
|
||||
return { changesToRelease: [], hasChangesToRelease: false };
|
||||
}
|
||||
const { from, unpickedPatches, verbose } = options;
|
||||
|
||||
const currentVersion = await getCurrentVersion();
|
||||
|
||||
console.log(
|
||||
`📐 Checking if there are any unreleased changes...`
|
||||
);
|
||||
|
||||
const { changes } = await getChanges({
|
||||
version: currentVersion,
|
||||
from: from || currentVersion,
|
||||
to: 'HEAD',
|
||||
unpickedPatches,
|
||||
verbose,
|
||||
});
|
||||
|
||||
const changesToRelease = changes
|
||||
.filter(({ labels }) => intersection(LABELS_TO_RELEASE, labels).length > 0);
|
||||
|
||||
const hasChangesToRelease = changesToRelease.length > 0;
|
||||
|
||||
if (process.env.GITHUB_ACTIONS === 'true') {
|
||||
setOutput('has-changes-to-release', hasChangesToRelease);
|
||||
}
|
||||
if (hasChangesToRelease) {
|
||||
console.log(
|
||||
`${chalk.green('🦋 The following changes are releasable')}:
|
||||
${chalk.blue(changesToRelease.map(({ title, pull }) => ` #${pull}: ${title}`).join('\n'))}`
|
||||
);
|
||||
} else {
|
||||
console.log(chalk.red('🫙 No changes to release!'));
|
||||
}
|
||||
|
||||
return { changesToRelease, hasChangesToRelease };
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
const parsed = program.parse();
|
||||
run(parsed.opts()).catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
4
scripts/release/utils/__mocks__/get-github-info.js
Normal file
4
scripts/release/utils/__mocks__/get-github-info.js
Normal file
@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
getPullInfoFromCommit: jest.fn(),
|
||||
getPullInfoFromPullRequest: jest.fn(),
|
||||
}
|
250
scripts/release/utils/get-changes.ts
Normal file
250
scripts/release/utils/get-changes.ts
Normal file
@ -0,0 +1,250 @@
|
||||
/* eslint-disable no-console */
|
||||
import chalk from 'chalk';
|
||||
import semver from 'semver';
|
||||
import simpleGit from 'simple-git';
|
||||
import type { PullRequestInfo } from './get-github-info';
|
||||
import { getPullInfoFromCommit } from './get-github-info';
|
||||
import { getUnpickedPRs } from './get-unpicked-prs';
|
||||
|
||||
const LABELS_FOR_CHANGELOG = ['BREAKING CHANGE', 'feature request', 'bug', 'maintenance'];
|
||||
|
||||
const git = simpleGit();
|
||||
|
||||
const getCommitAt = async (id: string, verbose?: boolean) => {
|
||||
if (!semver.valid(id)) {
|
||||
console.log(`🔍 ${chalk.red(id)} is not a valid semver string, assuming it is a commit hash`);
|
||||
return id;
|
||||
}
|
||||
const version = id.startsWith('v') ? id : `v${id}`;
|
||||
const commitSha = (await git.raw(['rev-list', '-n', '1', version])).split('\n')[0];
|
||||
if (verbose) {
|
||||
console.log(`🔍 Commit at tag ${chalk.green(version)}: ${chalk.blue(commitSha)}`);
|
||||
}
|
||||
return commitSha;
|
||||
};
|
||||
|
||||
export const getFromCommit = async (from?: string | undefined, verbose?: boolean) => {
|
||||
let actualFrom = from;
|
||||
if (!from) {
|
||||
console.log(`🔍 No 'from' specified, finding latest version tag, fetching all of them...`);
|
||||
// await git.fetch('origin', ['--all', '--tags']);
|
||||
const { latest } = await git.tags(['v*', '--sort=-v:refname', '--merged']);
|
||||
if (!latest) {
|
||||
throw new Error(
|
||||
'Could not automatically detect which commit to generate from, because no version tag was found in the history. Have you fetch tags?'
|
||||
);
|
||||
}
|
||||
actualFrom = latest;
|
||||
if (verbose) {
|
||||
console.log(`🔍 No 'from' specified, found latest tag: ${chalk.blue(latest)}`);
|
||||
}
|
||||
}
|
||||
const commit = await getCommitAt(actualFrom, verbose);
|
||||
if (verbose) {
|
||||
console.log(`🔍 Found 'from' commit: ${chalk.blue(commit)}`);
|
||||
}
|
||||
return commit;
|
||||
};
|
||||
|
||||
export const getToCommit = async (to?: string | undefined, verbose?: boolean) => {
|
||||
if (!to) {
|
||||
const head = await git.revparse('HEAD');
|
||||
if (verbose) {
|
||||
console.log(`🔍 No 'to' specified, HEAD is at commit: ${chalk.blue(head)}`);
|
||||
}
|
||||
return head;
|
||||
}
|
||||
|
||||
const commit = await getCommitAt(to, verbose);
|
||||
if (verbose) {
|
||||
console.log(`🔍 Found 'to' commit: ${chalk.blue(commit)}`);
|
||||
}
|
||||
return commit;
|
||||
};
|
||||
|
||||
export const getAllCommitsBetween = async ({
|
||||
from,
|
||||
to,
|
||||
verbose,
|
||||
}: {
|
||||
from: string;
|
||||
to?: string;
|
||||
verbose?: boolean;
|
||||
}) => {
|
||||
const logResult = await git.log({ from, to });
|
||||
if (verbose) {
|
||||
console.log(
|
||||
`🔍 Found ${chalk.blue(logResult.total)} commits between ${chalk.green(
|
||||
`${from}`
|
||||
)} and ${chalk.green(`${to}`)}:`
|
||||
);
|
||||
console.dir(logResult.all, { depth: null, colors: true });
|
||||
}
|
||||
return logResult.all;
|
||||
};
|
||||
|
||||
export const getRepo = async (verbose?: boolean): Promise<string> => {
|
||||
const remotes = await git.getRemotes(true);
|
||||
const originRemote = remotes.find((remote) => remote.name === 'origin');
|
||||
if (!originRemote) {
|
||||
console.error(
|
||||
'Could not determine repository URL because no remote named "origin" was found. Remotes found:'
|
||||
);
|
||||
console.dir(remotes, { depth: null, colors: true });
|
||||
throw new Error('No remote named "origin" found');
|
||||
}
|
||||
const pushUrl = originRemote.refs.push;
|
||||
const repo = pushUrl.replace(/\.git$/, '').replace(/.*:(\/\/github\.com\/)*/, '');
|
||||
if (verbose) {
|
||||
console.log(`📦 Extracted repo: ${chalk.blue(repo)}`);
|
||||
}
|
||||
return repo;
|
||||
};
|
||||
|
||||
export const getPullInfoFromCommits = async ({
|
||||
repo,
|
||||
commits,
|
||||
verbose,
|
||||
}: {
|
||||
repo: string;
|
||||
commits: readonly { hash: string }[];
|
||||
verbose?: boolean;
|
||||
}): Promise<PullRequestInfo[]> => {
|
||||
const pullRequests = await Promise.all(
|
||||
commits.map((commit) =>
|
||||
getPullInfoFromCommit({
|
||||
repo,
|
||||
commit: commit.hash,
|
||||
})
|
||||
)
|
||||
);
|
||||
if (verbose) {
|
||||
console.log(`🔍 Found pull requests:`);
|
||||
console.dir(pullRequests, { depth: null, colors: true });
|
||||
}
|
||||
return pullRequests;
|
||||
};
|
||||
|
||||
export type Change = PullRequestInfo;
|
||||
|
||||
export const mapToChanges = ({
|
||||
commits,
|
||||
pullRequests,
|
||||
unpickedPatches,
|
||||
verbose,
|
||||
}: {
|
||||
commits: readonly { hash: string; message?: string }[];
|
||||
pullRequests: PullRequestInfo[];
|
||||
unpickedPatches?: boolean;
|
||||
verbose?: boolean;
|
||||
}): Change[] => {
|
||||
if (pullRequests.length !== commits.length) {
|
||||
// not all commits are associated with a pull request, but the pullRequests array should still contain those commits
|
||||
console.error('Pull requests and commits are not the same length, this should not happen');
|
||||
console.error(`Pull Requests: ${pullRequests.length}`);
|
||||
console.dir(pullRequests, { depth: null, colors: true });
|
||||
console.error(`Commits: ${commits.length}`);
|
||||
console.dir(commits, { depth: null, colors: true });
|
||||
throw new Error('Pull requests and commits are not the same length, this should not happen');
|
||||
}
|
||||
const allEntries = pullRequests.map((pr, index) => {
|
||||
return {
|
||||
...pr,
|
||||
title: pr.title || commits[index].message,
|
||||
};
|
||||
});
|
||||
|
||||
const changes: Change[] = [];
|
||||
allEntries.forEach((entry) => {
|
||||
// filter out any duplicate entries, eg. when multiple commits are associated with the same pull request
|
||||
if (entry.pull && changes.findIndex((existing) => entry.pull === existing.pull) !== -1) {
|
||||
return;
|
||||
}
|
||||
// filter out any entries that are not patches if unpickedPatches is set. this will also filter out direct commits
|
||||
if (unpickedPatches && !entry.labels?.includes('patch')) {
|
||||
return;
|
||||
}
|
||||
changes.push(entry);
|
||||
});
|
||||
|
||||
if (verbose) {
|
||||
console.log(`📝 Generated changelog entries:`);
|
||||
console.dir(changes, { depth: null, colors: true });
|
||||
}
|
||||
return changes;
|
||||
};
|
||||
|
||||
export const getChangelogText = ({
|
||||
changes,
|
||||
version,
|
||||
}: {
|
||||
changes: Change[];
|
||||
version: string;
|
||||
}): string => {
|
||||
const heading = `## ${version}`;
|
||||
const formattedEntries = changes
|
||||
.filter((entry) => {
|
||||
// don't include direct commits that are not from pull requests
|
||||
if (!entry.pull) {
|
||||
return false;
|
||||
}
|
||||
// only include PRs that with labels listed in LABELS_FOR_CHANGELOG
|
||||
return entry.labels?.some((label) => LABELS_FOR_CHANGELOG.includes(label));
|
||||
})
|
||||
.map((entry) => {
|
||||
const { title, links } = entry;
|
||||
const { pull, commit, user } = links;
|
||||
return pull
|
||||
? `- ${title} - ${pull}, thanks ${user}!`
|
||||
: `- ⚠️ _Direct commit_ ${title} - ${commit} by ${user}`;
|
||||
});
|
||||
const text = [heading, '', ...formattedEntries].join('\n');
|
||||
|
||||
console.log(`✅ Generated Changelog:`);
|
||||
console.log(text);
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
export const getChanges = async ({
|
||||
version,
|
||||
from,
|
||||
to,
|
||||
unpickedPatches,
|
||||
verbose,
|
||||
}: {
|
||||
version: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
unpickedPatches?: boolean;
|
||||
verbose?: boolean;
|
||||
}) => {
|
||||
console.log(`💬 Getting changes for ${chalk.blue(version)}`);
|
||||
|
||||
let commits;
|
||||
if (unpickedPatches) {
|
||||
commits = (await getUnpickedPRs('next-v2', verbose)).map((it) => ({ hash: it.mergeCommit }));
|
||||
} else {
|
||||
commits = await getAllCommitsBetween({
|
||||
from: await getFromCommit(from, verbose),
|
||||
to: await getToCommit(to, verbose),
|
||||
verbose,
|
||||
});
|
||||
}
|
||||
|
||||
const repo = await getRepo(verbose);
|
||||
const pullRequests = await getPullInfoFromCommits({ repo, commits, verbose }).catch((err) => {
|
||||
console.error(
|
||||
`🚨 Could not get pull requests from commits, this is usually because you have unpushed commits, or you haven't set the GH_TOKEN environment variable`
|
||||
);
|
||||
console.error(err);
|
||||
throw err;
|
||||
});
|
||||
const changes = mapToChanges({ commits, pullRequests, unpickedPatches, verbose });
|
||||
const changelogText = getChangelogText({
|
||||
changes,
|
||||
version,
|
||||
});
|
||||
|
||||
return { changes, changelogText };
|
||||
};
|
289
scripts/release/utils/get-github-info.ts
Normal file
289
scripts/release/utils/get-github-info.ts
Normal file
@ -0,0 +1,289 @@
|
||||
/**
|
||||
* This file is soft-forked from @changesets/get-github-info
|
||||
* https://github.com/changesets/changesets/tree/main/packages/get-github-info
|
||||
*
|
||||
* The only modification is that it also returns the PR title and labels
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import DataLoader from 'dataloader';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
const validRepoNameRegex = /^[\w.-]+\/[\w.-]+$/;
|
||||
|
||||
type RequestData =
|
||||
| { kind: 'commit'; repo: string; commit: string }
|
||||
| { kind: 'pull'; repo: string; pull: number };
|
||||
|
||||
type ReposWithCommitsAndPRsToFetch = Record<
|
||||
string,
|
||||
({ kind: 'commit'; commit: string } | { kind: 'pull'; pull: number })[]
|
||||
>;
|
||||
|
||||
function makeQuery(repos: ReposWithCommitsAndPRsToFetch) {
|
||||
return `
|
||||
query {
|
||||
${Object.keys(repos)
|
||||
.map(
|
||||
(repo, i) =>
|
||||
`a${i}: repository(
|
||||
owner: ${JSON.stringify(repo.split('/')[0])}
|
||||
name: ${JSON.stringify(repo.split('/')[1])}
|
||||
) {
|
||||
${repos[repo]
|
||||
.map((data) =>
|
||||
data.kind === 'commit'
|
||||
? `a${data.commit}: object(expression: ${JSON.stringify(data.commit)}) {
|
||||
... on Commit {
|
||||
commitUrl
|
||||
associatedPullRequests(first: 50) {
|
||||
nodes {
|
||||
number
|
||||
title
|
||||
url
|
||||
mergedAt
|
||||
labels(first: 50) {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
author {
|
||||
login
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
author {
|
||||
user {
|
||||
login
|
||||
url
|
||||
}
|
||||
}
|
||||
}}`
|
||||
: `pr__${data.pull}: pullRequest(number: ${data.pull}) {
|
||||
url
|
||||
title
|
||||
author {
|
||||
login
|
||||
url
|
||||
}
|
||||
labels(first: 50) {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
mergeCommit {
|
||||
commitUrl
|
||||
abbreviatedOid
|
||||
}
|
||||
}`
|
||||
)
|
||||
.join('\n')}
|
||||
}`
|
||||
)
|
||||
.join('\n')}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
// why are we using dataloader?
|
||||
// it provides use with two things
|
||||
// 1. caching
|
||||
// since getInfo will be called inside of changeset's getReleaseLine
|
||||
// and there could be a lot of release lines for a single commit
|
||||
// caching is important so we don't do a bunch of requests for the same commit
|
||||
// 2. batching
|
||||
// getReleaseLine will be called a large number of times but it'll be called at the same time
|
||||
// so instead of doing a bunch of network requests, we can do a single one.
|
||||
const GHDataLoader = new DataLoader(async (requests: RequestData[]) => {
|
||||
if (!process.env.GH_TOKEN) {
|
||||
throw new Error(
|
||||
'Please create a GitHub personal access token at https://github.com/settings/tokens/new with `read:user` and `repo:status` permissions and add it as the GH_TOKEN environment variable'
|
||||
);
|
||||
}
|
||||
const repos: ReposWithCommitsAndPRsToFetch = {};
|
||||
requests.forEach(({ repo, ...data }) => {
|
||||
if (repos[repo] === undefined) {
|
||||
repos[repo] = [];
|
||||
}
|
||||
repos[repo].push(data);
|
||||
});
|
||||
|
||||
const data = await fetch('https://api.github.com/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Token ${process.env.GH_TOKEN}`,
|
||||
},
|
||||
body: JSON.stringify({ query: makeQuery(repos) }),
|
||||
}).then((x: any) => x.json());
|
||||
|
||||
if (data.errors) {
|
||||
throw new Error(
|
||||
`An error occurred when fetching data from GitHub\n${JSON.stringify(data.errors, null, 2)}`
|
||||
);
|
||||
}
|
||||
|
||||
// this is mainly for the case where there's an authentication problem
|
||||
if (!data.data) {
|
||||
throw new Error(`An error occurred when fetching data from GitHub\n${JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
const cleanedData: Record<string, { commit: Record<string, any>; pull: Record<string, any> }> =
|
||||
{};
|
||||
Object.keys(repos).forEach((repo, index) => {
|
||||
const output: { commit: Record<string, any>; pull: Record<string, any> } = {
|
||||
commit: {},
|
||||
pull: {},
|
||||
};
|
||||
cleanedData[repo] = output;
|
||||
Object.entries(data.data[`a${index}`]).forEach(([field, value]) => {
|
||||
// this is "a" because that's how it was when it was first written, "a" means it's a commit not a pr
|
||||
// we could change it to commit__ but then we have to get new GraphQL results from the GH API to put in the tests
|
||||
if (field[0] === 'a') {
|
||||
output.commit[field.substring(1)] = value;
|
||||
} else {
|
||||
output.pull[field.replace('pr__', '')] = value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return requests.map(
|
||||
({ repo, ...rest }) =>
|
||||
cleanedData[repo][rest.kind][rest.kind === 'pull' ? rest.pull : rest.commit]
|
||||
);
|
||||
});
|
||||
|
||||
export type PullRequestInfo = {
|
||||
user: string | null;
|
||||
title: string | null;
|
||||
commit: string | null;
|
||||
pull: string | null;
|
||||
labels: string[] | null;
|
||||
links: {
|
||||
commit: string;
|
||||
pull: string | null;
|
||||
user: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
export async function getPullInfoFromCommit(request: {
|
||||
commit: string;
|
||||
repo: string;
|
||||
}): Promise<PullRequestInfo> {
|
||||
if (!request.commit) {
|
||||
throw new Error('Please pass a commit SHA to getInfo');
|
||||
}
|
||||
|
||||
if (!request.repo) {
|
||||
throw new Error('Please pass a GitHub repository in the form of userOrOrg/repoName to getInfo');
|
||||
}
|
||||
|
||||
if (!validRepoNameRegex.test(request.repo)) {
|
||||
throw new Error(
|
||||
`Please pass a valid GitHub repository in the form of userOrOrg/repoName to getInfo (it has to match the "${validRepoNameRegex.source}" pattern)`
|
||||
);
|
||||
}
|
||||
|
||||
const data = await GHDataLoader.load({ kind: 'commit', ...request });
|
||||
if (!data) {
|
||||
return {
|
||||
user: null,
|
||||
pull: null,
|
||||
commit: request.commit,
|
||||
title: null,
|
||||
labels: null,
|
||||
links: {
|
||||
commit: request.commit,
|
||||
pull: null,
|
||||
user: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
let user = null;
|
||||
if (data.author && data.author.user) {
|
||||
user = data.author.user;
|
||||
}
|
||||
|
||||
const associatedPullRequest =
|
||||
data.associatedPullRequests &&
|
||||
data.associatedPullRequests.nodes &&
|
||||
data.associatedPullRequests.nodes.length
|
||||
? (data.associatedPullRequests.nodes as any[]).sort((a, b) => {
|
||||
if (a.mergedAt === null && b.mergedAt === null) {
|
||||
return 0;
|
||||
}
|
||||
if (a.mergedAt === null) {
|
||||
return 1;
|
||||
}
|
||||
if (b.mergedAt === null) {
|
||||
return -1;
|
||||
}
|
||||
const aDate = new Date(a.mergedAt);
|
||||
const bDate = new Date(b.mergedAt);
|
||||
if (aDate > bDate) {
|
||||
return 1;
|
||||
}
|
||||
if (aDate < bDate) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
})[0]
|
||||
: null;
|
||||
if (associatedPullRequest) {
|
||||
user = associatedPullRequest.author;
|
||||
}
|
||||
|
||||
return {
|
||||
user: user ? user.login : null,
|
||||
pull: associatedPullRequest ? associatedPullRequest.number : null,
|
||||
commit: request.commit,
|
||||
title: associatedPullRequest ? associatedPullRequest.title : null,
|
||||
labels: associatedPullRequest
|
||||
? (associatedPullRequest.labels.nodes || []).map((label: { name: string }) => label.name)
|
||||
: null,
|
||||
links: {
|
||||
commit: `[\`${request.commit}\`](${data.commitUrl})`,
|
||||
pull: associatedPullRequest
|
||||
? `[#${associatedPullRequest.number}](${associatedPullRequest.url})`
|
||||
: null,
|
||||
user: user ? `[@${user.login}](${user.url})` : null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function getPullInfoFromPullRequest(request: {
|
||||
pull: number;
|
||||
repo: string;
|
||||
}): Promise<PullRequestInfo> {
|
||||
if (request.pull === undefined) {
|
||||
throw new Error('Please pass a pull request number');
|
||||
}
|
||||
|
||||
if (!request.repo) {
|
||||
throw new Error('Please pass a GitHub repository in the form of userOrOrg/repoName to getInfo');
|
||||
}
|
||||
|
||||
if (!validRepoNameRegex.test(request.repo)) {
|
||||
throw new Error(
|
||||
`Please pass a valid GitHub repository in the form of userOrOrg/repoName to getInfo (it has to match the "${validRepoNameRegex.source}" pattern)`
|
||||
);
|
||||
}
|
||||
|
||||
const data = await GHDataLoader.load({ kind: 'pull', ...request });
|
||||
const user = data?.author;
|
||||
const title = data?.title;
|
||||
const commit = data?.mergeCommit;
|
||||
|
||||
return {
|
||||
user: user ? user.login : null,
|
||||
pull: request.pull.toString(),
|
||||
commit: commit ? commit.abbreviatedOid : null,
|
||||
title: title || null,
|
||||
labels: data ? (data.labels.nodes || []).map((label: { name: string }) => label.name) : null,
|
||||
links: {
|
||||
commit: commit ? `[\`${commit.abbreviatedOid}\`](${commit.commitUrl})` : null,
|
||||
pull: `[#${request.pull}](https://github.com/${request.repo}/pull/${request.pull})`,
|
||||
user: user ? `[@${user.login}](${user.url})` : null,
|
||||
},
|
||||
};
|
||||
}
|
67
scripts/release/utils/get-unpicked-prs.ts
Normal file
67
scripts/release/utils/get-unpicked-prs.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import type { GraphQlQueryResponseData } from "@octokit/graphql";
|
||||
import { githubGraphQlClient } from "./github-client";
|
||||
|
||||
export interface PR {
|
||||
number: number;
|
||||
id: string;
|
||||
branch: string;
|
||||
title: string;
|
||||
mergeCommit: string;
|
||||
}
|
||||
|
||||
export async function getUnpickedPRs(baseBranch: string, verbose?: boolean): Promise<Array<PR>> {
|
||||
console.log(`💬 Getting unpicked patch pull requests...`);
|
||||
const result = await githubGraphQlClient<GraphQlQueryResponseData>(
|
||||
`
|
||||
query ($owner: String!, $repo: String!, $state: PullRequestState!, $order: IssueOrder!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequests(states: [$state], labels: ["patch"], orderBy: $order, first: 50) {
|
||||
nodes {
|
||||
id
|
||||
number
|
||||
title
|
||||
baseRefName
|
||||
mergeCommit {
|
||||
abbreviatedOid
|
||||
}
|
||||
labels(first: 20) {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
owner: 'storybookjs',
|
||||
repo: 'monorepo-release-tooling-prototype',
|
||||
order: {
|
||||
field: 'UPDATED_AT',
|
||||
direction: 'ASC',
|
||||
},
|
||||
state: 'MERGED',
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
pullRequests: { nodes },
|
||||
} = result.repository;
|
||||
|
||||
const prs = nodes.map((node: any) => ({
|
||||
number: node.number,
|
||||
id: node.id,
|
||||
branch: node.baseRefName,
|
||||
title: node.title,
|
||||
mergeCommit: node.mergeCommit.abbreviatedOid,
|
||||
labels: node.labels.nodes.map((l: any) => l.name),
|
||||
}));
|
||||
|
||||
const unpickedPRs = prs.filter((pr: any) => !pr.labels.includes('picked')).filter((pr: any) => pr.branch === baseBranch);
|
||||
if(verbose){
|
||||
console.log(`🔍 Found unpicked patch pull requests:
|
||||
${JSON.stringify(unpickedPRs, null, 2)}`);
|
||||
}
|
||||
return unpickedPRs;
|
||||
}
|
5
scripts/release/utils/github-client.ts
Normal file
5
scripts/release/utils/github-client.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { graphql } from '@octokit/graphql';
|
||||
|
||||
export const githubGraphQlClient = graphql.defaults({
|
||||
headers: { authorization: `token ${process.env.GH_TOKEN}` },
|
||||
});
|
220
scripts/release/version.ts
Normal file
220
scripts/release/version.ts
Normal file
@ -0,0 +1,220 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
/* eslint-disable no-console */
|
||||
import { setOutput } from '@actions/core';
|
||||
import { ensureDir, readFile, readJson, writeFile, writeJson } from 'fs-extra';
|
||||
import chalk from 'chalk';
|
||||
import path from 'path';
|
||||
import program from 'commander';
|
||||
import semver from 'semver';
|
||||
import { z } from 'zod';
|
||||
import dedent from 'ts-dedent';
|
||||
import { execaCommand } from '../utils/exec';
|
||||
import { listOfPackages } from '../utils/list-packages';
|
||||
import packageVersionMap from '../../code/lib/cli/src/versions';
|
||||
|
||||
program
|
||||
.name('version')
|
||||
.description('version all packages')
|
||||
.option(
|
||||
'-R, --release-type <major|minor|patch|prerelease>',
|
||||
'Which release type to use to bump the version'
|
||||
)
|
||||
.option('-P, --pre-id <id>', 'Which prerelease identifer to change to, eg. "alpha", "beta", "rc"')
|
||||
.option(
|
||||
'-E, --exact <version>',
|
||||
'Use exact version instead of calculating from current version, eg. "7.2.0-canary.123". Can not be combined with --release-type or --pre-id'
|
||||
)
|
||||
.option('-V, --verbose', 'Enable verbose logging', false);
|
||||
|
||||
const optionsSchema = z
|
||||
.object({
|
||||
releaseType: z
|
||||
.enum(['major', 'minor', 'patch', 'prerelease', 'premajor', 'preminor', 'prepatch'])
|
||||
.optional(),
|
||||
preId: z.string().optional(),
|
||||
exact: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine((version) => (version ? semver.valid(version) !== null : true), {
|
||||
message: '--exact version has to be a valid semver string',
|
||||
}),
|
||||
verbose: z.boolean().optional(),
|
||||
})
|
||||
.superRefine((schema, ctx) => {
|
||||
// manual union validation because zod + commander is not great in this case
|
||||
const hasExact = 'exact' in schema && schema.exact;
|
||||
const hasReleaseType = 'releaseType' in schema && schema.releaseType;
|
||||
if ((hasExact && hasReleaseType) || (!hasExact && !hasReleaseType)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message:
|
||||
'Combining --exact with --release-type is invalid, but having one of them is required',
|
||||
});
|
||||
}
|
||||
if (schema.preId && !schema.releaseType.startsWith('pre')) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message:
|
||||
'Using prerelease identifier requires one of release types: premajor, preminor, prepatch, prerelease',
|
||||
});
|
||||
}
|
||||
return z.NEVER;
|
||||
});
|
||||
|
||||
type BaseOptions = { verbose: boolean };
|
||||
type BumpOptions = BaseOptions & {
|
||||
releaseType: semver.ReleaseType;
|
||||
preId?: string;
|
||||
};
|
||||
type ExactOptions = BaseOptions & {
|
||||
exact: semver.ReleaseType;
|
||||
};
|
||||
type Options = BumpOptions | ExactOptions;
|
||||
|
||||
const CODE_DIR_PATH = path.join(__dirname, '..', '..', 'code');
|
||||
const CODE_PACKAGE_JSON_PATH = path.join(CODE_DIR_PATH, 'package.json');
|
||||
|
||||
const validateOptions = (options: { [key: string]: any }): options is Options => {
|
||||
optionsSchema.parse(options);
|
||||
return true;
|
||||
};
|
||||
|
||||
const getCurrentVersion = async () => {
|
||||
console.log(`📐 Reading current version of Storybook...`);
|
||||
const { version } = await readJson(CODE_PACKAGE_JSON_PATH);
|
||||
return version;
|
||||
};
|
||||
|
||||
const bumpCodeVersion = async (nextVersion: string) => {
|
||||
console.log(`🤜 Bumping version of ${chalk.cyan('code')}'s package.json...`);
|
||||
|
||||
const codePkgJson = await readJson(CODE_PACKAGE_JSON_PATH);
|
||||
|
||||
codePkgJson.version = nextVersion;
|
||||
await writeJson(CODE_PACKAGE_JSON_PATH, codePkgJson, { spaces: 2 });
|
||||
|
||||
console.log(`✅ Bumped version of ${chalk.cyan('code')}'s package.json`);
|
||||
};
|
||||
|
||||
const bumpAllPackageVersions = async (nextVersion: string, verbose?: boolean) => {
|
||||
console.log(`🤜 Bumping version of ${chalk.cyan('all packages')}...`);
|
||||
|
||||
/**
|
||||
* This uses the release workflow outlined by Yarn documentation here:
|
||||
* https://yarnpkg.com/features/release-workflow
|
||||
*
|
||||
* However we build the release YAML file manually instead of using the `yarn version --deferred` command
|
||||
* This is super hacky, but it's also way faster than invoking `yarn version` for each package, which is 1s each
|
||||
*
|
||||
* A simpler alternative is to use Lerna with:
|
||||
* await execaCommand(`yarn lerna version ${nextVersion} --no-git-tag-version --exact`, {
|
||||
* cwd: CODE_DIR_PATH,
|
||||
* stdio: verbose ? 'inherit' : undefined,
|
||||
* });
|
||||
* However that doesn't update peer deps. Trade offs
|
||||
*/
|
||||
const yarnVersionsPath = path.join(__dirname, '..', '..', 'code', '.yarn', 'versions');
|
||||
let yarnDefferedVersionFileContents = dedent`# this file is auto-generated by scripts/release/version.ts
|
||||
releases:
|
||||
|
||||
`;
|
||||
Object.keys(packageVersionMap).forEach((packageName) => {
|
||||
yarnDefferedVersionFileContents += ` '${packageName}': ${nextVersion}\n`;
|
||||
});
|
||||
await ensureDir(yarnVersionsPath);
|
||||
await writeFile(
|
||||
path.join(yarnVersionsPath, 'generated-by-versions-script.yml'),
|
||||
yarnDefferedVersionFileContents
|
||||
);
|
||||
|
||||
await execaCommand('yarn version apply --all', {
|
||||
cwd: CODE_DIR_PATH,
|
||||
stdio: verbose ? 'inherit' : undefined,
|
||||
});
|
||||
|
||||
console.log(`✅ Bumped version of ${chalk.cyan('all packages')}`);
|
||||
};
|
||||
|
||||
const bumpVersionSources = async (currentVersion: string, nextVersion: string) => {
|
||||
const filesToUpdate = [
|
||||
path.join(CODE_DIR_PATH, 'lib', 'manager-api', 'src', 'version.ts'),
|
||||
path.join(CODE_DIR_PATH, 'lib', 'cli', 'src', 'versions.ts'),
|
||||
];
|
||||
console.log(`🤜 Bumping versions in...:\n ${chalk.cyan(filesToUpdate.join('\n '))}`);
|
||||
|
||||
await Promise.all(
|
||||
filesToUpdate.map(async (filename) => {
|
||||
const currentContent = await readFile(filename, { encoding: 'utf-8' });
|
||||
const nextContent = currentContent.replaceAll(currentVersion, nextVersion);
|
||||
return writeFile(filename, nextContent);
|
||||
})
|
||||
);
|
||||
|
||||
console.log(`✅ Bumped versions in:\n ${chalk.cyan(filesToUpdate.join('\n '))}`);
|
||||
};
|
||||
|
||||
export const run = async (options: unknown) => {
|
||||
if (!validateOptions(options)) {
|
||||
return;
|
||||
}
|
||||
const { verbose } = options;
|
||||
|
||||
console.log(`🚛 Finding Storybook packages...`);
|
||||
|
||||
const [packages, currentVersion] = await Promise.all([listOfPackages(), getCurrentVersion()]);
|
||||
|
||||
console.log(
|
||||
`📦 found ${packages.length} storybook packages at version ${chalk.red(currentVersion)}`
|
||||
);
|
||||
if (verbose) {
|
||||
const formattedPackages = packages.map(
|
||||
(pkg) =>
|
||||
`${chalk.green(pkg.name.padEnd(60))}${chalk.red(pkg.version)}: ${chalk.cyan(pkg.location)}`
|
||||
);
|
||||
console.log(`📦 Packages:
|
||||
${formattedPackages.join('\n ')}`);
|
||||
}
|
||||
|
||||
let nextVersion: string;
|
||||
|
||||
if ('exact' in options && options.exact) {
|
||||
console.log(`📈 Exact version selected: ${chalk.green(options.exact)}`);
|
||||
nextVersion = options.exact;
|
||||
} else {
|
||||
const { releaseType, preId } = options as BumpOptions;
|
||||
console.log(`📈 Release type selected: ${chalk.green(releaseType)}`);
|
||||
if (preId) {
|
||||
console.log(`🆔 Version prerelease identifier selected: ${chalk.yellow(preId)}`);
|
||||
}
|
||||
|
||||
nextVersion = semver.inc(currentVersion, releaseType, preId);
|
||||
|
||||
console.log(
|
||||
`⏭ Bumping version ${chalk.blue(currentVersion)} with release type ${chalk.green(
|
||||
releaseType
|
||||
)}${
|
||||
preId ? ` and ${chalk.yellow(preId)}` : ''
|
||||
} results in version: ${chalk.bgGreenBright.bold(nextVersion)}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`⏭ Bumping all packages to ${chalk.blue(nextVersion)}...`);
|
||||
|
||||
await bumpCodeVersion(nextVersion);
|
||||
await bumpAllPackageVersions(nextVersion, verbose);
|
||||
await bumpVersionSources(currentVersion, nextVersion);
|
||||
|
||||
if (process.env.GITHUB_ACTIONS === 'true') {
|
||||
setOutput('current-version', currentVersion);
|
||||
setOutput('next-version', nextVersion);
|
||||
}
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
const options = program.parse().opts();
|
||||
run(options).catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
114
scripts/release/write-changelog.ts
Normal file
114
scripts/release/write-changelog.ts
Normal file
@ -0,0 +1,114 @@
|
||||
/* eslint-disable no-continue */
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
/* eslint-disable no-console */
|
||||
import chalk from 'chalk';
|
||||
import path from 'path';
|
||||
import program from 'commander';
|
||||
import semver from 'semver';
|
||||
import { z } from 'zod';
|
||||
import { readFile, writeFile } from 'fs-extra';
|
||||
import { getChanges } from './utils/get-changes';
|
||||
|
||||
program
|
||||
.name('write-changelog')
|
||||
.description(
|
||||
'write changelog based on merged PRs and commits. the <version> argument describes the changelog entry heading, but NOT which commits/PRs to include, must be a semver string'
|
||||
)
|
||||
.arguments('<version>')
|
||||
.option('-P, --unpicked-patches', 'Set to only consider PRs labeled with "patch" label')
|
||||
.option(
|
||||
'-F, --from <tag>',
|
||||
'Which tag or commit to generate changelog from, eg. "7.0.7". Leave unspecified to select latest released tag in git history'
|
||||
)
|
||||
.option(
|
||||
'-T, --to <tag>',
|
||||
'Which tag or commit to generate changelog to, eg. "7.1.0-beta.8". Leave unspecified to select HEAD commit'
|
||||
)
|
||||
.option('-D, --dry-run', 'Do not write file, only output to shell', false)
|
||||
.option('-V, --verbose', 'Enable verbose logging', false);
|
||||
|
||||
const optionsSchema = z.object({
|
||||
unpickedPatches: z.boolean().optional(),
|
||||
from: z.string().optional(),
|
||||
to: z.string().optional(),
|
||||
verbose: z.boolean().optional(),
|
||||
dryRun: z.boolean().optional(),
|
||||
});
|
||||
|
||||
type Options = {
|
||||
unpickedPatches?: boolean;
|
||||
from?: string;
|
||||
to?: string;
|
||||
verbose: boolean;
|
||||
dryRun?: boolean;
|
||||
};
|
||||
|
||||
const validateOptions = (args: unknown[], options: { [key: string]: any }): options is Options => {
|
||||
optionsSchema.parse(options);
|
||||
if (args.length !== 1 || !semver.valid(args[0] as string)) {
|
||||
console.error(
|
||||
`🚨 Invalid arguments, expected a single argument with the version to generate changelog for, eg. ${chalk.green(
|
||||
'7.1.0-beta.8'
|
||||
)}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const writeToFile = async ({
|
||||
changelogText,
|
||||
version,
|
||||
verbose,
|
||||
}: {
|
||||
changelogText: string;
|
||||
version: string;
|
||||
verbose?: boolean;
|
||||
}) => {
|
||||
const isPrerelease = semver.prerelease(version) !== null;
|
||||
const changelogFilename = isPrerelease ? 'CHANGELOG.prerelease.md' : 'CHANGELOG.md';
|
||||
const changelogPath = path.join(__dirname, '..', '..', changelogFilename);
|
||||
|
||||
if (verbose) {
|
||||
console.log(`📝 Writing changelog to ${chalk.blue(changelogPath)}`);
|
||||
}
|
||||
|
||||
const currentChangelog = await readFile(changelogPath, 'utf-8');
|
||||
const nextChangelog = [changelogText, currentChangelog].join('\n\n');
|
||||
|
||||
await writeFile(changelogPath, nextChangelog);
|
||||
};
|
||||
|
||||
export const run = async (args: unknown[], options: unknown) => {
|
||||
if (!validateOptions(args, options)) {
|
||||
return;
|
||||
}
|
||||
const { from, to, unpickedPatches, dryRun, verbose } = options;
|
||||
const version = args[0] as string;
|
||||
|
||||
console.log(
|
||||
`💬 Generating changelog for ${chalk.blue(version)} between ${chalk.green(
|
||||
from || 'latest'
|
||||
)} and ${chalk.green(to || 'HEAD')}`
|
||||
);
|
||||
|
||||
const { changelogText } = await getChanges({ version, from, to, unpickedPatches, verbose });
|
||||
|
||||
if (dryRun) {
|
||||
console.log(`📝 Dry run, not writing file`);
|
||||
return;
|
||||
}
|
||||
|
||||
await writeToFile({ changelogText, version, verbose });
|
||||
|
||||
console.log(`✅ Wrote Changelog to file`);
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
const parsed = program.parse();
|
||||
run(parsed.args, parsed.opts()).catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user