[cli] Add CODEOWNERS support to create-plugin

This commit is contained in:
Fredrik Adelöw
2020-03-20 13:19:24 +01:00
parent 39dc09fdd7
commit a14584b485
5 changed files with 170 additions and 1 deletions
@@ -25,6 +25,7 @@ import { resolve as resolvePath } from 'path';
import { realpathSync, existsSync } from 'fs';
import os from 'os';
import ora from 'ora';
import { parseOwnerIds, addCodeownersEntry } from './lib/codeowners';
const MARKER_SUCCESS = chalk.green(` ✔︎\n`);
const MARKER_FAILURE = chalk.red(`\n`);
@@ -343,16 +344,39 @@ const createPlugin = async () => {
return true;
},
},
{
type: 'input',
name: 'owner',
message: chalk.blue(
'Enter the owner(s) of the plugin. If specified, this will be added to CODEOWNERS for the plugin path. [optional]',
),
validate: (value: any) => {
if (!value) {
return true;
}
const ownerIds = parseOwnerIds(value);
if (!ownerIds) {
return chalk.red(
'The owner must be a space separated list of team names (e.g. @org/team-name), usernames (e.g. @username), or the email addresses of users (e.g. user@example.com).',
);
}
return true;
},
},
];
const answers: Answers = await inquirer.prompt(questions);
const rootDir = realpathSync(process.cwd());
const codeownersPath = path.join(rootDir, '.github', 'CODEOWNERS');
const appPackage = resolvePath(rootDir, 'packages', 'app');
const cliPackage = resolvePath(__dirname, '..', '..');
const templateFolder = resolvePath(cliPackage, 'templates', 'default-plugin');
const tempDir = path.join(os.tmpdir(), answers.id);
const pluginDir = path.join(rootDir, 'plugins', answers.id);
const version = require(resolvePath(cliPackage, 'package.json')).version;
const ownerIds = parseOwnerIds(answers.owner);
console.log();
console.log(chalk.green('Creating the plugin...'));
@@ -369,6 +393,14 @@ const createPlugin = async () => {
addPluginToApp(rootDir, answers.id);
}
if (ownerIds && ownerIds.length) {
addCodeownersEntry(
codeownersPath,
path.join('plugins', answers.id),
ownerIds,
);
}
console.log();
console.log(
chalk.green(
@@ -0,0 +1,49 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { isValidSingleOwnerId, parseOwnerIds } from './codeowners';
describe('codeowners', () => {
it('isValidSingleOwnerId', () => {
[
'@foo',
'@a-b',
'@org-a/team-a',
'@a/b',
'adam_driver+spam@deathstar.com',
].forEach(id => {
expect(isValidSingleOwnerId(id)).toBeTruthy();
});
[
'',
'@',
'@/team-a',
'@orsdsd/',
'adam_driver@deathstar',
'something',
].forEach(id => {
expect(isValidSingleOwnerId(id)).toBeFalsy();
});
});
it('parseOwnerIds', () => {
expect(parseOwnerIds('')).toBeUndefined();
expect(parseOwnerIds('@foo')).toEqual(['@foo']);
expect(parseOwnerIds(' @foo @bar/baz ')).toEqual(['@foo', '@bar/baz']);
expect(parseOwnerIds(' @foo @bar/ ')).toBeUndefined();
});
});
@@ -0,0 +1,88 @@
/*
* Copyright 2020 Spotify AB
*
* 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 fs from 'fs-extra';
const TEAM_ID_RE = /^@[-\w]+\/[-\w]+$/;
const USER_ID_RE = /^@[-\w]+$/;
const EMAIL_RE = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
export function isValidSingleOwnerId(id: string): boolean {
if (!id || typeof id !== 'string') {
return false;
}
return TEAM_ID_RE.test(id) || USER_ID_RE.test(id) || EMAIL_RE.test(id);
}
export function parseOwnerIds(
spaceSeparatedOwnerIds: string,
): string[] | undefined {
if (!spaceSeparatedOwnerIds || typeof spaceSeparatedOwnerIds !== 'string') {
return undefined;
}
const ids = spaceSeparatedOwnerIds.split(' ').filter(Boolean);
if (!ids.every(isValidSingleOwnerId)) {
return undefined;
}
return ids;
}
export function addCodeownersEntry(
codeownersFilePath: string,
ownedPath: string,
ownerIds: string[],
): void {
const allLines = fs.readFileSync(codeownersFilePath, 'utf8').split('\n');
// Only keep comments from the top of the file
const commentLines = [];
for (const line of allLines) {
if (line[0] !== '#') {
break;
}
commentLines.push(line);
}
const oldDeclarationEntries = allLines
.filter(line => line[0] !== '#')
.map(line => line.split(/\s+/).filter(Boolean))
.filter(items => items.length >= 2);
const newDeclarationEntries = oldDeclarationEntries
.filter(items => items[0] !== '*')
.concat([[ownedPath, ...ownerIds]])
.sort((l1, l2) => l1[0].localeCompare(l2[0]))
.concat([['*', '@spotify/backstage-core']]);
// Calculate longest path to be able to align entries nicely
const longestOwnedPath = newDeclarationEntries.reduce(
(length, entry) => Math.max(length, entry[0].length),
0,
);
const newDeclarationLines = newDeclarationEntries.map(entry => {
const entryPath = entry[0] + ' '.repeat(longestOwnedPath - entry[0].length);
const entryOwners = entry.slice(1);
return [entryPath, ...entryOwners].join(' ');
});
const newLines = [...commentLines, '', ...newDeclarationLines, ''];
fs.writeFileSync(codeownersFilePath, newLines.join('\n'), 'utf8');
}
+1 -1
View File
@@ -17,7 +17,7 @@
import program from 'commander';
import chalk from 'chalk';
import fs from 'fs';
import createPluginCommand from './commands/createPlugin';
import createPluginCommand from './commands/create-plugin/createPlugin';
import watch from './commands/watch-deps';
import lintCommand from './commands/lint';
import testCommand from './commands/testCommand';