Merge pull request #9092 from backstage/mob/versions-bump2
cli: Add versions:bump params for bumping to specific releases
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Introduces a new `--release` parameter to the `backstage-cli versions:bump` command.
|
||||
The release can be either a specific version, for example `0.99.1`, or the latest `main` or `next` release.
|
||||
The default behavior is to bump to the latest `main` release.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/release-manifests': patch
|
||||
---
|
||||
|
||||
Introduces a new package with utilities for fetching release manifests.
|
||||
This package will primarily be used by the `@backstage/cli` package.
|
||||
@@ -32,6 +32,7 @@
|
||||
"@backstage/config": "^0.1.13",
|
||||
"@backstage/config-loader": "^0.9.3",
|
||||
"@backstage/errors": "^0.2.0",
|
||||
"@backstage/release-manifests": "^0.0.0",
|
||||
"@backstage/types": "^0.1.1",
|
||||
"@hot-loader/react-dom": "^16.13.0",
|
||||
"@manypkg/get-packages": "^1.1.3",
|
||||
@@ -143,7 +144,8 @@
|
||||
"del": "^6.0.0",
|
||||
"mock-fs": "^5.1.0",
|
||||
"nodemon": "^2.0.2",
|
||||
"ts-node": "^10.0.0"
|
||||
"ts-node": "^10.0.0",
|
||||
"msw": "^0.35.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@microsoft/api-extractor": "^7.19.2"
|
||||
|
||||
@@ -341,6 +341,11 @@ export function registerCommands(program: CommanderStatic) {
|
||||
'--pattern <glob>',
|
||||
'Override glob for matching packages to upgrade',
|
||||
)
|
||||
.option(
|
||||
'--release <version|next|main>',
|
||||
'Bump to a specific Backstage release line or version',
|
||||
'main',
|
||||
)
|
||||
.description('Bump Backstage packages to the latest versions')
|
||||
.action(lazy(() => import('./versions/bump').then(m => m.default)));
|
||||
|
||||
|
||||
@@ -20,8 +20,14 @@ import { Command } from 'commander';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { paths } from '../../lib/paths';
|
||||
import * as runObj from '../../lib/run';
|
||||
import bump, { bumpBackstageJsonVersion } from './bump';
|
||||
import { withLogCollector } from '@backstage/test-utils';
|
||||
import bump, { bumpBackstageJsonVersion, createVersionFinder } from './bump';
|
||||
import {
|
||||
setupRequestMockHandlers,
|
||||
withLogCollector,
|
||||
} from '@backstage/test-utils';
|
||||
import { YarnInfoInspectData } from '../../lib/versioning/packages';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { rest } from 'msw';
|
||||
|
||||
// Remove log coloring to simplify log matching
|
||||
jest.mock('chalk', () => ({
|
||||
@@ -38,6 +44,7 @@ const REGISTRY_VERSIONS: { [name: string]: string } = {
|
||||
'@backstage/theme': '2.0.0',
|
||||
'@backstage-extra/custom': '1.1.0',
|
||||
'@backstage-extra/custom-two': '2.0.0',
|
||||
'@backstage/create-app': '1.0.0',
|
||||
};
|
||||
|
||||
const HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
@@ -82,6 +89,8 @@ describe('bump', () => {
|
||||
mockFs.restore();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
|
||||
it('should bump backstage dependencies', async () => {
|
||||
mockFs({
|
||||
@@ -121,9 +130,20 @@ describe('bump', () => {
|
||||
}),
|
||||
);
|
||||
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
|
||||
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://versions.backstage.io/v1/tags/main/manifest.json',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
packages: [],
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
const { log: logs } = await withLogCollector(['log'], async () => {
|
||||
await bump({ pattern: null } as unknown as Command);
|
||||
await bump({ pattern: null, release: 'main' } as unknown as Command);
|
||||
});
|
||||
expect(logs.filter(Boolean)).toEqual([
|
||||
'Using default pattern glob @backstage/*',
|
||||
@@ -142,7 +162,7 @@ describe('bump', () => {
|
||||
'Version bump complete!',
|
||||
]);
|
||||
|
||||
expect(runObj.runPlain).toHaveBeenCalledTimes(4);
|
||||
expect(runObj.runPlain).toHaveBeenCalledTimes(3);
|
||||
expect(runObj.runPlain).toHaveBeenCalledWith(
|
||||
'yarn',
|
||||
'info',
|
||||
@@ -179,6 +199,225 @@ describe('bump', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should prefer dependency versions from release manifest', async () => {
|
||||
mockFs({
|
||||
'/yarn.lock': lockfileMock,
|
||||
'/package.json': JSON.stringify({
|
||||
workspaces: {
|
||||
packages: ['packages/*'],
|
||||
},
|
||||
}),
|
||||
'/packages/a/package.json': JSON.stringify({
|
||||
name: 'a',
|
||||
dependencies: {
|
||||
'@backstage/core': '^1.0.5',
|
||||
},
|
||||
}),
|
||||
'/packages/b/package.json': JSON.stringify({
|
||||
name: 'b',
|
||||
dependencies: {
|
||||
'@backstage/core': '^1.0.3',
|
||||
'@backstage/theme': '^1.0.0',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(paths, 'resolveTargetRoot')
|
||||
.mockImplementation((...path) => resolvePath('/', ...path));
|
||||
jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) =>
|
||||
JSON.stringify({
|
||||
type: 'inspect',
|
||||
data: {
|
||||
name: name,
|
||||
'dist-tags': {
|
||||
latest: REGISTRY_VERSIONS[name],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://versions.backstage.io/v1/tags/main/manifest.json',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
releaseVersion: '0.0.1',
|
||||
packages: [
|
||||
{
|
||||
name: '@backstage/theme',
|
||||
version: '5.0.0',
|
||||
},
|
||||
{
|
||||
name: '@backstage/create-app',
|
||||
version: '3.0.0',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
const { log: logs } = await withLogCollector(['log'], async () => {
|
||||
await bump({ pattern: null, release: 'main' } as unknown as Command);
|
||||
});
|
||||
expect(logs.filter(Boolean)).toEqual([
|
||||
'Using default pattern glob @backstage/*',
|
||||
'Checking for updates of @backstage/core',
|
||||
'Checking for updates of @backstage/theme',
|
||||
'Checking for updates of @backstage/theme',
|
||||
'Checking for updates of @backstage/core-api',
|
||||
'Some packages are outdated, updating',
|
||||
'unlocking @backstage/core@^1.0.3 ~> 1.0.6',
|
||||
'unlocking @backstage/core-api@^1.0.6 ~> 1.0.7',
|
||||
'unlocking @backstage/core-api@^1.0.3 ~> 1.0.7',
|
||||
'bumping @backstage/theme in b to ^5.0.0',
|
||||
'Creating backstage.json',
|
||||
'Running yarn install to install new versions',
|
||||
'⚠️ The following packages may have breaking changes:',
|
||||
' @backstage/theme : 1.0.0 ~> 5.0.0',
|
||||
' https://github.com/backstage/backstage/blob/master/packages/theme/CHANGELOG.md',
|
||||
'Version bump complete!',
|
||||
]);
|
||||
|
||||
expect(runObj.runPlain).toHaveBeenCalledTimes(2);
|
||||
expect(runObj.runPlain).toHaveBeenCalledWith(
|
||||
'yarn',
|
||||
'info',
|
||||
'--json',
|
||||
'@backstage/core',
|
||||
);
|
||||
expect(runObj.runPlain).not.toHaveBeenCalledWith(
|
||||
'yarn',
|
||||
'info',
|
||||
'--json',
|
||||
'@backstage/theme',
|
||||
);
|
||||
|
||||
expect(runObj.run).toHaveBeenCalledTimes(1);
|
||||
expect(runObj.run).toHaveBeenCalledWith('yarn', ['install']);
|
||||
|
||||
const lockfileContents = await fs.readFile('/yarn.lock', 'utf8');
|
||||
expect(lockfileContents).toBe(lockfileMockResult);
|
||||
|
||||
const packageA = await fs.readJson('/packages/a/package.json');
|
||||
expect(packageA).toEqual({
|
||||
name: 'a',
|
||||
dependencies: {
|
||||
'@backstage/core': '^1.0.5', // not bumped since new version is within range
|
||||
},
|
||||
});
|
||||
const packageB = await fs.readJson('/packages/b/package.json');
|
||||
expect(packageB).toEqual({
|
||||
name: 'b',
|
||||
dependencies: {
|
||||
'@backstage/core': '^1.0.3', // not bumped
|
||||
'@backstage/theme': '^5.0.0', // bumped since newer
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should only bump packages in the manifest when a specific release is specified', async () => {
|
||||
mockFs({
|
||||
'/yarn.lock': lockfileMock,
|
||||
'/package.json': JSON.stringify({
|
||||
workspaces: {
|
||||
packages: ['packages/*'],
|
||||
},
|
||||
}),
|
||||
'/packages/a/package.json': JSON.stringify({
|
||||
name: 'a',
|
||||
dependencies: {
|
||||
'@backstage/core': '^1.0.5',
|
||||
},
|
||||
}),
|
||||
'/packages/b/package.json': JSON.stringify({
|
||||
name: 'b',
|
||||
dependencies: {
|
||||
'@backstage/core': '^1.0.3',
|
||||
'@backstage/theme': '^1.0.0',
|
||||
},
|
||||
}),
|
||||
});
|
||||
jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) =>
|
||||
JSON.stringify({
|
||||
type: 'inspect',
|
||||
data: {
|
||||
name: name,
|
||||
'dist-tags': {
|
||||
latest: REGISTRY_VERSIONS[name],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
jest
|
||||
.spyOn(paths, 'resolveTargetRoot')
|
||||
.mockImplementation((...path) => resolvePath('/', ...path));
|
||||
|
||||
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://versions.backstage.io/v1/releases/1.0.0/manifest.json',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
releaseVersion: '2.0.0',
|
||||
packages: [
|
||||
{ name: '@backstage/core', version: '5.0.0' },
|
||||
{ name: '@backstage/core-api', version: '5.0.0' },
|
||||
],
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
const { log: logs } = await withLogCollector(['log'], async () => {
|
||||
await expect(
|
||||
bump({ pattern: null, release: '1.0.0' } as unknown as Command),
|
||||
).rejects.toThrow('Duplicate versions present after package bump');
|
||||
});
|
||||
expect(logs.filter(Boolean)).toEqual([
|
||||
'Using default pattern glob @backstage/*',
|
||||
'Checking for updates of @backstage/core',
|
||||
'Checking for updates of @backstage/theme',
|
||||
'Package info not found, ignoring package @backstage/theme',
|
||||
'Checking for updates of @backstage/core',
|
||||
'Checking for updates of @backstage/theme',
|
||||
'Checking for updates of @backstage/core-api',
|
||||
'Package info not found, ignoring package @backstage/theme',
|
||||
'Some packages are outdated, updating',
|
||||
'bumping @backstage/core in a to ^5.0.0',
|
||||
'bumping @backstage/core in b to ^5.0.0',
|
||||
'Creating backstage.json',
|
||||
'Running yarn install to install new versions',
|
||||
'⚠️ The following packages may have breaking changes:',
|
||||
' @backstage/core : 1.0.3 ~> 5.0.0',
|
||||
' https://github.com/backstage/backstage/blob/master/packages/core/CHANGELOG.md',
|
||||
'Version bump complete!',
|
||||
]);
|
||||
|
||||
expect(runObj.run).toHaveBeenCalledTimes(1);
|
||||
expect(runObj.run).toHaveBeenCalledWith('yarn', ['install']);
|
||||
|
||||
const packageA = await fs.readJson('/packages/a/package.json');
|
||||
expect(packageA).toEqual({
|
||||
name: 'a',
|
||||
dependencies: {
|
||||
'@backstage/core': '^5.0.0',
|
||||
},
|
||||
});
|
||||
const packageB = await fs.readJson('/packages/b/package.json');
|
||||
expect(packageB).toEqual({
|
||||
name: 'b',
|
||||
dependencies: {
|
||||
'@backstage/core': '^5.0.0',
|
||||
'@backstage/theme': '^1.0.0',
|
||||
},
|
||||
});
|
||||
expect(await fs.readJson('/backstage.json')).toEqual({ version: '2.0.0' });
|
||||
});
|
||||
|
||||
it('should bump backstage dependencies and dependencies matching pattern glob', async () => {
|
||||
const customLockfileMock = `${lockfileMock}
|
||||
"@backstage-extra/custom@^1.1.0":
|
||||
@@ -246,9 +485,23 @@ describe('bump', () => {
|
||||
}),
|
||||
);
|
||||
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
|
||||
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://versions.backstage.io/v1/tags/main/manifest.json',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
packages: [],
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
const { log: logs } = await withLogCollector(['log'], async () => {
|
||||
await bump({ pattern: '@{backstage,backstage-extra}/*' } as any);
|
||||
await bump({
|
||||
pattern: '@{backstage,backstage-extra}/*',
|
||||
release: 'main',
|
||||
} as any);
|
||||
});
|
||||
expect(logs.filter(Boolean)).toEqual([
|
||||
'Using custom pattern glob @{backstage,backstage-extra}/*',
|
||||
@@ -265,6 +518,7 @@ describe('bump', () => {
|
||||
'bumping @backstage-extra/custom-two in a to ^2.0.0',
|
||||
'bumping @backstage-extra/custom-two in b to ^2.0.0',
|
||||
'bumping @backstage/theme in b to ^2.0.0',
|
||||
'Skipping backstage.json update as custom pattern is used',
|
||||
'Running yarn install to install new versions',
|
||||
'⚠️ The following packages may have breaking changes:',
|
||||
' @backstage-extra/custom-two : 1.0.0 ~> 2.0.0',
|
||||
@@ -273,7 +527,7 @@ describe('bump', () => {
|
||||
'Version bump complete!',
|
||||
]);
|
||||
|
||||
expect(runObj.runPlain).toHaveBeenCalledTimes(6);
|
||||
expect(runObj.runPlain).toHaveBeenCalledTimes(5);
|
||||
expect(runObj.runPlain).toHaveBeenCalledWith(
|
||||
'yarn',
|
||||
'info',
|
||||
@@ -342,9 +596,20 @@ describe('bump', () => {
|
||||
.mockImplementation((...path) => resolvePath('/', ...path));
|
||||
jest.spyOn(runObj, 'runPlain').mockImplementation(async () => '');
|
||||
jest.spyOn(runObj, 'run').mockResolvedValue(undefined);
|
||||
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://versions.backstage.io/v1/tags/main/manifest.json',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
packages: [],
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
const { log: logs } = await withLogCollector(['log'], async () => {
|
||||
await bump({ pattern: null } as unknown as Command);
|
||||
await bump({ pattern: null, release: 'main' } as unknown as Command);
|
||||
});
|
||||
expect(logs.filter(Boolean)).toEqual([
|
||||
'Using default pattern glob @backstage/*',
|
||||
@@ -393,27 +658,12 @@ describe('bumpBackstageJsonVersion', () => {
|
||||
'/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' });
|
||||
await bumpBackstageJsonVersion('1.4.1');
|
||||
expect(await fs.readJson('/backstage.json')).toEqual({ version: '1.4.1' });
|
||||
});
|
||||
|
||||
it("should create backstage.json if doesn't exist", async () => {
|
||||
@@ -423,22 +673,100 @@ describe('bumpBackstageJsonVersion', () => {
|
||||
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' });
|
||||
await bumpBackstageJsonVersion(latest);
|
||||
expect(await fs.readJson('/backstage.json')).toEqual({ version: latest });
|
||||
});
|
||||
});
|
||||
|
||||
describe('createVersionFinder', () => {
|
||||
async function findVersion(tag: string, data: Partial<YarnInfoInspectData>) {
|
||||
const fetcher = () =>
|
||||
Promise.resolve({
|
||||
name: '@backstage/core',
|
||||
'dist-tags': {},
|
||||
versions: [],
|
||||
time: {},
|
||||
...data,
|
||||
});
|
||||
|
||||
const versionFinder = createVersionFinder({
|
||||
releaseLine: tag,
|
||||
packageInfoFetcher: fetcher,
|
||||
});
|
||||
let result;
|
||||
await withLogCollector(async () => {
|
||||
result = await versionFinder('@backstage/core');
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
it('should create version finder', async () => {
|
||||
await expect(
|
||||
findVersion('latest', {
|
||||
time: { '1.0.0': '2020-01-01T00:00:00.000Z' },
|
||||
'dist-tags': { latest: '1.0.0' },
|
||||
}),
|
||||
).resolves.toBe('1.0.0');
|
||||
|
||||
await expect(
|
||||
findVersion('main', {
|
||||
time: { '1.0.0': '2020-01-01T00:00:00.000Z' },
|
||||
'dist-tags': { latest: '1.0.0' },
|
||||
}),
|
||||
).resolves.toBe('1.0.0');
|
||||
|
||||
await expect(
|
||||
findVersion('next', {
|
||||
time: { '1.0.0': '2020-01-01T00:00:00.000Z' },
|
||||
'dist-tags': { latest: '1.0.0' },
|
||||
}),
|
||||
).resolves.toBe('1.0.0');
|
||||
|
||||
await expect(
|
||||
findVersion('next', {
|
||||
time: {
|
||||
'1.0.0': '2020-01-01T00:00:00.000Z',
|
||||
'0.9.0': '2010-01-01T00:00:00.000Z',
|
||||
},
|
||||
'dist-tags': { latest: '1.0.0', next: '0.9.0' },
|
||||
}),
|
||||
).resolves.toBe('1.0.0');
|
||||
|
||||
await expect(
|
||||
findVersion('next', {
|
||||
time: {
|
||||
'1.0.0': '2020-01-01T00:00:00.000Z',
|
||||
'0.9.0': '2020-02-01T00:00:00.000Z',
|
||||
},
|
||||
'dist-tags': { latest: '1.0.0', next: '0.9.0' },
|
||||
}),
|
||||
).resolves.toBe('0.9.0');
|
||||
|
||||
await expect(findVersion('next', {})).rejects.toThrow(
|
||||
"No target 'latest' version found for @backstage/core",
|
||||
);
|
||||
|
||||
await expect(
|
||||
findVersion('next', {
|
||||
time: {
|
||||
'0.9.0': '2020-02-01T00:00:00.000Z',
|
||||
},
|
||||
'dist-tags': { latest: '1.0.0', next: '0.9.0' },
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"No time available for version '1.0.0' of @backstage/core",
|
||||
);
|
||||
|
||||
await expect(
|
||||
findVersion('next', {
|
||||
time: {
|
||||
'1.0.0': '2020-01-01T00:00:00.000Z',
|
||||
},
|
||||
'dist-tags': { latest: '1.0.0', next: '0.9.0' },
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"No time available for version '0.9.0' of @backstage/core",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ import chalk from 'chalk';
|
||||
import semver from 'semver';
|
||||
import minimatch from 'minimatch';
|
||||
import { Command } from 'commander';
|
||||
import { isError } from '@backstage/errors';
|
||||
import { isError, NotFoundError } from '@backstage/errors';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { run } from '../../lib/run';
|
||||
import { paths } from '../../lib/paths';
|
||||
@@ -27,10 +27,16 @@ import {
|
||||
mapDependencies,
|
||||
fetchPackageInfo,
|
||||
Lockfile,
|
||||
YarnInfoInspectData,
|
||||
} from '../../lib/versioning';
|
||||
import { forbiddenDuplicatesFilter } from './lint';
|
||||
import { BACKSTAGE_JSON } from '@backstage/cli-common';
|
||||
import { runParallelWorkers } from '../../lib/parallel';
|
||||
import {
|
||||
getManifestByReleaseLine,
|
||||
getManifestByVersion,
|
||||
ReleaseManifest,
|
||||
} from '@backstage/release-manifests';
|
||||
|
||||
const DEP_TYPES = [
|
||||
'dependencies',
|
||||
@@ -60,7 +66,22 @@ export default async (cmd: Command) => {
|
||||
console.log(`Using custom pattern glob ${pattern}`);
|
||||
}
|
||||
|
||||
const findTargetVersion = createVersionFinder();
|
||||
let findTargetVersion: (name: string) => Promise<string>;
|
||||
let releaseManifest: ReleaseManifest;
|
||||
if (semver.valid(cmd.release)) {
|
||||
releaseManifest = await getManifestByVersion({ version: cmd.release });
|
||||
findTargetVersion = createStrictVersionFinder({
|
||||
releaseManifest,
|
||||
});
|
||||
} else {
|
||||
releaseManifest = await getManifestByReleaseLine({
|
||||
releaseLine: cmd.release,
|
||||
});
|
||||
findTargetVersion = createVersionFinder({
|
||||
releaseLine: cmd.releaseLine,
|
||||
releaseManifest,
|
||||
});
|
||||
}
|
||||
|
||||
// First we discover all Backstage dependencies within our own repo
|
||||
const dependencyMap = await mapDependencies(paths.targetDir, pattern);
|
||||
@@ -213,8 +234,16 @@ export default async (cmd: Command) => {
|
||||
|
||||
console.log();
|
||||
|
||||
await bumpBackstageJsonVersion();
|
||||
|
||||
// Do not update backstage.json when upgrade patterns are used.
|
||||
if (pattern === DEFAULT_PATTERN_GLOB) {
|
||||
await bumpBackstageJsonVersion(releaseManifest.releaseVersion);
|
||||
} else {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`Skipping backstage.json update as custom pattern is used`,
|
||||
),
|
||||
);
|
||||
}
|
||||
console.log();
|
||||
console.log(
|
||||
`Running ${chalk.blue('yarn install')} to install new versions`,
|
||||
@@ -284,9 +313,38 @@ export default async (cmd: Command) => {
|
||||
}
|
||||
};
|
||||
|
||||
function createVersionFinder() {
|
||||
const found = new Map<string, string>();
|
||||
export function createStrictVersionFinder(options: {
|
||||
releaseManifest: ReleaseManifest;
|
||||
}) {
|
||||
const releasePackages = new Map(
|
||||
options.releaseManifest.packages.map(p => [p.name, p.version]),
|
||||
);
|
||||
return async function findTargetVersion(name: string) {
|
||||
console.log(`Checking for updates of ${name}`);
|
||||
const manifestVersion = releasePackages.get(name);
|
||||
if (manifestVersion) {
|
||||
return manifestVersion;
|
||||
}
|
||||
throw new NotFoundError(`Package ${name} not found in release manifest`);
|
||||
};
|
||||
}
|
||||
|
||||
export function createVersionFinder(options: {
|
||||
releaseLine?: string;
|
||||
packageInfoFetcher?: () => Promise<YarnInfoInspectData>;
|
||||
releaseManifest?: ReleaseManifest;
|
||||
}) {
|
||||
const {
|
||||
releaseLine = 'latest',
|
||||
packageInfoFetcher = fetchPackageInfo,
|
||||
releaseManifest,
|
||||
} = options;
|
||||
// The main release line is just an alias for latest
|
||||
const distTag = releaseLine === 'main' ? 'latest' : releaseLine;
|
||||
const found = new Map<string, string>();
|
||||
const releasePackages = new Map(
|
||||
releaseManifest?.packages.map(p => [p.name, p.version]),
|
||||
);
|
||||
return async function findTargetVersion(name: string) {
|
||||
const existing = found.get(name);
|
||||
if (existing) {
|
||||
@@ -294,17 +352,50 @@ function createVersionFinder() {
|
||||
}
|
||||
|
||||
console.log(`Checking for updates of ${name}`);
|
||||
const info = await fetchPackageInfo(name);
|
||||
const latest = info['dist-tags'].latest;
|
||||
if (!latest) {
|
||||
throw new Error(`No latest version found for ${name}`);
|
||||
const manifestVersion = releasePackages.get(name);
|
||||
if (manifestVersion) {
|
||||
return manifestVersion;
|
||||
}
|
||||
found.set(name, latest);
|
||||
return latest;
|
||||
|
||||
const info = await packageInfoFetcher(name);
|
||||
const latestVersion = info['dist-tags'].latest;
|
||||
if (!latestVersion) {
|
||||
throw new Error(`No target 'latest' version found for ${name}`);
|
||||
}
|
||||
|
||||
const taggedVersion = info['dist-tags'][distTag];
|
||||
if (distTag === 'latest' || !taggedVersion) {
|
||||
found.set(name, latestVersion);
|
||||
return latestVersion;
|
||||
}
|
||||
|
||||
const latestVersionDateStr = info.time[latestVersion];
|
||||
const taggedVersionDateStr = info.time[taggedVersion];
|
||||
if (!latestVersionDateStr) {
|
||||
throw new Error(
|
||||
`No time available for version '${latestVersion}' of ${name}`,
|
||||
);
|
||||
}
|
||||
if (!taggedVersionDateStr) {
|
||||
throw new Error(
|
||||
`No time available for version '${taggedVersion}' of ${name}`,
|
||||
);
|
||||
}
|
||||
|
||||
const latestVersionRelease = new Date(latestVersionDateStr).getTime();
|
||||
const taggedVersionRelease = new Date(taggedVersionDateStr).getTime();
|
||||
if (latestVersionRelease > taggedVersionRelease) {
|
||||
// Prefer latest version if it's newer.
|
||||
found.set(name, latestVersion);
|
||||
return latestVersion;
|
||||
}
|
||||
|
||||
found.set(name, taggedVersion);
|
||||
return taggedVersion;
|
||||
};
|
||||
}
|
||||
|
||||
export async function bumpBackstageJsonVersion() {
|
||||
export async function bumpBackstageJsonVersion(version: string) {
|
||||
const backstageJsonPath = paths.resolveTargetRoot(BACKSTAGE_JSON);
|
||||
const backstageJson = await fs.readJSON(backstageJsonPath).catch(e => {
|
||||
if (e.code === 'ENOENT') {
|
||||
@@ -314,10 +405,7 @@ export async function bumpBackstageJsonVersion() {
|
||||
throw e;
|
||||
});
|
||||
|
||||
const info = await fetchPackageInfo('@backstage/create-app');
|
||||
const { latest } = info['dist-tags'];
|
||||
|
||||
if (backstageJson?.version === latest) {
|
||||
if (backstageJson?.version === version) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -331,7 +419,7 @@ export async function bumpBackstageJsonVersion() {
|
||||
|
||||
await fs.writeJson(
|
||||
backstageJsonPath,
|
||||
{ ...backstageJson, version: latest },
|
||||
{ ...backstageJson, version },
|
||||
{
|
||||
spaces: 2,
|
||||
encoding: 'utf8',
|
||||
|
||||
@@ -28,7 +28,7 @@ const DEP_TYPES = [
|
||||
// Package data as returned by `yarn info`
|
||||
export type YarnInfoInspectData = {
|
||||
name: string;
|
||||
'dist-tags': { latest: string };
|
||||
'dist-tags': Record<string, string>;
|
||||
versions: string[];
|
||||
time: { [version: string]: string };
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
# @backstage/release-manifests
|
||||
@@ -0,0 +1,3 @@
|
||||
# @backstage/release-manifests
|
||||
|
||||
This package provides a mapping between a Backstage release and the packages included in that release.
|
||||
@@ -0,0 +1,34 @@
|
||||
## API Report File for "@backstage/release-manifests"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
// @public
|
||||
export function getManifestByReleaseLine(
|
||||
options: GetManifestByReleaseLineOptions,
|
||||
): Promise<ReleaseManifest>;
|
||||
|
||||
// @public
|
||||
export type GetManifestByReleaseLineOptions = {
|
||||
releaseLine: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function getManifestByVersion(
|
||||
options: GetManifestByVersionOptions,
|
||||
): Promise<ReleaseManifest>;
|
||||
|
||||
// @public
|
||||
export type GetManifestByVersionOptions = {
|
||||
version: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type ReleaseManifest = {
|
||||
releaseVersion: string;
|
||||
packages: {
|
||||
name: string;
|
||||
version: string;
|
||||
}[];
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "@backstage/release-manifests",
|
||||
"description": "Helper library for receiving release manifests",
|
||||
"version": "0.0.0",
|
||||
"private": false,
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "packages/release-manifests"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"build": "backstage-cli build",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"cross-fetch": "^3.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/test-utils": "^0.2.3",
|
||||
"msw": "^0.35.0",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^14.14.32"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Contains mapping between Backstage release and package versions.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export { getManifestByVersion, getManifestByReleaseLine } from './manifest';
|
||||
export type {
|
||||
ReleaseManifest,
|
||||
GetManifestByReleaseLineOptions,
|
||||
GetManifestByVersionOptions,
|
||||
} from './manifest';
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { setupRequestMockHandlers } from '@backstage/test-utils';
|
||||
import { getManifestByReleaseLine, getManifestByVersion } from './manifest';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { rest } from 'msw';
|
||||
|
||||
describe('getManifestByVersion', () => {
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
|
||||
it('should return a list of packages in a release', async () => {
|
||||
worker.use(
|
||||
rest.get('*/v1/releases/0.0.0/manifest.json', (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
packages: [{ name: '@backstage/core', version: '1.2.3' }],
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.get('*/v1/releases/999.0.1/manifest.json', (_, res, ctx) =>
|
||||
res(ctx.status(404), ctx.json({})),
|
||||
),
|
||||
);
|
||||
|
||||
const pkgs = await getManifestByVersion({ version: '0.0.0' });
|
||||
expect(pkgs.packages).toEqual([
|
||||
{
|
||||
name: '@backstage/core',
|
||||
version: '1.2.3',
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(getManifestByVersion({ version: '999.0.1' })).rejects.toThrow(
|
||||
'No release found for 999.0.1 version',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getManifestByReleaseLine', () => {
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
|
||||
it('should return a list of packages in a release', async () => {
|
||||
worker.use(
|
||||
rest.get('*/v1/tags/main/manifest.json', (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
packages: [{ name: '@backstage/core', version: '1.2.3' }],
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.get('*/v1/tags/foo/manifest.json', (_, res, ctx) =>
|
||||
res(ctx.status(404), ctx.json({})),
|
||||
),
|
||||
);
|
||||
|
||||
const pkgs = await getManifestByReleaseLine({ releaseLine: 'main' });
|
||||
expect(pkgs.packages).toEqual([
|
||||
{
|
||||
name: '@backstage/core',
|
||||
version: '1.2.3',
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(
|
||||
getManifestByReleaseLine({ releaseLine: 'foo' }),
|
||||
).rejects.toThrow("No 'foo' release line found");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fetch from 'cross-fetch';
|
||||
|
||||
const VERSIONS_DOMAIN = 'https://versions.backstage.io';
|
||||
|
||||
/**
|
||||
* Contains mapping between Backstage release and package versions.
|
||||
* @public
|
||||
*/
|
||||
export type ReleaseManifest = {
|
||||
releaseVersion: string;
|
||||
packages: { name: string; version: string }[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for {@link getManifestByVersion}.
|
||||
* @public
|
||||
*/
|
||||
export type GetManifestByVersionOptions = {
|
||||
version: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a release manifest based on supplied version.
|
||||
* @public
|
||||
*/
|
||||
export async function getManifestByVersion(
|
||||
options: GetManifestByVersionOptions,
|
||||
): Promise<ReleaseManifest> {
|
||||
const url = `${VERSIONS_DOMAIN}/v1/releases/${encodeURIComponent(
|
||||
options.version,
|
||||
)}/manifest.json`;
|
||||
const response = await fetch(url);
|
||||
if (response.status === 404) {
|
||||
throw new Error(`No release found for ${options.version} version`);
|
||||
}
|
||||
if (response.status !== 200) {
|
||||
throw new Error(
|
||||
`Unexpected response status ${response.status} when fetching release from ${url}.`,
|
||||
);
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for {@link getManifestByReleaseLine}.
|
||||
* @public
|
||||
*/
|
||||
export type GetManifestByReleaseLineOptions = {
|
||||
releaseLine: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a release manifest based on supplied release line.
|
||||
* @public
|
||||
*/
|
||||
export async function getManifestByReleaseLine(
|
||||
options: GetManifestByReleaseLineOptions,
|
||||
): Promise<ReleaseManifest> {
|
||||
const url = `${VERSIONS_DOMAIN}/v1/tags/${encodeURIComponent(
|
||||
options.releaseLine,
|
||||
)}/manifest.json`;
|
||||
const response = await fetch(url);
|
||||
if (response.status === 404) {
|
||||
throw new Error(`No '${options.releaseLine}' release line found`);
|
||||
}
|
||||
if (response.status !== 200) {
|
||||
throw new Error(
|
||||
`Unexpected response status ${response.status} when fetching release from ${url}.`,
|
||||
);
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
@@ -216,6 +216,7 @@ const NO_WARNING_PACKAGES = [
|
||||
'packages/test-utils',
|
||||
'packages/theme',
|
||||
'packages/types',
|
||||
'packages/release-manifests',
|
||||
'packages/version-bridge',
|
||||
'plugins/catalog-backend-module-ldap',
|
||||
'plugins/catalog-backend-module-msgraph',
|
||||
|
||||
Reference in New Issue
Block a user