diff --git a/.changeset/cyan-bottles-hug.md b/.changeset/cyan-bottles-hug.md new file mode 100644 index 0000000000..7c2083c4ff --- /dev/null +++ b/.changeset/cyan-bottles-hug.md @@ -0,0 +1,8 @@ +--- +'@backstage/cli': patch +'@backstage/cli-common': patch +--- + +Keep backstage.json in sync + +The `versions:bump` script now takes care about updating the `version` property inside `backstage.json` file. The file is created if is not present. diff --git a/.changeset/healthy-kids-mate.md b/.changeset/healthy-kids-mate.md new file mode 100644 index 0000000000..10e8222a15 --- /dev/null +++ b/.changeset/healthy-kids-mate.md @@ -0,0 +1,11 @@ +--- +'@backstage/create-app': patch +--- + +Create backstage.json file + +`@backstage/create-app` will create a new `backstage.json` file. At this point, the file will contain a `version` property, representing the version of `@backstage/create-app` used for creating the application. If the backstage's application has been bootstrapped using an older version of `@backstage/create-app`, the `backstage.json` file can be created and kept in sync, together with all the changes of the latest version of backstage, by running the following script: + +```bash +yarn backstage-cli versions:bump +``` diff --git a/packages/cli-common/api-report.md b/packages/cli-common/api-report.md index 4b61cfedb8..34f56c3f19 100644 --- a/packages/cli-common/api-report.md +++ b/packages/cli-common/api-report.md @@ -3,6 +3,9 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +// @public +export const BACKSTAGE_JSON = 'backstage.json'; + // @public export function findPaths(searchDir: string): Paths; diff --git a/packages/cli-common/src/index.ts b/packages/cli-common/src/index.ts index e7cda19514..8809764b43 100644 --- a/packages/cli-common/src/index.ts +++ b/packages/cli-common/src/index.ts @@ -20,6 +20,6 @@ * @packageDocumentation */ -export { findPaths } from './paths'; +export { findPaths, BACKSTAGE_JSON } from './paths'; export { isChildPath } from './isChildPath'; export type { Paths, ResolveFunc } from './paths'; diff --git a/packages/cli-common/src/paths.ts b/packages/cli-common/src/paths.ts index c9f486752f..2b1ca34ca5 100644 --- a/packages/cli-common/src/paths.ts +++ b/packages/cli-common/src/paths.ts @@ -167,3 +167,10 @@ export function findPaths(searchDir: string): Paths { resolveTargetRoot: (...paths) => resolvePath(getTargetRoot(), ...paths), }; } + +/** + * The name of the backstage's config file + * + * @public + */ +export const BACKSTAGE_JSON = 'backstage.json'; diff --git a/packages/cli/src/commands/versions/bump.test.ts b/packages/cli/src/commands/versions/bump.test.ts index 96bd1ab6fd..bc4049f1f4 100644 --- a/packages/cli/src/commands/versions/bump.test.ts +++ b/packages/cli/src/commands/versions/bump.test.ts @@ -20,7 +20,7 @@ import { resolve as resolvePath } from 'path'; import { paths } from '../../lib/paths'; import { mapDependencies } from '../../lib/versioning'; import * as runObj from '../../lib/run'; -import bump from './bump'; +import bump, { bumpBackstageJsonVersion } from './bump'; import { withLogCollector } from '@backstage/test-utils'; // Remove log coloring to simplify log matching @@ -141,7 +141,7 @@ describe('bump', () => { 'Version bump complete!', ]); - expect(runObj.runPlain).toHaveBeenCalledTimes(3); + expect(runObj.runPlain).toHaveBeenCalledTimes(4); expect(runObj.runPlain).toHaveBeenCalledWith( 'yarn', 'info', @@ -245,3 +245,64 @@ describe('bump', () => { }); }); }); + +describe('bumpBackstageJsonVersion', () => { + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + + it('should bump version in backstage.json', async () => { + mockFs({ + '/backstage.json': JSON.stringify({ version: '0.0.1' }), + }); + paths.targetDir = '/'; + const latest = '1.4.1'; + jest + .spyOn(paths, 'resolveTargetRoot') + .mockImplementation((...path) => resolvePath('/', ...path)); + jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) => + JSON.stringify({ + type: 'inspect', + data: { + name, + 'dist-tags': { + latest, + }, + }, + }), + ); + jest.spyOn(runObj, 'run').mockResolvedValue(undefined); + + await bumpBackstageJsonVersion(); + + const json = await fs.readJson('/backstage.json'); + expect(json).toEqual({ version: '1.4.1' }); + }); + + it("should create backstage.json if doesn't exist", async () => { + mockFs({}); + paths.targetDir = '/'; + const latest = '1.4.1'; + jest + .spyOn(paths, 'resolveTargetRoot') + .mockImplementation((...path) => resolvePath('/', ...path)); + jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) => + JSON.stringify({ + type: 'inspect', + data: { + name, + 'dist-tags': { + latest, + }, + }, + }), + ); + jest.spyOn(runObj, 'run').mockResolvedValue(undefined); + + await bumpBackstageJsonVersion(); + + const json = await fs.readJson('/backstage.json'); + expect(json).toEqual({ version: '1.4.1' }); + }); +}); diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index 7b967839b2..3898c1654a 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -27,6 +27,7 @@ import { Lockfile, } from '../../lib/versioning'; import { includedFilter, forbiddenDuplicatesFilter } from './lint'; +import { BACKSTAGE_JSON } from '@backstage/cli-common'; const DEP_TYPES = [ 'dependencies', @@ -182,6 +183,10 @@ export default async () => { await fs.writeJson(pkgPath, pkgJson, { spaces: 2 }); }); + console.log(); + + await bumpBackstageJsonVersion(); + console.log(); console.log( `Running ${chalk.blue('yarn install')} to install new versions`, @@ -271,6 +276,41 @@ function createVersionFinder() { }; } +export async function bumpBackstageJsonVersion() { + const backstageJsonPath = paths.resolveTargetRoot(BACKSTAGE_JSON); + const backstageJson = await fs.readJSON(backstageJsonPath).catch(e => { + if (e.code === 'ENOENT') { + // gracefully continue in case the file doesn't exist + return; + } + throw e; + }); + + const info = await fetchPackageInfo('@backstage/create-app'); + const { latest } = info['dist-tags']; + + if (backstageJson?.version === latest) { + return; + } + + console.log( + chalk.yellow( + typeof backstageJson === 'undefined' + ? `Creating ${BACKSTAGE_JSON}` + : `Bumping version in ${BACKSTAGE_JSON}`, + ), + ); + + await fs.writeJson( + backstageJsonPath, + { ...backstageJson, version: latest }, + { + spaces: 2, + encoding: 'utf8', + }, + ); +} + async function workerThreads( count: number, items: IterableIterator, diff --git a/packages/create-app/src/createApp.test.ts b/packages/create-app/src/createApp.test.ts index 09099e2d15..ee01e8fd42 100644 --- a/packages/create-app/src/createApp.test.ts +++ b/packages/create-app/src/createApp.test.ts @@ -59,7 +59,7 @@ describe('command entrypoint', () => { test('should call expected tasks with no path option', async () => { const cmd = {} as unknown as Command; - await createApp(cmd); + await createApp(cmd, '1.0.0'); expect(checkAppExistsMock).toHaveBeenCalled(); expect(createTemporaryAppFolderMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); @@ -69,7 +69,7 @@ describe('command entrypoint', () => { it('should call expected tasks with path option', async () => { const cmd = { path: 'myDirectory' } as unknown as Command; - await createApp(cmd); + await createApp(cmd, '1.0.0'); expect(checkPathExistsMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); expect(buildAppMock).toHaveBeenCalled(); @@ -77,7 +77,7 @@ describe('command entrypoint', () => { it('should not call `buildAppTask` when `skipInstall` is supplied', async () => { const cmd = { skipInstall: true } as unknown as Command; - await createApp(cmd); + await createApp(cmd, '1.0.0'); expect(buildAppMock).not.toHaveBeenCalled(); }); }); diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 8823e6c858..d301792380 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -30,7 +30,7 @@ import { templatingTask, } from './lib/tasks'; -export default async (cmd: Command): Promise => { +export default async (cmd: Command, version: string): Promise => { /* eslint-disable-next-line no-restricted-syntax */ const paths = findPaths(__dirname); @@ -82,7 +82,7 @@ export default async (cmd: Command): Promise => { await checkPathExistsTask(appDir); Task.section('Preparing files'); - await templatingTask(templateDir, cmd.path, answers); + await templatingTask(templateDir, cmd.path, answers, version); } else { // Template to temporary location, and then move files @@ -93,7 +93,7 @@ export default async (cmd: Command): Promise => { await createTemporaryAppFolderTask(tempDir); Task.section('Preparing files'); - await templatingTask(templateDir, tempDir, answers); + await templatingTask(templateDir, tempDir, answers, version); Task.section('Moving to final location'); await moveAppTask(tempDir, appDir, answers.name); diff --git a/packages/create-app/src/index.ts b/packages/create-app/src/index.ts index 1f37be56d2..8a2da2fad8 100644 --- a/packages/create-app/src/index.ts +++ b/packages/create-app/src/index.ts @@ -38,7 +38,7 @@ const main = (argv: string[]) => { '--skip-install', 'Skip the install and builds steps after creating the app', ) - .action(createApp); + .action(cmd => createApp(cmd, version)); program.parse(argv); }; diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index d69526cb49..24df97c694 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -195,9 +195,12 @@ describe('templatingTask', () => { name: 'SuperCoolBackstageInstance', dbTypeSqlite: true, }; - await templatingTask(templateDir, destinationDir, context); + await templatingTask(templateDir, destinationDir, context, '1.0.0'); expect(fs.existsSync('templatedApp/package.json')).toBe(true); expect(fs.existsSync('templatedApp/.dockerignore')).toBe(true); + await expect(fs.readJson('templatedApp/backstage.json')).resolves.toEqual({ + version: '1.0.0', + }); // catalog was populated with `context.name` expect( fs.readFileSync('templatedApp/catalog-info.yaml', 'utf-8'), diff --git a/packages/create-app/src/lib/tasks.ts b/packages/create-app/src/lib/tasks.ts index 3961e84753..2793088892 100644 --- a/packages/create-app/src/lib/tasks.ts +++ b/packages/create-app/src/lib/tasks.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { BACKSTAGE_JSON } from '@backstage/cli-common'; import chalk from 'chalk'; import fs from 'fs-extra'; import handlebars from 'handlebars'; @@ -22,6 +23,7 @@ import recursive from 'recursive-readdir'; import { basename, dirname, + join, resolve as resolvePath, relative as relativePath, } from 'path'; @@ -84,6 +86,7 @@ export async function templatingTask( templateDir: string, destinationDir: string, context: any, + version: string, ) { const files = await recursive(templateDir).catch(error => { throw new Error(`Failed to read template directory: ${error.message}`); @@ -133,6 +136,12 @@ export async function templatingTask( }); } } + await Task.forItem('creating', BACKSTAGE_JSON, () => + fs.writeFile( + join(destinationDir, BACKSTAGE_JSON), + `{\n "version": ${JSON.stringify(version)}\n}\n`, + ), + ); } /**