From 3d88455be92142d4d604d0a14ed5bb51210618d0 Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Mon, 22 Apr 2024 22:22:46 +0100 Subject: [PATCH 1/4] cli: use backstage:^ versions when yarn plugin is installed Introduces basic support for backstage:^ versions in the versions:bump command in backstage-cli. The behavior changes in the following ways: 1. If the yarn plugin is detected, write backstage:^ versions in package.json files in the repo where appropriate. 2. Update breaking change detection to correctly resolve lockfile entries for packages using backstage:^ versions. Signed-off-by: MT Lewis --- .changeset/two-islands-deny.md | 5 ++ packages/cli/package.json | 1 + packages/cli/src/commands/versions/bump.ts | 91 ++++++++++++++++++++-- yarn.lock | 1 + 4 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 .changeset/two-islands-deny.md diff --git a/.changeset/two-islands-deny.md b/.changeset/two-islands-deny.md new file mode 100644 index 0000000000..1ee2020b5e --- /dev/null +++ b/.changeset/two-islands-deny.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Add support for `backstage:^` version ranges to versions:bump when using the experimental yarn plugin diff --git a/packages/cli/package.json b/packages/cli/package.json index 99eba89987..d8f17b1aa7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -117,6 +117,7 @@ "jest-css-modules": "^2.1.0", "jest-environment-jsdom": "^29.0.2", "jest-runtime": "^29.0.2", + "js-yaml": "^4.1.0", "json-schema": "^0.4.0", "lodash": "^4.17.21", "mini-css-extract-plugin": "^2.4.2", diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index 5b981d3890..235bd30435 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -25,6 +25,8 @@ import chalk from 'chalk'; import ora from 'ora'; import semver from 'semver'; import { OptionValues } from 'commander'; +import yaml from 'js-yaml'; +import z from 'zod'; import { isError, NotFoundError } from '@backstage/errors'; import { resolve as resolvePath } from 'path'; import { run } from '../../lib/run'; @@ -76,6 +78,14 @@ type PkgVersionInfo = { export default async (opts: OptionValues) => { const lockfilePath = paths.resolveTargetRoot('yarn.lock'); const lockfile = await Lockfile.load(lockfilePath); + const hasYarnPlugin = await getHasYarnPlugin(); + + if (hasYarnPlugin) { + console.log( + `Backstage yarn plugin detected, will use backstage: version ranges where possible...`, + ); + } + let pattern = opts.pattern; if (!pattern) { @@ -177,12 +187,27 @@ export default async (opts: OptionValues) => { for (const depType of DEP_TYPES) { if (depType in pkgJson && dep.name in pkgJson[depType]) { const oldRange = pkgJson[depType][dep.name]; - pkgJson[depType][dep.name] = dep.range; + + // backstage:^ are written to the lockfile as + // backstage:, so that updates to + // backstage.json can be detected during yarn install. In order to + // locate the corresponding lockfile entry for "backstage:^" + // versions, we need to perform the same transformation. + const oldLockfileRange = await asLockfileVersion(oldRange); + + // Don't use backstage:^ versions for peerDependencies; they only + // support npm and workspace: versions. + const useBackstageRange = + hasYarnPlugin && depType !== 'peerDependencies'; + + const newRange = useBackstageRange ? 'backstage:^' : dep.range; + + pkgJson[depType][dep.name] = newRange; // Check if the update was at least a pre-v1 minor or post-v1 major release const lockfileEntry = lockfile .get(dep.name) - ?.find(entry => entry.range === oldRange); + ?.find(entry => entry.range === oldLockfileRange); if (lockfileEntry) { const from = lockfileEntry.version; const to = dep.target; @@ -354,16 +379,23 @@ export function createVersionFinder(options: { }; } -export async function bumpBackstageJsonVersion(version: string) { - const backstageJsonPath = paths.resolveTargetRoot(BACKSTAGE_JSON); - const backstageJson = await fs.readJSON(backstageJsonPath).catch(e => { +function getBackstageJsonPath() { + return paths.resolveTargetRoot(BACKSTAGE_JSON); +} + +async function getBackstageJson() { + const backstageJsonPath = getBackstageJsonPath(); + return fs.readJSON(backstageJsonPath).catch(e => { if (e.code === 'ENOENT') { // gracefully continue in case the file doesn't exist return; } throw e; }); +} +export async function bumpBackstageJsonVersion(version: string) { + const backstageJson = await getBackstageJson(); const prevVersion = backstageJson?.version; if (prevVersion === version) { @@ -394,7 +426,7 @@ export async function bumpBackstageJsonVersion(version: string) { } await fs.writeJson( - backstageJsonPath, + getBackstageJsonPath(), { ...backstageJson, version }, { spaces: 2, @@ -403,6 +435,53 @@ export async function bumpBackstageJsonVersion(version: string) { ); } +async function asLockfileVersion(version: string) { + if (version === 'backstage:^') { + return `backstage:${(await getBackstageJson())?.version}`; + } + + return version; +} + +const yarnRcSchema = z.object({ + plugins: z + .array( + z.object({ + path: z.string(), + }), + ) + .optional(), +}); + +async function getHasYarnPlugin() { + const yarnRcPath = paths.resolveTargetRoot('.yarnrc.yml'); + const yarnRcContent = await fs.readFile(yarnRcPath, 'utf-8').catch(e => { + if (e.code === 'ENOENT') { + // gracefully continue in case the file doesn't exist + return ''; + } + throw e; + }); + + if (!yarnRcContent) { + return false; + } + + const parseResult = yarnRcSchema.safeParse(yaml.load(yarnRcContent)); + + if (!parseResult.success) { + throw new Error( + `Unexpected content in .yarnrc.yml: ${parseResult.error.toString()}`, + ); + } + + const yarnRc = parseResult.data; + + return yarnRc.plugins?.some( + plugin => plugin.path === '.yarn/plugins/@yarnpkg/plugin-backstage.cjs', + ); +} + export async function runYarnInstall() { const spinner = ora({ prefixText: `Running ${chalk.blue('yarn install')} to install new versions`, diff --git a/yarn.lock b/yarn.lock index 82455ac5f1..4fe52d4987 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3993,6 +3993,7 @@ __metadata: jest-css-modules: ^2.1.0 jest-environment-jsdom: ^29.0.2 jest-runtime: ^29.0.2 + js-yaml: ^4.1.0 json-schema: ^0.4.0 lodash: ^4.17.21 mini-css-extract-plugin: ^2.4.2 From 1e4b91f6d7180cdaad721bf1c2212c1ae64d0ae6 Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Sat, 27 Jul 2024 14:48:31 +0100 Subject: [PATCH 2/4] backstage-cli: only use backstage:^ version if the package exists in the new manifest Signed-off-by: MT Lewis --- packages/cli/src/commands/versions/bump.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index 235bd30435..6579fc84d8 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -195,10 +195,17 @@ export default async (opts: OptionValues) => { // versions, we need to perform the same transformation. const oldLockfileRange = await asLockfileVersion(oldRange); - // Don't use backstage:^ versions for peerDependencies; they only - // support npm and workspace: versions. const useBackstageRange = - hasYarnPlugin && depType !== 'peerDependencies'; + hasYarnPlugin && + // Only use backstage:^ versions if the package is present in + // the manifest for the release we're bumping to. + releaseManifest.packages.find( + ({ name: manifestPackageName }) => + dep.name === manifestPackageName, + ) && + // Don't use backstage:^ versions for peerDependencies; they only + // support npm and workspace: versions. + depType !== 'peerDependencies'; const newRange = useBackstageRange ? 'backstage:^' : dep.range; From 7dbf877e8d9dbe74db435f4645f11183acc1eb60 Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Sat, 27 Jul 2024 15:03:57 +0100 Subject: [PATCH 3/4] backstage-cli: improve messaging around yarn-plugin Moves the message to the end to make it more likely users will see it, and add some details on how to migrate away from the plugin. Signed-off-by: MT Lewis --- packages/cli/src/commands/versions/bump.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index 6579fc84d8..e092302393 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -80,12 +80,6 @@ export default async (opts: OptionValues) => { const lockfile = await Lockfile.load(lockfilePath); const hasYarnPlugin = await getHasYarnPlugin(); - if (hasYarnPlugin) { - console.log( - `Backstage yarn plugin detected, will use backstage: version ranges where possible...`, - ); - } - let pattern = opts.pattern; if (!pattern) { @@ -298,6 +292,21 @@ export default async (opts: OptionValues) => { console.log(); } + if (hasYarnPlugin) { + console.log(); + console.log( + chalk.blue( + `${chalk.bold( + 'NOTE', + )}: this bump used backstage:^ versions in package.json files, since the Backstage ` + + `yarn plugin was detected in the repository. To migrate back to explicit npm versions, ` + + `remove the plugin by running "yarn plugin remove @yarnpkg/plugin-backstage", then ` + + `repeat this command.`, + ), + ); + console.log(); + } + console.log(chalk.green('Version bump complete!')); } From ac9dc88477a48727d6e106a1e63bad94025312f5 Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Tue, 10 Sep 2024 14:09:04 +0100 Subject: [PATCH 4/4] backstage-cli: remove dependency on js-yaml Signed-off-by: MT Lewis --- packages/cli/package.json | 1 - packages/cli/src/commands/versions/bump.ts | 4 ++-- yarn.lock | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index d8f17b1aa7..99eba89987 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -117,7 +117,6 @@ "jest-css-modules": "^2.1.0", "jest-environment-jsdom": "^29.0.2", "jest-runtime": "^29.0.2", - "js-yaml": "^4.1.0", "json-schema": "^0.4.0", "lodash": "^4.17.21", "mini-css-extract-plugin": "^2.4.2", diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index e092302393..e41bd98c49 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -25,7 +25,7 @@ import chalk from 'chalk'; import ora from 'ora'; import semver from 'semver'; import { OptionValues } from 'commander'; -import yaml from 'js-yaml'; +import yaml from 'yaml'; import z from 'zod'; import { isError, NotFoundError } from '@backstage/errors'; import { resolve as resolvePath } from 'path'; @@ -483,7 +483,7 @@ async function getHasYarnPlugin() { return false; } - const parseResult = yarnRcSchema.safeParse(yaml.load(yarnRcContent)); + const parseResult = yarnRcSchema.safeParse(yaml.parse(yarnRcContent)); if (!parseResult.success) { throw new Error( diff --git a/yarn.lock b/yarn.lock index 4fe52d4987..82455ac5f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3993,7 +3993,6 @@ __metadata: jest-css-modules: ^2.1.0 jest-environment-jsdom: ^29.0.2 jest-runtime: ^29.0.2 - js-yaml: ^4.1.0 json-schema: ^0.4.0 lodash: ^4.17.21 mini-css-extract-plugin: ^2.4.2