diff --git a/.github/workflows/prepare-patch-release.yml b/.github/workflows/prepare-patch-release.yml index 26a9453b38ca..d5a8ca655af6 100644 --- a/.github/workflows/prepare-patch-release.yml +++ b/.github/workflows/prepare-patch-release.yml @@ -44,9 +44,10 @@ jobs: yarn-v1-${{ hashFiles('scripts/yarn.lock') }} yarn-v1 - - name: Install Script Dependencies + - name: Install Dependencies + working-directory: . run: | - yarn install + yarn task --task=install - name: Check if pull request is frozen if: github.event_name != 'workflow_dispatch' @@ -85,10 +86,6 @@ jobs: git config --global user.email 'github-actions[bot]@users.noreply.github.com' yarn release:pick-patches - - name: Install code dependencies - working-directory: . - run: yarn task --task=install --start-from=install - - name: Bump version id: bump-version if: steps.unreleased-changes.outputs.has-changes-to-release == 'true' diff --git a/.github/workflows/prepare-prerelease.yml b/.github/workflows/prepare-prerelease.yml index f597cdad3d0a..932c4b31f64f 100644 --- a/.github/workflows/prepare-prerelease.yml +++ b/.github/workflows/prepare-prerelease.yml @@ -65,9 +65,10 @@ jobs: yarn-v1-${{ hashFiles('scripts/yarn.lock') }} yarn-v1 - - name: Install Script Dependencies + - name: Install Dependencies + working-directory: . run: | - yarn install + yarn task --task=install - name: Check if pull request is frozen if: github.event_name != 'workflow_dispatch' @@ -104,10 +105,6 @@ jobs: gh run cancel ${{ github.run_id }} gh run watch ${{ github.run_id }} - - name: Install code dependencies - working-directory: . - run: yarn task --task=install --start-from=install - - name: Bump version id: bump-version run: | diff --git a/code/package.json b/code/package.json index 8f34d5ae3d53..b05f4d8beea3 100644 --- a/code/package.json +++ b/code/package.json @@ -325,10 +325,6 @@ [ "dependencies", "Dependency Upgrades" - ], - [ - "other", - "Other" ] ] } diff --git a/scripts/release/__tests__/version.test.ts b/scripts/release/__tests__/version.test.ts index ccf9cb825438..c33c5fc31b8b 100644 --- a/scripts/release/__tests__/version.test.ts +++ b/scripts/release/__tests__/version.test.ts @@ -14,8 +14,21 @@ jest.mock('../../../code/lib/cli/src/versions', () => ({ jest.mock('../../utils/exec'); const { execaCommand } = require('../../utils/exec'); +jest.mock('../../utils/workspace', () => ({ + getWorkspaces: jest.fn().mockResolvedValue([ + { + name: '@storybook/addon-a11y', + location: 'addons/a11y', + }, + ]), +})); + +jest.spyOn(console, 'log').mockImplementation(() => {}); +jest.spyOn(console, 'warn').mockImplementation(() => {}); +jest.spyOn(console, 'error').mockImplementation(() => {}); + beforeEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); describe('Version', () => { @@ -29,6 +42,7 @@ describe('Version', () => { 'version.ts' ); const VERSIONS_PATH = path.join(CODE_DIR_PATH, 'lib', 'cli', 'src', 'versions.ts'); + const A11Y_PACKAGE_JSON_PATH = path.join(CODE_DIR_PATH, 'addons', 'a11y', 'package.json'); it('should throw when release type is invalid', async () => { fsExtra.__setMockFiles({ @@ -152,6 +166,23 @@ describe('Version', () => { [CODE_PACKAGE_JSON_PATH]: JSON.stringify({ version: currentVersion }), [MANAGER_API_VERSION_PATH]: `export const version = "${currentVersion}";`, [VERSIONS_PATH]: `export default { "@storybook/addon-a11y": "${currentVersion}" };`, + [A11Y_PACKAGE_JSON_PATH]: JSON.stringify({ + version: currentVersion, + dependencies: { + '@storybook/core-server': currentVersion, + 'unrelated-package-a': '1.0.0', + }, + devDependencies: { + 'unrelated-package-b': currentVersion, + '@storybook/core-common': `^${currentVersion}`, + }, + peerDependencies: { + '@storybook/preview-api': `*`, + '@storybook/svelte': '0.1.1', + '@storybook/manager-api': `~${currentVersion}`, + }, + }), + [VERSIONS_PATH]: `export default { "@storybook/addon-a11y": "${currentVersion}" };`, }); await version({ releaseType, preId, exact }); @@ -161,19 +192,43 @@ describe('Version', () => { { 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) + `export const version = "${expectedVersion}";` ); expect(fsExtra.writeFile).toHaveBeenCalledWith( VERSIONS_PATH, - expect.stringContaining(expectedVersion) + `export default { "@storybook/addon-a11y": "${expectedVersion}" };` ); + expect(fsExtra.writeJson).toHaveBeenCalledWith( + A11Y_PACKAGE_JSON_PATH, + expect.objectContaining({ + // should update package version + version: expectedVersion, + dependencies: { + // should update storybook dependencies matching current version + '@storybook/core-server': expectedVersion, + 'unrelated-package-a': '1.0.0', + }, + devDependencies: { + // should not update non-storybook dependencies, even if they match current version + 'unrelated-package-b': currentVersion, + // should update dependencies with range modifiers correctly (e.g. ^1.0.0 -> ^2.0.0) + '@storybook/core-common': `^${expectedVersion}`, + }, + peerDependencies: { + // should not update storybook depenedencies if they don't match current version + '@storybook/preview-api': `*`, + '@storybook/svelte': '0.1.1', + '@storybook/manager-api': `~${expectedVersion}`, + }, + }), + { spaces: 2 } + ); + expect(execaCommand).toHaveBeenCalledWith('yarn install --mode=update-lockfile', { + cwd: path.join(CODE_DIR_PATH), + stdio: undefined, + }); } ); }); diff --git a/scripts/release/generate-pr-description.ts b/scripts/release/generate-pr-description.ts index b36fd7fc7165..8e49b17c3e73 100644 --- a/scripts/release/generate-pr-description.ts +++ b/scripts/release/generate-pr-description.ts @@ -48,6 +48,7 @@ const LABELS_BY_IMPORTANCE = { 'feature request': '✨ Feature Request', bug: '🐛 Bug', maintenance: '🔧 Maintenance', + dependencies: '📦 Dependencies', documentation: '📝 Documentation', build: '🏗️ Build', unknown: '❔ Missing Label', diff --git a/scripts/release/version.ts b/scripts/release/version.ts index 01c6ffe46027..03f608567392 100644 --- a/scripts/release/version.ts +++ b/scripts/release/version.ts @@ -1,15 +1,14 @@ /* eslint-disable no-console */ import { setOutput } from '@actions/core'; -import { ensureDir, readFile, readJson, writeFile, writeJson } from 'fs-extra'; +import { 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 type { Workspace } from '../utils/workspace'; +import { getWorkspaces } from '../utils/workspace'; import { execaCommand } from '../utils/exec'; -import { listOfPackages } from '../utils/list-packages'; -import packageVersionMap from '../../code/lib/cli/src/versions'; program .name('version') @@ -98,39 +97,6 @@ const bumpCodeVersion = async (nextVersion: string) => { 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')}`); }; @@ -152,6 +118,76 @@ const bumpVersionSources = async (currentVersion: string, nextVersion: string) = console.log(`✅ Bumped versions in:\n ${chalk.cyan(filesToUpdate.join('\n '))}`); }; +const bumpAllPackageJsons = async ({ + packages, + currentVersion, + nextVersion, + verbose, +}: { + packages: Workspace[]; + currentVersion: string; + nextVersion: string; + verbose?: boolean; +}) => { + console.log( + `🤜 Bumping versions and dependencies in ${chalk.cyan( + `all ${packages.length} package.json` + )}'s...` + ); + // 1. go through all packages in the monorepo + await Promise.all( + packages.map(async (pkg) => { + // 2. get the package.json + const packageJsonPath = path.join(CODE_DIR_PATH, pkg.location, 'package.json'); + const packageJson: { + version: string; + dependencies: Record; + devDependencies: Record; + peerDependencies: Record; + [key: string]: any; + } = await readJson(packageJsonPath); + // 3. bump the version + packageJson.version = nextVersion; + const { dependencies, devDependencies, peerDependencies } = packageJson; + if (verbose) { + console.log( + ` Bumping ${chalk.blue(pkg.name)}'s version to ${chalk.yellow(nextVersion)}` + ); + } + // 4. go through all deps in the package.json + Object.entries({ dependencies, devDependencies, peerDependencies }).forEach( + ([depType, deps]) => { + if (!deps) { + return; + } + // 5. find all storybook deps + Object.entries(deps) + .filter( + ([depName, depVersion]) => + depName.startsWith('@storybook/') && + // ignore storybook dependneices that don't use the current version + depVersion.includes(currentVersion) + ) + .forEach(([depName, depVersion]) => { + // 6. bump the version of any found storybook dep + const nextDepVersion = depVersion.replace(currentVersion, nextVersion); + if (verbose) { + console.log( + ` Bumping ${chalk.blue(pkg.name)}'s ${chalk.red(depType)} on ${chalk.green( + depName + )} from ${chalk.yellow(depVersion)} to ${chalk.yellow(nextDepVersion)}` + ); + } + packageJson[depType][depName] = nextDepVersion; + }); + } + ); + await writeJson(packageJsonPath, packageJson, { spaces: 2 }); + }) + ); + console.log(`✅ Bumped peer dependency versions in ${chalk.cyan('all packages')}`); +}; + export const run = async (options: unknown) => { if (!validateOptions(options)) { return; @@ -160,15 +196,14 @@ export const run = async (options: unknown) => { console.log(`🚛 Finding Storybook packages...`); - const [packages, currentVersion] = await Promise.all([listOfPackages(), getCurrentVersion()]); + const [packages, currentVersion] = await Promise.all([getWorkspaces(), 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)}` + (pkg) => `${chalk.green(pkg.name.padEnd(60))}: ${chalk.cyan(pkg.location)}` ); console.log(`📦 Packages: ${formattedPackages.join('\n ')}`); @@ -200,8 +235,15 @@ export const run = async (options: unknown) => { console.log(`⏭ Bumping all packages to ${chalk.blue(nextVersion)}...`); await bumpCodeVersion(nextVersion); - await bumpAllPackageVersions(nextVersion, verbose); await bumpVersionSources(currentVersion, nextVersion); + await bumpAllPackageJsons({ packages, currentVersion, nextVersion, verbose }); + + console.log(`⬆️ Updating lock file with ${chalk.blue('yarn install --mode=update-lockfile')}`); + await execaCommand(`yarn install --mode=update-lockfile`, { + cwd: path.join(CODE_DIR_PATH), + stdio: verbose ? 'inherit' : undefined, + }); + console.log(`✅ Updated lock file with ${chalk.blue('yarn install --mode=update-lockfile')}`); if (process.env.GITHUB_ACTIONS === 'true') { setOutput('current-version', currentVersion); diff --git a/scripts/utils/workspace.ts b/scripts/utils/workspace.ts index c490d593f1b5..3219c558599f 100644 --- a/scripts/utils/workspace.ts +++ b/scripts/utils/workspace.ts @@ -4,7 +4,7 @@ import { execaCommand } from './exec'; export type Workspace = { name: string; location: string }; -async function getWorkspaces() { +export async function getWorkspaces() { const { stdout } = await execaCommand('yarn workspaces list --json', { cwd: CODE_DIRECTORY, shell: true,