Merge pull request #340 from spotify/freben/codeowners
[cli] Add CODEOWNERS support to create-plugin
This commit is contained in:
+43
-2
@@ -25,6 +25,11 @@ import { resolve as resolvePath } from 'path';
|
||||
import { realpathSync, existsSync } from 'fs';
|
||||
import os from 'os';
|
||||
import ora from 'ora';
|
||||
import {
|
||||
parseOwnerIds,
|
||||
addCodeownersEntry,
|
||||
getCodeownersFilePath,
|
||||
} from './lib/codeowners';
|
||||
|
||||
const MARKER_SUCCESS = chalk.green(` ✔︎\n`);
|
||||
const MARKER_FAILURE = chalk.red(` ✘\n`);
|
||||
@@ -327,6 +332,9 @@ export const movePlugin = (
|
||||
};
|
||||
|
||||
const createPlugin = async () => {
|
||||
const rootDir = realpathSync(process.cwd());
|
||||
const codeownersPath = await getCodeownersFilePath(rootDir);
|
||||
|
||||
const questions: Question[] = [
|
||||
{
|
||||
type: 'input',
|
||||
@@ -344,15 +352,40 @@ const createPlugin = async () => {
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (codeownersPath) {
|
||||
questions.push({
|
||||
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 appPackage = resolvePath(rootDir, 'packages', 'app');
|
||||
const cliPackage = resolvePath(__dirname, '..', '..');
|
||||
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 +402,14 @@ const createPlugin = async () => {
|
||||
addPluginToApp(rootDir, answers.id);
|
||||
}
|
||||
|
||||
if (ownerIds && ownerIds.length) {
|
||||
await 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,117 @@
|
||||
/*
|
||||
* 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';
|
||||
import path from 'path';
|
||||
|
||||
const TEAM_ID_RE = /^@[-\w]+\/[-\w]+$/;
|
||||
const USER_ID_RE = /^@[-\w]+$/;
|
||||
const EMAIL_RE = /^[^@]+@[-.\w]+\.[-\w]+$/i;
|
||||
|
||||
type CodeownersEntry = {
|
||||
ownedPath: string;
|
||||
ownerIds: string[];
|
||||
};
|
||||
|
||||
export async function getCodeownersFilePath(
|
||||
rootDir: string,
|
||||
): Promise<string | undefined> {
|
||||
const paths = [
|
||||
path.join(rootDir, '.github', 'CODEOWNERS'),
|
||||
path.join(rootDir, '.gitlab', 'CODEOWNERS'),
|
||||
path.join(rootDir, 'docs', 'CODEOWNERS'),
|
||||
path.join(rootDir, 'CODEOWNERS'),
|
||||
];
|
||||
|
||||
for (const p of paths) {
|
||||
if (await fs.pathExists(p)) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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 async function addCodeownersEntry(
|
||||
codeownersFilePath: string,
|
||||
ownedPath: string,
|
||||
ownerIds: string[],
|
||||
): Promise<void> {
|
||||
const allLines = (await fs.readFile(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: CodeownersEntry[] = allLines
|
||||
.filter(line => line[0] !== '#')
|
||||
.map(line => line.split(/\s+/).filter(Boolean))
|
||||
.filter(tokens => tokens.length >= 2)
|
||||
.map(tokens => ({
|
||||
ownedPath: tokens[0],
|
||||
ownerIds: tokens.slice(1),
|
||||
}));
|
||||
|
||||
const newDeclarationEntries = oldDeclarationEntries
|
||||
.filter(entry => entry.ownedPath !== '*')
|
||||
.concat([{ ownedPath, ownerIds }])
|
||||
.sort((l1, l2) => l1.ownedPath.localeCompare(l2.ownedPath))
|
||||
.concat([{ ownedPath: '*', ownerIds: ['@spotify/backstage-core'] }]);
|
||||
|
||||
// Calculate longest path to be able to align entries nicely
|
||||
const longestOwnedPath = newDeclarationEntries.reduce(
|
||||
(length, entry) => Math.max(length, entry.ownedPath.length),
|
||||
0,
|
||||
);
|
||||
|
||||
const newDeclarationLines = newDeclarationEntries.map(entry => {
|
||||
const entryPath =
|
||||
entry.ownedPath + ' '.repeat(longestOwnedPath - entry.ownedPath.length);
|
||||
return [entryPath, ...entry.ownerIds].join(' ');
|
||||
});
|
||||
|
||||
const newLines = [...commentLines, '', ...newDeclarationLines, ''];
|
||||
|
||||
await fs.writeFile(codeownersFilePath, newLines.join('\n'), 'utf8');
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -37,6 +37,8 @@ async function main() {
|
||||
|
||||
const createPlugin = spawnPiped(['yarn', 'create-plugin']);
|
||||
createPlugin.stdin.write('test-plugin\n');
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
createPlugin.stdin.write('@someuser\n');
|
||||
|
||||
print('Waiting for plugin create script to be done');
|
||||
await waitForExit(createPlugin);
|
||||
|
||||
Reference in New Issue
Block a user