Skip to content

Commit 02863fe

Browse files
authored
feat(cli): create bcr entry from command (#215)
1 parent 35f5231 commit 02863fe

13 files changed

+550
-221
lines changed

src/application/cli/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ ts_project(
3535
"//:node_modules/@types/jest", # keep
3636
"//:node_modules/@types/yargs",
3737
"//:node_modules/yargs",
38+
"//src/domain",
3839
],
3940
)
4041

src/application/cli/app.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { Module } from '@nestjs/common';
22

3+
import { CreateEntryService } from '../../domain/create-entry.js';
34
import { CreateEntryCommand } from './create-entry-command.js';
45

56
@Module({
6-
providers: [CreateEntryCommand],
7+
providers: [CreateEntryCommand, CreateEntryService],
78
})
89
export class AppModule {}

src/application/cli/create-entry-command.ts

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,61 @@ import { Injectable } from '@nestjs/common';
22
import path from 'path';
33
import { ArgumentsCamelCase } from 'yargs';
44

5+
import { CreateEntryService } from '../../domain/create-entry.js';
6+
import { MetadataFile } from '../../domain/metadata-file.js';
7+
import { Repository } from '../../domain/repository.js';
58
import {
6-
Configuration,
7-
MissingConfigurationFileError,
8-
} from '../../domain/configuration.js';
9+
SourceTemplate,
10+
SubstitutableVar,
11+
} from '../../domain/source-template.js';
912
import { CreateEntryArgs } from './yargs.js';
1013

1114
@Injectable()
1215
export class CreateEntryCommand {
13-
public async handle(_args: ArgumentsCamelCase<CreateEntryArgs>) {
14-
this.loadConfiguration(_args.templatesDir);
16+
constructor(private readonly createEntryService: CreateEntryService) {}
17+
18+
public async handle(args: ArgumentsCamelCase<CreateEntryArgs>) {
19+
const metadataTemplate = new MetadataFile(
20+
path.join(args.templatesDir, 'metadata.template.json')
21+
);
22+
const sourceTemplate = new SourceTemplate(
23+
path.join(args.templatesDir, 'source.template.json')
24+
);
25+
const presubmitPath = path.join(args.templatesDir, 'presubmit.yml');
26+
const patchesPath = path.join(args.templatesDir, 'patches');
27+
28+
sourceTemplate.substitute({
29+
...ghRepoSubstitutions(args.githubRepository),
30+
...(args.tag ? { TAG: args.tag } : {}),
31+
VERSION: args.moduleVersion,
32+
});
33+
34+
const { moduleName } = await this.createEntryService.createEntryFiles(
35+
metadataTemplate,
36+
sourceTemplate,
37+
presubmitPath,
38+
patchesPath,
39+
args.localRegistry,
40+
args.moduleVersion
41+
);
42+
43+
console.error(
44+
`Created entry for ${moduleName}@${args.moduleVersion} at ${args.localRegistry}`
45+
);
1546

1647
return Promise.resolve(null);
1748
}
49+
}
1850

19-
private loadConfiguration(templatesDir: string): Configuration {
20-
const filepaths = [
21-
path.join(templatesDir, 'config.yml'),
22-
path.join(templatesDir, 'config.yaml'),
23-
];
24-
for (const filepath of filepaths) {
25-
try {
26-
return Configuration.fromFile(filepath);
27-
} catch (e) {
28-
if (e instanceof MissingConfigurationFileError) {
29-
continue;
30-
}
31-
throw e;
32-
}
33-
}
34-
35-
// No configuration files at the expected paths. Load the defaults.
36-
console.error('No configuration file found; using defaults');
37-
return Configuration.defaults();
51+
function ghRepoSubstitutions(
52+
githubRepository?: string
53+
): Partial<Record<SubstitutableVar, string>> {
54+
if (githubRepository) {
55+
const repo = Repository.fromCanonicalName(githubRepository);
56+
return {
57+
OWNER: repo.owner,
58+
REPO: repo.name,
59+
};
3860
}
61+
return {};
3962
}

src/application/cli/yargs.spec.ts

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import { Argv } from 'yargs';
22

3+
import { CreateEntryService } from '../../domain/create-entry.js';
34
import { CreateEntryCommand } from './create-entry-command.js';
45
import { ApplicationArgs, createParser } from './yargs.js';
56

67
jest.mock('./create-entry-command.js');
8+
jest.mock('../../domain/create-entry.js');
79

810
describe('createParser', () => {
911
let parser: Argv<ApplicationArgs>;
1012

1113
beforeEach(() => {
12-
parser = createParser(new CreateEntryCommand());
14+
parser = createParser(new CreateEntryCommand(new CreateEntryService()));
1315
});
1416

1517
test('displays --help', async () => {
@@ -31,20 +33,102 @@ describe('createParser', () => {
3133

3234
describe('create-entry', () => {
3335
test('missing --templates-dir', async () => {
34-
expect(() => parser.parse('create-entry')).toThrow(
35-
'Missing required argument: templates-dir'
36-
);
36+
expect(() =>
37+
parser.parse(
38+
'create-entry --local-registry /path/to/bcr --module-version 1.0.0'
39+
)
40+
).toThrow('Missing required argument: templates-dir');
3741
});
3842

3943
test('missing --templates-dir arg', () => {
40-
expect(() => parser.parse('create-entry --templates-dir')).toThrow(
41-
'Not enough arguments following: templates-dir'
42-
);
44+
expect(() =>
45+
parser.parse(
46+
'create-entry --local-registry /path/to/bcr --module-version 1.0.0 --templates-dir'
47+
)
48+
).toThrow('Not enough arguments following: templates-dir');
4349
});
4450

4551
test('parses --templates-dir', async () => {
46-
const args = await parser.parse('create-entry --templates-dir .bcr');
52+
const args = await parser.parse(
53+
'create-entry --templates-dir .bcr --local-registry /path/to/bcr --module-version 1.0.0'
54+
);
4755
expect(args.templatesDir).toEqual('.bcr');
4856
});
57+
58+
test('missing --module-version', () => {
59+
expect(() =>
60+
parser.parse(
61+
'create-entry --templates-dir .bcr --local-registry /path/to/bcr'
62+
)
63+
).toThrow('Missing required argument: module-version');
64+
});
65+
66+
test('missing --module-version arg', () => {
67+
expect(() =>
68+
parser.parse(
69+
'create-entry --templates-dir .bcr --local-registry /path/to/bcr --tag v1.0.0 --module-version'
70+
)
71+
).toThrow('Not enough arguments following: module-version');
72+
});
73+
74+
test('parses --module-version', async () => {
75+
const args = await parser.parse(
76+
'create-entry --templates-dir .bcr --local-registry /path/to/bcr --module-version 1.0.0'
77+
);
78+
expect(args.moduleVersion).toEqual('1.0.0');
79+
});
80+
81+
test('missing --tag arg', () => {
82+
expect(() =>
83+
parser.parse(
84+
'create-entry --templates-dir .bcr --local-registry /path/to/bcr --tag'
85+
)
86+
).toThrow('Not enough arguments following: tag');
87+
});
88+
89+
test('parses --tag', async () => {
90+
const args = await parser.parse(
91+
'create-entry --templates-dir .bcr --local-registry /path/to/bcr --module-version 1.0.0 --tag v1.0.0'
92+
);
93+
expect(args.tag).toEqual('v1.0.0');
94+
});
95+
96+
test('missing --local-registry', () => {
97+
expect(() =>
98+
parser.parse(
99+
'create-entry --templates-dir .bcr --tag v1.0.0 --module-version 1.0.0'
100+
)
101+
).toThrow('Missing required argument: local-registry');
102+
});
103+
104+
test('missing --local-registry arg', () => {
105+
expect(() =>
106+
parser.parse(
107+
'create-entry --templates-dir .bcr --tag v1.0.0 --module-version 1.0.0 --local-registry'
108+
)
109+
).toThrow('Not enough arguments following: local-registry');
110+
});
111+
112+
test('parses --local-registry', async () => {
113+
const args = await parser.parse(
114+
'create-entry --templates-dir .bcr --local-registry /path/to/bcr --module-version 1.0.0'
115+
);
116+
expect(args.localRegistry).toEqual('/path/to/bcr');
117+
});
118+
119+
test('missing --github-repository arg', () => {
120+
expect(() =>
121+
parser.parse(
122+
'create-entry --templates-dir .bcr --tag v1.0.0 --local-registry /path/to/bcr --github-repository'
123+
)
124+
).toThrow('Not enough arguments following: github-repository');
125+
});
126+
127+
test('parses --github-repository', async () => {
128+
const args = await parser.parse(
129+
'create-entry --templates-dir .bcr --module-version 1.0.0 --github-repository foo/bar --local-registry /path/to/bcr'
130+
);
131+
expect(args.githubRepository).toEqual('foo/bar');
132+
});
49133
});
50134
});

src/application/cli/yargs.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import { hideBin } from 'yargs/helpers';
44
import { CreateEntryCommand } from './create-entry-command';
55

66
export interface CreateEntryArgs {
7+
githubRepository?: string;
8+
moduleVersion: string;
9+
localRegistry: string;
10+
tag: string;
711
templatesDir: string;
812
}
913

@@ -22,9 +26,36 @@ export function createParser(
2226
'create-entry',
2327
'Create a new module version entry for the BCR',
2428
(yargs) => {
29+
yargs.option('github-repository', {
30+
describe:
31+
'GitHub repository for the module being published. Used to substititue the OWNER and REPO vars into the source template.',
32+
type: 'string',
33+
required: false,
34+
requiresArg: true,
35+
});
36+
yargs.option('local-registry', {
37+
describe:
38+
'Path to a locally checked out registry where the entry files will be created.',
39+
type: 'string',
40+
required: true,
41+
requiresArg: true,
42+
});
43+
yargs.option('module-version', {
44+
describe: 'The module version to publish to the registry.',
45+
type: 'string',
46+
required: true,
47+
requiresArg: true,
48+
});
49+
yargs.option('tag', {
50+
describe:
51+
"Tag of the the module repository's release. Used for substitution in the source template.",
52+
type: 'string',
53+
required: false,
54+
requiresArg: true,
55+
});
2556
yargs.option('templates-dir', {
2657
describe:
27-
'Directory containing a config file, BCR templates, and other release files: config.yml, source.template.json, metadata.template.json, presubmit.yaml. Equivalent to the .bcr directory required by the legacy GitHub app.',
58+
'Directory containing BCR release template files: metadata.template.json, source.template.json, presubmit.yaml, patches/. Equivalent to the .bcr directory required by the legacy GitHub app.',
2859
type: 'string',
2960
required: true,
3061
requiresArg: true,

src/application/release-event-handler.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,30 @@ export class ReleaseEventHandler {
6666
let branch: string;
6767
const candidateBcrForks: Repository[] = [];
6868
try {
69+
await Promise.all([
70+
bcr.shallowCloneAndCheckout('main'),
71+
rulesetRepo.shallowCloneAndCheckout(tag),
72+
]);
73+
6974
for (const moduleRoot of rulesetRepo.config.moduleRoots) {
7075
console.log(`Creating BCR entry for module root '${moduleRoot}'`);
7176

77+
const sourceTemplate = rulesetRepo.sourceTemplate(moduleRoot);
78+
const version = RulesetRepository.getVersionFromTag(tag);
79+
sourceTemplate.substitute({
80+
OWNER: rulesetRepo.owner,
81+
REPO: rulesetRepo.name,
82+
VERSION: version,
83+
TAG: tag,
84+
});
85+
7286
const { moduleName } = await this.createEntryService.createEntryFiles(
73-
rulesetRepo,
74-
bcr,
75-
tag,
76-
moduleRoot
87+
rulesetRepo.metadataTemplate(moduleRoot),
88+
sourceTemplate,
89+
rulesetRepo.presubmitPath(moduleRoot),
90+
rulesetRepo.patchesPath(moduleRoot),
91+
bcr.diskPath,
92+
version
7793
);
7894
moduleNames.push(moduleName);
7995
}

0 commit comments

Comments
 (0)