Add --ci flag to generate-catalog-info command

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2023-08-17 17:30:34 +02:00
parent 34f6a8cc28
commit db60a16e0a
5 changed files with 107 additions and 21 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/repo-tools': patch
---
Added a `--ci` flag to the `generate-catalog-info` command. This flag behaves similarly to the same flag on `api-reports`: if `catalog-info.yaml` files would have been added or modified, then the process exits with status code `1`, and instructions are printed.
+1
View File
@@ -44,6 +44,7 @@ Usage: backstage-repo-tools generate-catalog-info [options]
Options:
--dry-run
--ci
-h, --help
```
@@ -29,26 +29,34 @@ import {
import {
BackstagePackageJson,
isBackstagePackage,
isRejected,
isFulfilled,
readFile,
writeFile,
} from './utils';
import { CodeOwnersEntry } from 'codeowners-utils';
type CreateFixPackageInfoYamlsOptions = {
ci?: boolean;
dryRun?: boolean;
};
export default async (opts: CreateFixPackageInfoYamlsOptions) => {
const { dryRun = false } = opts;
const { dryRun = false, ci = false } = opts;
const packages = await PackageGraph.listTargetPackages();
const codeowners = await loadCodeowners();
const limit = pLimit(10);
const results = await Promise.allSettled<void>(
// If --ci is passed, don't make any changes to the file system; we're only
// interested in knowing if changes would have been made.
const isDryRun = ci ? true : dryRun;
const checkForChanges = ci;
const results = await Promise.allSettled<string>(
packages.map(({ packageJson, dir }) =>
limit(async () => {
if (!isBackstagePackage(packageJson)) {
return;
return '';
}
// Check if there is already a corresponding catalog-info.yaml
@@ -59,32 +67,30 @@ export default async (opts: CreateFixPackageInfoYamlsOptions) => {
yamlString = await readFile(infoYamlPath, { encoding: 'utf-8' });
} catch (e) {
if (e.code === 'ENOENT') {
await createCatalogInfoYaml({
return await createCatalogInfoYaml({
yamlPath: infoYamlPath,
packageJson,
codeowners,
dryRun,
dryRun: isDryRun,
});
return;
}
throw e;
}
await fixCatalogInfoYaml({
return await fixCatalogInfoYaml({
yamlPath: infoYamlPath,
packageJson,
codeowners,
yamlString,
dryRun,
dryRun: isDryRun,
ci,
});
}),
),
);
const rejects = results.filter(
r => r.status === 'rejected',
) as PromiseRejectedResult[];
const rejects = results.filter(isRejected);
if (rejects.length > 0) {
// Problems encountered. Print details here.
console.error(
@@ -94,6 +100,25 @@ export default async (opts: CreateFixPackageInfoYamlsOptions) => {
console.error();
process.exit(1);
}
if (checkForChanges) {
const instructions = results
.filter(isFulfilled)
.map(r => r.value)
.filter(r => r !== '');
// Non-empty instructions indicate changes would have been made.
if (instructions.length > 0) {
console.error(
'\ncatalog-info.yaml file(s) out of sync with CODEOWNERS and/or package.json (see instructions above)\n',
);
process.exit(1);
} else {
console.error(
'catalog-info.yaml file(s) in sync with CODEOWNERS and package.json',
);
}
}
};
type CreateOptions = {
@@ -105,6 +130,7 @@ type CreateOptions = {
type FixOptions = CreateOptions & {
yamlString: string;
ci: boolean;
};
type BackstagePackageEntity = Entity & {
@@ -117,18 +143,23 @@ type BackstagePackageEntity = Entity & {
};
};
function createCatalogInfoYaml(options: CreateOptions) {
async function createCatalogInfoYaml(options: CreateOptions) {
const { codeowners, dryRun, packageJson, yamlPath } = options;
const instruction = `Create ${relativePath('.', yamlPath)}`;
const owner = getOwnerFromCodeowners(codeowners, yamlPath);
const entity = createOrMergeEntity(packageJson, owner);
return dryRun
? Promise.resolve(console.error(`Create ${relativePath('.', yamlPath)}`))
: writeFile(yamlPath, YAML.dump(entity));
if (dryRun) {
console.error(instruction);
} else {
await writeFile(yamlPath, YAML.dump(entity));
}
return instruction;
}
function fixCatalogInfoYaml(options: FixOptions) {
const { codeowners, dryRun, packageJson, yamlPath, yamlString } = options;
async function fixCatalogInfoYaml(options: FixOptions) {
const { ci, codeowners, dryRun, packageJson, yamlPath, yamlString } = options;
const possibleOwners = getPossibleCodeowners(
codeowners,
relativePath('.', yamlPath),
@@ -156,12 +187,49 @@ function fixCatalogInfoYaml(options: FixOptions) {
? getOwnerFromCodeowners(codeowners, yamlPath)
: yamlJson.spec?.owner;
const newJson = createOrMergeEntity(packageJson, owner, yamlJson);
return dryRun
? Promise.resolve(console.error(`Update ${relativePath('.', yamlPath)}`))
: writeFile(yamlPath, yamlOverwrite(yamlString, newJson));
const instructions = [`Update ${relativePath('.', yamlPath)}`];
// Show more detailed instructions when --ci flag is set.
if (ci) {
if (badOwner) {
instructions.push(
` spec.owner cannot be "${
yamlJson.spec.owner
}" because it must be one of (${possibleOwners.join(
', ',
)}) as listed in CODEOWNERS`,
);
}
if (badTitle) {
instructions.push(
` metadata.title cannot be "${yamlJson.metadata.title}" because it must be exactly "${packageJson.name}", the package.json name`,
);
}
if (badName) {
instructions.push(
` metadata.name cannot be "${yamlJson.metadata.name}" because it must be exactly "${safeName}", as derived from package.json name`,
);
}
if (badType) {
instructions.push(
` spec.type cannot be "${yamlJson.spec.type}" because it must be exactly "backstage-${packageJson.backstage.role}", as derived from package.json backstage.role`,
);
}
}
if (dryRun) {
console.error(instructions.join('\n'));
} else {
await writeFile(yamlPath, yamlOverwrite(yamlString, newJson));
}
return instructions.join('\n');
}
return Promise.resolve();
return '';
}
/**
@@ -40,3 +40,11 @@ export function isBackstagePackage(
(packageJson as any)?.backstage?.role !== 'undefined'
);
}
export const isRejected = (
input: PromiseSettledResult<unknown>,
): input is PromiseRejectedResult => input.status === 'rejected';
export const isFulfilled = <T>(
input: PromiseSettledResult<T>,
): input is PromiseFulfilledResult<T> => input.status === 'fulfilled';
@@ -102,6 +102,10 @@ export function registerCommands(program: Command) {
'--dry-run',
'Shows what would happen without actually writing any yaml.',
)
.option(
'--ci',
'CI run checks that there are no changes to catalog-info.yaml files',
)
.description('Create or fix info yaml files for all backstage packages')
.action(
lazy(() =>