Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Release tooling: Defer version bumping to the publish step #23393

Merged
merged 13 commits into from
Jul 12, 2023
6 changes: 3 additions & 3 deletions .github/workflows/prepare-patch-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,11 @@ jobs:
git config --global user.email '[email protected]'
yarn release:pick-patches

- name: Bump version
- name: Bump version deferred
id: bump-version
if: steps.unreleased-changes.outputs.has-changes-to-release == 'true'
run: |
yarn release:version --release-type patch --verbose
yarn release:version --deferred --release-type patch --verbose

# We need the current version to set the branch name, even when not bumping the version
- name: Get current version
Expand Down Expand Up @@ -121,7 +121,7 @@ jobs:
git config --global user.email '[email protected]'
git checkout -b version-patch-from-${{ steps.versions.outputs.current }}
git add .
git commit -m "Bump version from ${{ steps.versions.outputs.current }} to ${{ steps.versions.outputs.next }}" || true
git commit -m "Write changelog for ${{ steps.versions.outputs.next }}" || true
git push --force origin version-patch-from-${{ steps.versions.outputs.current }}

- name: Generate PR description
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/prepare-prerelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,10 @@ jobs:
gh run cancel ${{ github.run_id }}
gh run watch ${{ github.run_id }}

- name: Bump version
- name: Bump version deferred
id: bump-version
run: |
yarn release:version --release-type ${{ inputs.release-type || 'prerelease' }} ${{ inputs.pre-id && format('{0} {1}', '--pre-id', inputs.pre-id) || '' }} --verbose
yarn release:version --deferred --release-type ${{ inputs.release-type || 'prerelease' }} ${{ inputs.pre-id && format('{0} {1}', '--pre-id', inputs.pre-id) || '' }} --verbose

- name: Write changelog
env:
Expand All @@ -125,7 +125,7 @@ jobs:
git config --global user.email '[email protected]'
git checkout -b version-prerelease-from-${{ steps.bump-version.outputs.current-version }}
git add .
git commit -m "Bump version from ${{ steps.bump-version.outputs.current-version }} to ${{ steps.bump-version.outputs.next-version }}" || true
git commit -m "Write changelog for ${{ steps.bump-version.outputs.next-version }}" || true
git push --force origin version-prerelease-from-${{ steps.bump-version.outputs.current-version }}

- name: Generate PR description
Expand Down
21 changes: 21 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,27 @@ jobs:
run: |
yarn install

- name: Apply deferred version bump and commit
id: version-bump
working-directory: .
run: |
CURRENT_VERSION=$(cat ./code/package.json | jq '.version')
DEFERRED_NEXT_VERSION=$(cat ./code/package.json | jq '.deferredNextVersion')

if [[ "$DEFERRED_NEXT_VERSION" == "null" ]]; then
echo "No deferred version set, not bumping versions"
exit 0
fi
cd scripts
yarn release:version --apply --verbose
cd ..

git config --global user.name "storybook-bot"
git config --global user.email "[email protected]"
git add .
git commit -m "Bump version from $CURRENT_VERSION to $DEFERRED_NEXT_VERSION" || true
git push origin ${{ github.ref_name }}

- name: Get current version
id: version
run: yarn release:get-current-version
Expand Down
52 changes: 30 additions & 22 deletions CONTRIBUTING/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ The high-level flow is:

1. When a PR is merged to `next` (or a commit is pushed), both release pull requests are (re)generated.
2. They create a new branch - `version-(patch|prerelease)-from-<CURRENT-VERSION>`.
3. They bump all versions according to the version strategy.
3. They calculate which version to bump to according to the version strategy.
4. They update `CHANGELOG(.prerelease).md` with all changes detected.
5. They commit everything.
6. They **force push**.
Expand All @@ -112,7 +112,7 @@ A few key points to note in this flow:

- The PRs are regenerated on any changes to `next`, or can be manually triggered (see [the Re-trigger the Workflow section](#4-re-trigger-the-workflow)).
- The changes are force pushed to the branch, so any manual changes on the release branch before merging risk being overwritten if someone else merges a new change to `next`, triggering the workflow. To avoid this, apply the **"freeze"** label to the pull request.
- The version bumps and changelogs are committed during the preparation, but the packages are not published until later.
- The changelogs are committed during the preparation, but the packages are not version bumped and not published until later.
- The release pull requests don't target their working branches (`next` and `main`), but rather `next-release` and `latest-release`.

### Prereleases
Expand Down Expand Up @@ -149,9 +149,10 @@ gitGraph
checkout next
merge some-bugfix type: HIGHLIGHT
branch version-prerelease-from-7.1.0-alpha.28
commit id: "bump version"
commit id: "write changelog"
checkout next-release
merge version-prerelease-from-7.1.0-alpha.28 tag: "7.1.0-alpha.29"
merge version-prerelease-from-7.1.0-alpha.28
commit id: "bump versions" tag: "7.1.0-alpha.29"
checkout next
merge next-release
```
Expand Down Expand Up @@ -199,9 +200,10 @@ gitGraph
branch version-patch-from-7.0.18
cherry-pick id: "patch1"
cherry-pick id: "patch2"
commit id: "version bump"
commit id: "write changelog"
checkout latest-release
merge version-patch-from-7.0.18 tag: "v7.0.19"
merge version-patch-from-7.0.18
commit id: "bump versions" tag: "v7.0.19"
checkout main
merge latest-release
```
Expand All @@ -213,13 +215,14 @@ gitGraph

When either a prerelease or a patch release branch is merged into `main` or `next-release`, the publishing workflow is triggered. This workflow performs the following tasks:

1. Install dependencies and build all packages.
2. Publish packages to npm.
3. (If this is a patch release, add the "**picked**" label to all relevant pull requests.)
4. Create a new GitHub Release, including a version tag in the release branch (`latest-release` or `next-release`).
5. Merge the release branch into the core branch (`main` or `next`).
6. (If this is a patch release, copy the `CHANGELOG.md` changes from `main` to `next`.)
7. (If this is [a promotion from a prerelease to a stable release](#minormajor-releases---710-rc2---710-or-800-rc3---800), force push `next` to `main`.)
1. Bump versions of all packages according to the plan from the prepared PRs
2. Install dependencies and build all packages.
3. Publish packages to npm.
4. (If this is a patch release, add the "**picked**" label to all relevant pull requests.)
5. Create a new GitHub Release, including a version tag in the release branch (`latest-release` or `next-release`).
6. Merge the release branch into the core branch (`main` or `next`).
7. (If this is a patch release, copy the `CHANGELOG.md` changes from `main` to `next`.)
8. (If this is [a promotion from a prerelease to a stable release](#minormajor-releases---710-rc2---710-or-800-rc3---800), force push `next` to `main`.)

The publish workflow runs in the "release" GitHub environment, which has the npm token required to publish packages to the `@storybook` npm organization. For security reasons, this environment can only be accessed from the four "core" branches: `main`, `next`, `latest-release` and `next-release`.

Expand Down Expand Up @@ -318,7 +321,7 @@ When the pull request was frozen, a CI run was triggered on the branch. If it's

### 7. See the "Publish" Workflow Finish

Merging the pull request will trigger [the publish workflow](https://github.com/storybookjs/storybook/actions/workflows/publish.yml), which does the final publishing. As a Releaser, you're responsible for this to finish successfully, so you should watch it until the end. If it fails, it will notify in Discord, so you can monitor that instead if you want to.
Merging the pull request will trigger [the publish workflow](https://github.com/storybookjs/storybook/actions/workflows/publish.yml), which does the final version bumping and publishing. As a Releaser, you're responsible for this to finish successfully, so you should watch it until the end. If it fails, it will notify in Discord, so you can monitor that instead if you want to.

Done! 🚀

Expand All @@ -339,15 +342,18 @@ Before you start you should make sure that your working tree is clean and the re
5. (If patch release) Cherry pick:
1. `yarn release:pick-patches`
2. Manually cherry pick any necessary patches based on the previous output
6. Bump versions: `yarn release:version --verbose --release-type <RELEASE_TYPE> --pre-id <PRE_ID>`
6. Bump versions:
1. If you plan on using automatic publishing (ie. stop at step 12), bump with deferred: `yarn release:version --verbose --deferred --release-type <RELEASE_TYPE> --pre-id <PRE_ID>`
2. If doing the whole release locally, **do not** defer the bump: `yarn release:version --verbose --release-type <RELEASE_TYPE> --pre-id <PRE_ID>`
7. To see a list of changes (for your own to-do list), run `yarn release:generate-pr-description --current-version <CURRENT_VERSION> --next-version <NEXT_VERSION_FROM_PREVIOUS_STEP> --verbose`
8. Write changelogs: `yarn release:write-changelog <NEXT_VERSION_FROM_PREVIOUS_STEP> --verbose`
9. `git add .`.
10. Commit changes: `git commit -m "Bump version from <CURRENT_VERSION> to <NEXT_VERSION_FROM_PREVIOUS_STEP> MANUALLY"`
11. Merge changes to the release branch:
1. `git checkout <"latest-release" | "next-release">`
2. `git merge <PREVIOUS_BRANCH>`
3. `git push origin`
2. `git pull`
3. `git merge <PREVIOUS_BRANCH>`
4. `git push origin`
12. (If automatic publishing is still working, it should kick in now and the rest of the steps can be skipped)
13. `cd ..`
14. Publish to the registry: `YARN_NPM_AUTH_TOKEN=<NPM_TOKEN> yarn release:publish --tag <"next" OR "latest"> --verbose`
Expand Down Expand Up @@ -500,7 +506,7 @@ gitGraph
commit
checkout next
branch version-prerelease-from-7.1.0-alpha.28
commit id: "bump version"
commit id
checkout next
merge some-simultaneous-bugfix type: HIGHLIGHT id: "whoops!"
merge version-prerelease-from-7.1.0-alpha.28 tag: "v7.1.0-alpha.29"
Expand All @@ -524,17 +530,19 @@ gitGraph
commit
checkout next
branch version-prerelease-from-7.1.0-alpha.28
commit id: "bump version"
commit id: "write changelog"
checkout next
merge some-simultanous-bugfix id: "whoops!"
checkout next-release
merge version-prerelease-from-7.1.0-alpha.28 tag: "v7.1.0-alpha.29"
merge version-prerelease-from-7.1.0-alpha.28
commit id: "bump versions" tag: "v7.1.0-alpha.29"
checkout next
merge next-release
branch version-prerelease-from-7.1.0-alpha.29
commit id: "bump version again"
commit id: "write changelog again"
checkout next-release
merge version-prerelease-from-7.1.0-alpha.29 tag: "v7.1.0-alpha.30"
merge version-prerelease-from-7.1.0-alpha.29
commit id: "bump versions again" tag: "v7.1.0-alpha.30"
checkout next
merge next-release
```
Expand Down
4 changes: 4 additions & 0 deletions code/__mocks__/fs-extra.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const readJsonSync = (filePath = '') => JSON.parse(mockFiles[filePath]);
const lstatSync = (filePath) => ({
isFile: () => !!mockFiles[filePath],
});
const writeJson = jest.fn((filePath, json, { spaces } = {}) => {
mockFiles[filePath] = JSON.stringify(json, null, spaces);
});

// eslint-disable-next-line no-underscore-dangle
fs.__setMockFiles = __setMockFiles;
Expand All @@ -29,5 +32,6 @@ fs.readJson = readJson;
fs.readJsonSync = readJsonSync;
fs.existsSync = existsSync;
fs.lstatSync = lstatSync;
fs.writeJson = writeJson;

module.exports = fs;
116 changes: 112 additions & 4 deletions scripts/release/__tests__/version.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,77 @@ describe('Version', () => {
`);
});

it('should throw when apply is combined with releaseType', 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 { "@storybook/addon-a11y": "1.0.0" };`,
});

await expect(version({ apply: true, releaseType: 'prerelease' })).rejects
.toThrowErrorMatchingInlineSnapshot(`
"[
{
"code": "custom",
"message": "--apply cannot be combined with --exact or --release-type, as it will always read from code/package.json#deferredNextVersion",
"path": []
}
]"
`);
});

it('should throw when apply is combined with exact', 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 { "@storybook/addon-a11y": "1.0.0" };`,
});

await expect(version({ apply: true, exact: '1.0.0' })).rejects
.toThrowErrorMatchingInlineSnapshot(`
"[
{
"code": "custom",
"message": "--apply cannot be combined with --exact or --release-type, as it will always read from code/package.json#deferredNextVersion",
"path": []
}
]"
`);
});

it('should throw when apply is combined with deferred', 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 { "@storybook/addon-a11y": "1.0.0" };`,
});

await expect(version({ apply: true, deferred: true })).rejects
.toThrowErrorMatchingInlineSnapshot(`
"[
{
"code": "custom",
"message": "--deferred cannot be combined with --apply",
"path": []
}
]"
`);
});

it('should throw when applying without a "deferredNextVersion" set', async () => {
fsExtra.__setMockFiles({
[CODE_PACKAGE_JSON_PATH]: JSON.stringify({ version: '1.0.0' }),
});

await expect(version({ apply: true })).rejects.toThrowErrorMatchingInlineSnapshot(
`"The 'deferredNextVersion' property in code/package.json is unset. This is necessary to apply a deferred version bump"`
);

expect(fsExtra.writeJson).not.toHaveBeenCalled();
expect(fsExtra.writeFile).not.toHaveBeenCalled();
expect(execaCommand).not.toHaveBeenCalled();
});

it.each([
// prettier-ignore
{ releaseType: 'major', currentVersion: '1.1.1', expectedVersion: '2.0.0' },
Expand Down Expand Up @@ -159,11 +230,21 @@ describe('Version', () => {
{ 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' },
// prettier-ignore
{ apply: true, currentVersion: '1.0.0', deferredNextVersion: '1.2.0', expectedVersion: '1.2.0' },
])(
'bump with type: "$releaseType", pre id "$preId" or exact "$exact", from: $currentVersion, to: $expectedVersion',
async ({ releaseType, preId, exact, currentVersion, expectedVersion }) => {
'bump with type: "$releaseType", pre id "$preId" or exact "$exact" or apply $apply, from: $currentVersion, to: $expectedVersion',
async ({
releaseType,
preId,
exact,
apply,
currentVersion,
expectedVersion,
deferredNextVersion,
}) => {
fsExtra.__setMockFiles({
[CODE_PACKAGE_JSON_PATH]: JSON.stringify({ version: currentVersion }),
[CODE_PACKAGE_JSON_PATH]: JSON.stringify({ version: currentVersion, deferredNextVersion }),
[MANAGER_API_VERSION_PATH]: `export const version = "${currentVersion}";`,
[VERSIONS_PATH]: `export default { "@storybook/addon-a11y": "${currentVersion}" };`,
[A11Y_PACKAGE_JSON_PATH]: JSON.stringify({
Expand All @@ -185,7 +266,17 @@ describe('Version', () => {
[VERSIONS_PATH]: `export default { "@storybook/addon-a11y": "${currentVersion}" };`,
});

await version({ releaseType, preId, exact });
await version({ releaseType, preId, exact, apply });
expect(fsExtra.writeJson).toHaveBeenCalledTimes(apply ? 3 : 2);
if (apply) {
// eslint-disable-next-line jest/no-conditional-expect -- guarded against problems with the assertion above
expect(fsExtra.writeJson).toHaveBeenCalledWith(
CODE_PACKAGE_JSON_PATH,
// this call is the write that removes the "deferredNextVersion" property
{ version: currentVersion },
{ spaces: 2 }
);
}

expect(fsExtra.writeJson).toHaveBeenCalledWith(
CODE_PACKAGE_JSON_PATH,
Expand Down Expand Up @@ -231,4 +322,21 @@ describe('Version', () => {
});
}
);

it('should only set version in "deferredNextVersion" when using --deferred', async () => {
fsExtra.__setMockFiles({
[CODE_PACKAGE_JSON_PATH]: JSON.stringify({ version: '1.0.0' }),
});

await version({ releaseType: 'premajor', preId: 'beta', deferred: true });

expect(fsExtra.writeJson).toHaveBeenCalledTimes(1);
expect(fsExtra.writeJson).toHaveBeenCalledWith(
CODE_PACKAGE_JSON_PATH,
{ version: '1.0.0', deferredNextVersion: '2.0.0-beta.0' },
{ spaces: 2 }
);
expect(fsExtra.writeFile).not.toHaveBeenCalled();
expect(execaCommand).not.toHaveBeenCalled();
});
});
Loading