refactor(dev-infra): use a singleton for GitClient (#41515)
Creates a singleton class for GitClient rather than relying on creating an instance to require being passed around throughout its usages. PR Close #41515
This commit is contained in:
parent
c20db69f9f
commit
9bf8e5164d
|
@ -5,6 +5,7 @@
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
import {installVirtualGitClientSpies} from '../../utils/testing';
|
||||||
import {BaseModule} from './base';
|
import {BaseModule} from './base';
|
||||||
|
|
||||||
/** Data mocking as the "retrieved data". */
|
/** Data mocking as the "retrieved data". */
|
||||||
|
@ -23,17 +24,18 @@ describe('BaseModule', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
retrieveDataSpy = spyOn(ConcreteBaseModule.prototype, 'retrieveData');
|
retrieveDataSpy = spyOn(ConcreteBaseModule.prototype, 'retrieveData');
|
||||||
|
installVirtualGitClientSpies();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('begins retrieving data during construction', () => {
|
it('begins retrieving data during construction', () => {
|
||||||
new ConcreteBaseModule({} as any, {} as any);
|
new ConcreteBaseModule({} as any);
|
||||||
|
|
||||||
expect(retrieveDataSpy).toHaveBeenCalled();
|
expect(retrieveDataSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('makes the data available via the data attribute', async () => {
|
it('makes the data available via the data attribute', async () => {
|
||||||
retrieveDataSpy.and.callThrough();
|
retrieveDataSpy.and.callThrough();
|
||||||
const module = new ConcreteBaseModule({} as any, {} as any);
|
const module = new ConcreteBaseModule({} as any);
|
||||||
|
|
||||||
expect(await module.data).toBe(exampleData);
|
expect(await module.data).toBe(exampleData);
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,11 +11,12 @@ import {CaretakerConfig} from '../config';
|
||||||
|
|
||||||
/** The BaseModule to extend modules for caretaker checks from. */
|
/** The BaseModule to extend modules for caretaker checks from. */
|
||||||
export abstract class BaseModule<Data> {
|
export abstract class BaseModule<Data> {
|
||||||
|
/** The singleton instance of the GitClient. */
|
||||||
|
protected git = GitClient.getAuthenticatedInstance();
|
||||||
/** The data for the module. */
|
/** The data for the module. */
|
||||||
readonly data = this.retrieveData();
|
readonly data = this.retrieveData();
|
||||||
|
|
||||||
constructor(
|
constructor(protected config: NgDevConfig<{caretaker: CaretakerConfig}>) {}
|
||||||
protected git: GitClient, protected config: NgDevConfig<{caretaker: CaretakerConfig}>) {}
|
|
||||||
|
|
||||||
/** Asyncronously retrieve data for the module. */
|
/** Asyncronously retrieve data for the module. */
|
||||||
protected abstract retrieveData(): Promise<Data>;
|
protected abstract retrieveData(): Promise<Data>;
|
||||||
|
|
|
@ -23,15 +23,13 @@ const moduleList = [
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Check the status of services which Angular caretakers need to monitor. */
|
/** Check the status of services which Angular caretakers need to monitor. */
|
||||||
export async function checkServiceStatuses(githubToken: string) {
|
export async function checkServiceStatuses() {
|
||||||
|
// Set the verbose logging state of the GitClient.
|
||||||
|
GitClient.getAuthenticatedInstance().setVerboseLoggingState(false);
|
||||||
/** The configuration for the caretaker commands. */
|
/** The configuration for the caretaker commands. */
|
||||||
const config = getCaretakerConfig();
|
const config = getCaretakerConfig();
|
||||||
/** The GitClient for interacting with git and Github. */
|
|
||||||
const git = new GitClient(githubToken, config);
|
|
||||||
// Prevent logging of the git commands being executed during the check.
|
|
||||||
GitClient.LOG_COMMANDS = false;
|
|
||||||
/** List of instances of Caretaker Check modules */
|
/** List of instances of Caretaker Check modules */
|
||||||
const caretakerCheckModules = moduleList.map(module => new module(git, config));
|
const caretakerCheckModules = moduleList.map(module => new module(config));
|
||||||
|
|
||||||
// Module's `data` is casted as Promise<unknown> because the data types of the `module`'s `data`
|
// Module's `data` is casted as Promise<unknown> because the data types of the `module`'s `data`
|
||||||
// promises do not match typings, however our usage here is only to determine when the promise
|
// promises do not match typings, however our usage here is only to determine when the promise
|
||||||
|
|
|
@ -7,11 +7,11 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import {SemVer} from 'semver';
|
import {SemVer} from 'semver';
|
||||||
import {ReleaseTrain} from '../../release/versioning';
|
|
||||||
|
|
||||||
|
import {ReleaseTrain} from '../../release/versioning';
|
||||||
import * as versioning from '../../release/versioning/active-release-trains';
|
import * as versioning from '../../release/versioning/active-release-trains';
|
||||||
import * as console from '../../utils/console';
|
import * as console from '../../utils/console';
|
||||||
import {buildVirtualGitClient, mockNgDevConfig, VirtualGitClient} from '../../utils/testing';
|
import {installVirtualGitClientSpies, mockNgDevConfig} from '../../utils/testing';
|
||||||
|
|
||||||
import {CiModule} from './ci';
|
import {CiModule} from './ci';
|
||||||
|
|
||||||
|
@ -20,10 +20,9 @@ describe('CiModule', () => {
|
||||||
let getBranchStatusFromCiSpy: jasmine.Spy;
|
let getBranchStatusFromCiSpy: jasmine.Spy;
|
||||||
let infoSpy: jasmine.Spy;
|
let infoSpy: jasmine.Spy;
|
||||||
let debugSpy: jasmine.Spy;
|
let debugSpy: jasmine.Spy;
|
||||||
let virtualGitClient: VirtualGitClient;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
virtualGitClient = buildVirtualGitClient();
|
installVirtualGitClientSpies();
|
||||||
fetchActiveReleaseTrainsSpy = spyOn(versioning, 'fetchActiveReleaseTrains');
|
fetchActiveReleaseTrainsSpy = spyOn(versioning, 'fetchActiveReleaseTrains');
|
||||||
getBranchStatusFromCiSpy = spyOn(CiModule.prototype, 'getBranchStatusFromCi' as any);
|
getBranchStatusFromCiSpy = spyOn(CiModule.prototype, 'getBranchStatusFromCi' as any);
|
||||||
infoSpy = spyOn(console, 'info');
|
infoSpy = spyOn(console, 'info');
|
||||||
|
@ -34,7 +33,7 @@ describe('CiModule', () => {
|
||||||
it('handles active rc train', async () => {
|
it('handles active rc train', async () => {
|
||||||
const trains = buildMockActiveReleaseTrains(true);
|
const trains = buildMockActiveReleaseTrains(true);
|
||||||
fetchActiveReleaseTrainsSpy.and.resolveTo(trains);
|
fetchActiveReleaseTrainsSpy.and.resolveTo(trains);
|
||||||
const module = new CiModule(virtualGitClient, {caretaker: {}, ...mockNgDevConfig});
|
const module = new CiModule({caretaker: {}, ...mockNgDevConfig});
|
||||||
await module.data;
|
await module.data;
|
||||||
|
|
||||||
expect(getBranchStatusFromCiSpy).toHaveBeenCalledWith(trains.releaseCandidate.branchName);
|
expect(getBranchStatusFromCiSpy).toHaveBeenCalledWith(trains.releaseCandidate.branchName);
|
||||||
|
@ -46,7 +45,7 @@ describe('CiModule', () => {
|
||||||
it('handles an inactive rc train', async () => {
|
it('handles an inactive rc train', async () => {
|
||||||
const trains = buildMockActiveReleaseTrains(false);
|
const trains = buildMockActiveReleaseTrains(false);
|
||||||
fetchActiveReleaseTrainsSpy.and.resolveTo(trains);
|
fetchActiveReleaseTrainsSpy.and.resolveTo(trains);
|
||||||
const module = new CiModule(virtualGitClient, {caretaker: {}, ...mockNgDevConfig});
|
const module = new CiModule({caretaker: {}, ...mockNgDevConfig});
|
||||||
await module.data;
|
await module.data;
|
||||||
|
|
||||||
expect(getBranchStatusFromCiSpy).toHaveBeenCalledWith(trains.latest.branchName);
|
expect(getBranchStatusFromCiSpy).toHaveBeenCalledWith(trains.latest.branchName);
|
||||||
|
@ -58,7 +57,7 @@ describe('CiModule', () => {
|
||||||
const trains = buildMockActiveReleaseTrains(false);
|
const trains = buildMockActiveReleaseTrains(false);
|
||||||
fetchActiveReleaseTrainsSpy.and.resolveTo(trains);
|
fetchActiveReleaseTrainsSpy.and.resolveTo(trains);
|
||||||
getBranchStatusFromCiSpy.and.returnValue('success');
|
getBranchStatusFromCiSpy.and.returnValue('success');
|
||||||
const module = new CiModule(virtualGitClient, {caretaker: {}, ...mockNgDevConfig});
|
const module = new CiModule({caretaker: {}, ...mockNgDevConfig});
|
||||||
const data = await module.data;
|
const data = await module.data;
|
||||||
|
|
||||||
expect(data[0]).toEqual(
|
expect(data[0]).toEqual(
|
||||||
|
@ -89,7 +88,7 @@ describe('CiModule', () => {
|
||||||
]);
|
]);
|
||||||
fetchActiveReleaseTrainsSpy.and.resolveTo([]);
|
fetchActiveReleaseTrainsSpy.and.resolveTo([]);
|
||||||
|
|
||||||
const module = new CiModule(virtualGitClient, {caretaker: {}, ...mockNgDevConfig});
|
const module = new CiModule({caretaker: {}, ...mockNgDevConfig});
|
||||||
Object.defineProperty(module, 'data', {value: fakeData});
|
Object.defineProperty(module, 'data', {value: fakeData});
|
||||||
|
|
||||||
await module.printToTerminal();
|
await module.printToTerminal();
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Arguments, Argv, CommandModule} from 'yargs';
|
import {Argv, CommandModule} from 'yargs';
|
||||||
|
|
||||||
import {addGithubTokenOption} from '../../utils/git/github-yargs';
|
import {addGithubTokenOption} from '../../utils/git/github-yargs';
|
||||||
|
|
||||||
|
@ -23,8 +23,8 @@ function builder(yargs: Argv) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Handles the command. */
|
/** Handles the command. */
|
||||||
async function handler({githubToken}: Arguments<CaretakerCheckOptions>) {
|
async function handler() {
|
||||||
await checkServiceStatuses(githubToken);
|
await checkServiceStatuses();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** yargs command module for checking status information for the repository */
|
/** yargs command module for checking status information for the repository */
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
import {SpawnSyncReturns} from 'child_process';
|
import {SpawnSyncReturns} from 'child_process';
|
||||||
|
|
||||||
import * as console from '../../utils/console';
|
import * as console from '../../utils/console';
|
||||||
import {buildVirtualGitClient, mockNgDevConfig, VirtualGitClient} from '../../utils/testing';
|
import {GitClient} from '../../utils/git';
|
||||||
|
import {installVirtualGitClientSpies, mockNgDevConfig} from '../../utils/testing';
|
||||||
|
|
||||||
import {G3Module, G3StatsData} from './g3';
|
import {G3Module, G3StatsData} from './g3';
|
||||||
|
|
||||||
|
@ -18,10 +19,9 @@ describe('G3Module', () => {
|
||||||
let getLatestShas: jasmine.Spy;
|
let getLatestShas: jasmine.Spy;
|
||||||
let getDiffStats: jasmine.Spy;
|
let getDiffStats: jasmine.Spy;
|
||||||
let infoSpy: jasmine.Spy;
|
let infoSpy: jasmine.Spy;
|
||||||
let virtualGitClient: VirtualGitClient;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
virtualGitClient = buildVirtualGitClient();
|
installVirtualGitClientSpies();
|
||||||
getG3FileIncludeAndExcludeLists =
|
getG3FileIncludeAndExcludeLists =
|
||||||
spyOn(G3Module.prototype, 'getG3FileIncludeAndExcludeLists' as any).and.returnValue(null);
|
spyOn(G3Module.prototype, 'getG3FileIncludeAndExcludeLists' as any).and.returnValue(null);
|
||||||
getLatestShas = spyOn(G3Module.prototype, 'getLatestShas' as any).and.returnValue(null);
|
getLatestShas = spyOn(G3Module.prototype, 'getLatestShas' as any).and.returnValue(null);
|
||||||
|
@ -33,7 +33,7 @@ describe('G3Module', () => {
|
||||||
it('unless the g3 merge config is not defined in the angular robot file', async () => {
|
it('unless the g3 merge config is not defined in the angular robot file', async () => {
|
||||||
getG3FileIncludeAndExcludeLists.and.returnValue(null);
|
getG3FileIncludeAndExcludeLists.and.returnValue(null);
|
||||||
getLatestShas.and.returnValue({g3: 'abc123', master: 'zxy987'});
|
getLatestShas.and.returnValue({g3: 'abc123', master: 'zxy987'});
|
||||||
const module = new G3Module(virtualGitClient, {caretaker: {}, ...mockNgDevConfig});
|
const module = new G3Module({caretaker: {}, ...mockNgDevConfig});
|
||||||
|
|
||||||
expect(getDiffStats).not.toHaveBeenCalled();
|
expect(getDiffStats).not.toHaveBeenCalled();
|
||||||
expect(await module.data).toBe(undefined);
|
expect(await module.data).toBe(undefined);
|
||||||
|
@ -42,7 +42,7 @@ describe('G3Module', () => {
|
||||||
it('unless the branch shas are not able to be retrieved', async () => {
|
it('unless the branch shas are not able to be retrieved', async () => {
|
||||||
getLatestShas.and.returnValue(null);
|
getLatestShas.and.returnValue(null);
|
||||||
getG3FileIncludeAndExcludeLists.and.returnValue({include: ['file1'], exclude: []});
|
getG3FileIncludeAndExcludeLists.and.returnValue({include: ['file1'], exclude: []});
|
||||||
const module = new G3Module(virtualGitClient, {caretaker: {}, ...mockNgDevConfig});
|
const module = new G3Module({caretaker: {}, ...mockNgDevConfig});
|
||||||
|
|
||||||
expect(getDiffStats).not.toHaveBeenCalled();
|
expect(getDiffStats).not.toHaveBeenCalled();
|
||||||
expect(await module.data).toBe(undefined);
|
expect(await module.data).toBe(undefined);
|
||||||
|
@ -52,7 +52,7 @@ describe('G3Module', () => {
|
||||||
getLatestShas.and.returnValue({g3: 'abc123', master: 'zxy987'});
|
getLatestShas.and.returnValue({g3: 'abc123', master: 'zxy987'});
|
||||||
getG3FileIncludeAndExcludeLists.and.returnValue({include: ['project1/*'], exclude: []});
|
getG3FileIncludeAndExcludeLists.and.returnValue({include: ['project1/*'], exclude: []});
|
||||||
getDiffStats.and.callThrough();
|
getDiffStats.and.callThrough();
|
||||||
spyOn(virtualGitClient, 'run').and.callFake((args: string[]): any => {
|
spyOn(GitClient.prototype, 'run').and.callFake((args: string[]): any => {
|
||||||
const output: Partial<SpawnSyncReturns<string>> = {};
|
const output: Partial<SpawnSyncReturns<string>> = {};
|
||||||
if (args[0] === 'rev-list') {
|
if (args[0] === 'rev-list') {
|
||||||
output.stdout = '3';
|
output.stdout = '3';
|
||||||
|
@ -63,7 +63,7 @@ describe('G3Module', () => {
|
||||||
return output;
|
return output;
|
||||||
});
|
});
|
||||||
|
|
||||||
const module = new G3Module(virtualGitClient, {caretaker: {}, ...mockNgDevConfig});
|
const module = new G3Module({caretaker: {}, ...mockNgDevConfig});
|
||||||
const {insertions, deletions, files, commits} = (await module.data) as G3StatsData;
|
const {insertions, deletions, files, commits} = (await module.data) as G3StatsData;
|
||||||
|
|
||||||
expect(insertions).toBe(12);
|
expect(insertions).toBe(12);
|
||||||
|
@ -82,7 +82,7 @@ describe('G3Module', () => {
|
||||||
commits: 2,
|
commits: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
const module = new G3Module(virtualGitClient, {caretaker: {}, ...mockNgDevConfig});
|
const module = new G3Module({caretaker: {}, ...mockNgDevConfig});
|
||||||
Object.defineProperty(module, 'data', {value: fakeData});
|
Object.defineProperty(module, 'data', {value: fakeData});
|
||||||
await module.printToTerminal();
|
await module.printToTerminal();
|
||||||
|
|
||||||
|
@ -98,7 +98,7 @@ describe('G3Module', () => {
|
||||||
commits: 25,
|
commits: 25,
|
||||||
});
|
});
|
||||||
|
|
||||||
const module = new G3Module(virtualGitClient, {caretaker: {}, ...mockNgDevConfig});
|
const module = new G3Module({caretaker: {}, ...mockNgDevConfig});
|
||||||
Object.defineProperty(module, 'data', {value: fakeData});
|
Object.defineProperty(module, 'data', {value: fakeData});
|
||||||
await module.printToTerminal();
|
await module.printToTerminal();
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import * as console from '../../utils/console';
|
import * as console from '../../utils/console';
|
||||||
import {GithubGraphqlClient} from '../../utils/git/github';
|
import {GithubClient} from '../../utils/git/github';
|
||||||
import {buildVirtualGitClient, mockNgDevConfig, VirtualGitClient} from '../../utils/testing';
|
import {installVirtualGitClientSpies, mockNgDevConfig} from '../../utils/testing';
|
||||||
|
|
||||||
import {GithubQueriesModule} from './github';
|
import {GithubQueriesModule} from './github';
|
||||||
|
|
||||||
|
@ -16,37 +16,35 @@ describe('GithubQueriesModule', () => {
|
||||||
let githubApiSpy: jasmine.Spy;
|
let githubApiSpy: jasmine.Spy;
|
||||||
let infoSpy: jasmine.Spy;
|
let infoSpy: jasmine.Spy;
|
||||||
let infoGroupSpy: jasmine.Spy;
|
let infoGroupSpy: jasmine.Spy;
|
||||||
let virtualGitClient: VirtualGitClient;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
githubApiSpy = spyOn(GithubGraphqlClient.prototype, 'query')
|
githubApiSpy = spyOn(GithubClient.prototype, 'graphql')
|
||||||
.and.throwError(
|
.and.throwError(
|
||||||
'The graphql query response must always be manually defined in a test.');
|
'The graphql query response must always be manually defined in a test.');
|
||||||
virtualGitClient = buildVirtualGitClient();
|
installVirtualGitClientSpies();
|
||||||
infoGroupSpy = spyOn(console.info, 'group');
|
infoGroupSpy = spyOn(console.info, 'group');
|
||||||
infoSpy = spyOn(console, 'info');
|
infoSpy = spyOn(console, 'info');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('gathering stats', () => {
|
describe('gathering stats', () => {
|
||||||
it('unless githubQueries are `undefined`', async () => {
|
it('unless githubQueries are `undefined`', async () => {
|
||||||
const module = new GithubQueriesModule(
|
const module =
|
||||||
virtualGitClient, {...mockNgDevConfig, caretaker: {githubQueries: undefined}});
|
new GithubQueriesModule({...mockNgDevConfig, caretaker: {githubQueries: undefined}});
|
||||||
|
|
||||||
expect(await module.data).toBe(undefined);
|
expect(await module.data).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('unless githubQueries are an empty array', async () => {
|
it('unless githubQueries are an empty array', async () => {
|
||||||
const module = new GithubQueriesModule(
|
const module = new GithubQueriesModule({...mockNgDevConfig, caretaker: {githubQueries: []}});
|
||||||
virtualGitClient, {...mockNgDevConfig, caretaker: {githubQueries: []}});
|
|
||||||
|
|
||||||
expect(await module.data).toBe(undefined);
|
expect(await module.data).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('for the requestd Github queries', async () => {
|
it('for the requested Github queries', async () => {
|
||||||
githubApiSpy.and.returnValue({
|
githubApiSpy.and.returnValue({
|
||||||
'keynamewithspaces': {issueCount: 1, nodes: [{url: 'http://gituhb.com/owner/name/issue/1'}]}
|
'keynamewithspaces': {issueCount: 1, nodes: [{url: 'http://github.com/owner/name/issue/1'}]}
|
||||||
});
|
});
|
||||||
const module = new GithubQueriesModule(virtualGitClient, {
|
const module = new GithubQueriesModule({
|
||||||
...mockNgDevConfig,
|
...mockNgDevConfig,
|
||||||
caretaker: {githubQueries: [{name: 'key name with spaces', query: 'issue: yes'}]}
|
caretaker: {githubQueries: [{name: 'key name with spaces', query: 'issue: yes'}]}
|
||||||
});
|
});
|
||||||
|
@ -55,7 +53,7 @@ describe('GithubQueriesModule', () => {
|
||||||
queryName: 'key name with spaces',
|
queryName: 'key name with spaces',
|
||||||
count: 1,
|
count: 1,
|
||||||
queryUrl: 'https://github.com/owner/name/issues?q=issue:%20yes',
|
queryUrl: 'https://github.com/owner/name/issues?q=issue:%20yes',
|
||||||
matchedUrls: ['http://gituhb.com/owner/name/issue/1'],
|
matchedUrls: ['http://github.com/owner/name/issue/1'],
|
||||||
}]);
|
}]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -78,7 +76,7 @@ describe('GithubQueriesModule', () => {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
const module = new GithubQueriesModule(virtualGitClient, {caretaker: {}, ...mockNgDevConfig});
|
const module = new GithubQueriesModule({caretaker: {}, ...mockNgDevConfig});
|
||||||
Object.defineProperty(module, 'data', {value: fakeData});
|
Object.defineProperty(module, 'data', {value: fakeData});
|
||||||
|
|
||||||
await module.printToTerminal();
|
await module.printToTerminal();
|
||||||
|
@ -95,7 +93,7 @@ describe('GithubQueriesModule', () => {
|
||||||
queryName: 'query1',
|
queryName: 'query1',
|
||||||
count: 1,
|
count: 1,
|
||||||
queryUrl: 'https://github.com/owner/name/issues?q=issue:%20yes',
|
queryUrl: 'https://github.com/owner/name/issues?q=issue:%20yes',
|
||||||
matchedUrls: ['http://gituhb.com/owner/name/issue/1'],
|
matchedUrls: ['http://github.com/owner/name/issue/1'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
queryName: 'query2',
|
queryName: 'query2',
|
||||||
|
@ -105,7 +103,7 @@ describe('GithubQueriesModule', () => {
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const module = new GithubQueriesModule(virtualGitClient, {caretaker: {}, ...mockNgDevConfig});
|
const module = new GithubQueriesModule({caretaker: {}, ...mockNgDevConfig});
|
||||||
Object.defineProperty(module, 'data', {value: fakeData});
|
Object.defineProperty(module, 'data', {value: fakeData});
|
||||||
|
|
||||||
await module.printToTerminal();
|
await module.printToTerminal();
|
||||||
|
@ -114,7 +112,7 @@ describe('GithubQueriesModule', () => {
|
||||||
expect(infoSpy).toHaveBeenCalledWith('query1 1');
|
expect(infoSpy).toHaveBeenCalledWith('query1 1');
|
||||||
expect(infoGroupSpy)
|
expect(infoGroupSpy)
|
||||||
.toHaveBeenCalledWith('https://github.com/owner/name/issues?q=issue:%20yes');
|
.toHaveBeenCalledWith('https://github.com/owner/name/issues?q=issue:%20yes');
|
||||||
expect(infoSpy).toHaveBeenCalledWith('- http://gituhb.com/owner/name/issue/1');
|
expect(infoSpy).toHaveBeenCalledWith('- http://github.com/owner/name/issue/1');
|
||||||
expect(infoSpy).toHaveBeenCalledWith('query2 0');
|
expect(infoSpy).toHaveBeenCalledWith('query2 0');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -56,7 +56,7 @@ export class GithubQueriesModule extends BaseModule<GithubQueryResults|void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The results of the generated github query. */
|
/** The results of the generated github query. */
|
||||||
const queryResult = await this.git.github.graphql.query(this.buildGraphqlQuery(queries));
|
const queryResult = await this.git.github.graphql(this.buildGraphqlQuery(queries));
|
||||||
const results = Object.values(queryResult);
|
const results = Object.values(queryResult);
|
||||||
|
|
||||||
const {owner, name: repo} = this.git.remoteConfig;
|
const {owner, name: repo} = this.git.remoteConfig;
|
||||||
|
@ -74,16 +74,16 @@ export class GithubQueriesModule extends BaseModule<GithubQueryResults|void> {
|
||||||
/** Build a Graphql query statement for the provided queries. */
|
/** Build a Graphql query statement for the provided queries. */
|
||||||
private buildGraphqlQuery(queries: NonNullable<CaretakerConfig['githubQueries']>) {
|
private buildGraphqlQuery(queries: NonNullable<CaretakerConfig['githubQueries']>) {
|
||||||
/** The query object for graphql. */
|
/** The query object for graphql. */
|
||||||
const graphQlQuery: GithubQueryResult = {};
|
const graphqlQuery: GithubQueryResult = {};
|
||||||
const {owner, name: repo} = this.git.remoteConfig;
|
const {owner, name: repo} = this.git.remoteConfig;
|
||||||
/** The Github search filter for the configured repository. */
|
/** The Github search filter for the configured repository. */
|
||||||
const repoFilter = `repo:${owner}/${repo}`;
|
const repoFilter = `repo:${owner}/${repo}`;
|
||||||
|
|
||||||
|
|
||||||
queries.forEach(({name, query}) => {
|
queries.forEach(({name, query}) => {
|
||||||
/** The name of the query, with spaces removed to match GraphQL requirements. */
|
/** The name of the query, with spaces removed to match Graphql requirements. */
|
||||||
const queryKey = alias(name.replace(/ /g, ''), 'search');
|
const queryKey = alias(name.replace(/ /g, ''), 'search');
|
||||||
graphQlQuery[queryKey] = params(
|
graphqlQuery[queryKey] = params(
|
||||||
{
|
{
|
||||||
type: 'ISSUE',
|
type: 'ISSUE',
|
||||||
first: MAX_RETURNED_ISSUES,
|
first: MAX_RETURNED_ISSUES,
|
||||||
|
@ -92,7 +92,7 @@ export class GithubQueriesModule extends BaseModule<GithubQueryResults|void> {
|
||||||
{...GithubQueryResultFragment});
|
{...GithubQueryResultFragment});
|
||||||
});
|
});
|
||||||
|
|
||||||
return graphQlQuery;
|
return graphqlQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
async printToTerminal() {
|
async printToTerminal() {
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as console from '../../utils/console';
|
import * as console from '../../utils/console';
|
||||||
import {buildVirtualGitClient, mockNgDevConfig, VirtualGitClient} from '../../utils/testing';
|
import {installVirtualGitClientSpies, mockNgDevConfig} from '../../utils/testing';
|
||||||
|
|
||||||
import {services, ServicesModule} from './services';
|
import {services, ServicesModule} from './services';
|
||||||
|
|
||||||
|
@ -16,20 +16,19 @@ describe('ServicesModule', () => {
|
||||||
let getStatusFromStandardApiSpy: jasmine.Spy;
|
let getStatusFromStandardApiSpy: jasmine.Spy;
|
||||||
let infoSpy: jasmine.Spy;
|
let infoSpy: jasmine.Spy;
|
||||||
let infoGroupSpy: jasmine.Spy;
|
let infoGroupSpy: jasmine.Spy;
|
||||||
let virtualGitClient: VirtualGitClient;
|
|
||||||
|
|
||||||
services.splice(0, Infinity, {url: 'fakeStatus.com/api.json', name: 'Service Name'});
|
services.splice(0, Infinity, {url: 'fakeStatus.com/api.json', name: 'Service Name'});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
getStatusFromStandardApiSpy = spyOn(ServicesModule.prototype, 'getStatusFromStandardApi');
|
getStatusFromStandardApiSpy = spyOn(ServicesModule.prototype, 'getStatusFromStandardApi');
|
||||||
virtualGitClient = buildVirtualGitClient();
|
installVirtualGitClientSpies();
|
||||||
infoGroupSpy = spyOn(console.info, 'group');
|
infoGroupSpy = spyOn(console.info, 'group');
|
||||||
infoSpy = spyOn(console, 'info');
|
infoSpy = spyOn(console, 'info');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('gathering status', () => {
|
describe('gathering status', () => {
|
||||||
it('for each of the services', async () => {
|
it('for each of the services', async () => {
|
||||||
new ServicesModule(virtualGitClient, {caretaker: {}, ...mockNgDevConfig});
|
new ServicesModule({caretaker: {}, ...mockNgDevConfig});
|
||||||
|
|
||||||
expect(getStatusFromStandardApiSpy)
|
expect(getStatusFromStandardApiSpy)
|
||||||
.toHaveBeenCalledWith({url: 'fakeStatus.com/api.json', name: 'Service Name'});
|
.toHaveBeenCalledWith({url: 'fakeStatus.com/api.json', name: 'Service Name'});
|
||||||
|
@ -54,7 +53,7 @@ describe('ServicesModule', () => {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
const module = new ServicesModule(virtualGitClient, {caretaker: {}, ...mockNgDevConfig});
|
const module = new ServicesModule({caretaker: {}, ...mockNgDevConfig});
|
||||||
Object.defineProperty(module, 'data', {value: fakeData});
|
Object.defineProperty(module, 'data', {value: fakeData});
|
||||||
await module.printToTerminal();
|
await module.printToTerminal();
|
||||||
|
|
||||||
|
|
|
@ -405,36 +405,6 @@ function getListCommitsInBranchUrl(_a, branchName) {
|
||||||
return "https://github.com/" + remoteParams.owner + "/" + remoteParams.repo + "/commits/" + branchName;
|
return "https://github.com/" + remoteParams.owner + "/" + remoteParams.repo + "/commits/" + branchName;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright Google LLC All Rights Reserved.
|
|
||||||
*
|
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
|
||||||
* found in the LICENSE file at https://angular.io/license
|
|
||||||
*/
|
|
||||||
/** Sets up the `github-token` command option for the given Yargs instance. */
|
|
||||||
function addGithubTokenOption(yargs) {
|
|
||||||
return yargs
|
|
||||||
// 'github-token' is casted to 'githubToken' to properly set up typings to reflect the key in
|
|
||||||
// the Argv object being camelCase rather than kebob case due to the `camel-case-expansion`
|
|
||||||
// config: https://github.com/yargs/yargs-parser#camel-case-expansion
|
|
||||||
.option('github-token', {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Github token. If not set, token is retrieved from the environment variables.',
|
|
||||||
coerce: function (token) {
|
|
||||||
var githubToken = token || process.env.GITHUB_TOKEN || process.env.TOKEN;
|
|
||||||
if (!githubToken) {
|
|
||||||
error(red('No Github token set. Please set the `GITHUB_TOKEN` environment variable.'));
|
|
||||||
error(red('Alternatively, pass the `--github-token` command line flag.'));
|
|
||||||
error(yellow("You can generate a token here: " + GITHUB_TOKEN_GENERATE_URL));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
return githubToken;
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.default('github-token', '', '<LOCAL TOKEN>');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @license
|
* @license
|
||||||
* Copyright Google LLC All Rights Reserved.
|
* Copyright Google LLC All Rights Reserved.
|
||||||
|
@ -477,6 +447,14 @@ var GithubApiRequestError = /** @class */ (function (_super) {
|
||||||
}
|
}
|
||||||
return GithubApiRequestError;
|
return GithubApiRequestError;
|
||||||
}(Error));
|
}(Error));
|
||||||
|
/** Error for failed Github API requests. */
|
||||||
|
var GithubGraphqlClientError = /** @class */ (function (_super) {
|
||||||
|
tslib.__extends(GithubGraphqlClientError, _super);
|
||||||
|
function GithubGraphqlClientError() {
|
||||||
|
return _super !== null && _super.apply(this, arguments) || this;
|
||||||
|
}
|
||||||
|
return GithubGraphqlClientError;
|
||||||
|
}(Error));
|
||||||
/**
|
/**
|
||||||
* A Github client for interacting with the Github APIs.
|
* A Github client for interacting with the Github APIs.
|
||||||
*
|
*
|
||||||
|
@ -485,21 +463,48 @@ var GithubApiRequestError = /** @class */ (function (_super) {
|
||||||
**/
|
**/
|
||||||
var GithubClient = /** @class */ (function (_super) {
|
var GithubClient = /** @class */ (function (_super) {
|
||||||
tslib.__extends(GithubClient, _super);
|
tslib.__extends(GithubClient, _super);
|
||||||
|
/**
|
||||||
|
* @param token The github authentication token for Github Rest and Graphql API requests.
|
||||||
|
*/
|
||||||
function GithubClient(token) {
|
function GithubClient(token) {
|
||||||
var _this =
|
var _this =
|
||||||
// Pass in authentication token to base Octokit class.
|
// Pass in authentication token to base Octokit class.
|
||||||
_super.call(this, { auth: token }) || this;
|
_super.call(this, { auth: token }) || this;
|
||||||
|
_this.token = token;
|
||||||
/** The current user based on checking against the Github API. */
|
/** The current user based on checking against the Github API. */
|
||||||
_this._currentUser = null;
|
_this._currentUser = null;
|
||||||
|
/** The graphql instance with authentication set during construction. */
|
||||||
|
_this._graphql = graphql.graphql.defaults({ headers: { authorization: "token " + _this.token } });
|
||||||
_this.hook.error('request', function (error) {
|
_this.hook.error('request', function (error) {
|
||||||
// Wrap API errors in a known error class. This allows us to
|
// Wrap API errors in a known error class. This allows us to
|
||||||
// expect Github API errors better and in a non-ambiguous way.
|
// expect Github API errors better and in a non-ambiguous way.
|
||||||
throw new GithubApiRequestError(error.status, error.message);
|
throw new GithubApiRequestError(error.status, error.message);
|
||||||
});
|
});
|
||||||
// Create authenticated graphql client.
|
// Note: The prototype must be set explictly as Github's Octokit class is a non-standard class
|
||||||
_this.graphql = new GithubGraphqlClient(token);
|
// definition which adjusts the prototype chain.
|
||||||
|
// See:
|
||||||
|
// https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work
|
||||||
|
// https://github.com/octokit/rest.js/blob/7b51cee4a22b6e52adcdca011f93efdffa5df998/lib/constructor.js
|
||||||
|
Object.setPrototypeOf(_this, GithubClient.prototype);
|
||||||
return _this;
|
return _this;
|
||||||
}
|
}
|
||||||
|
/** Perform a query using Github's Graphql API. */
|
||||||
|
GithubClient.prototype.graphql = function (queryObject, params) {
|
||||||
|
if (params === void 0) { params = {}; }
|
||||||
|
return tslib.__awaiter(this, void 0, void 0, function () {
|
||||||
|
return tslib.__generator(this, function (_a) {
|
||||||
|
switch (_a.label) {
|
||||||
|
case 0:
|
||||||
|
if (this.token === undefined) {
|
||||||
|
throw new GithubGraphqlClientError('Cannot query via graphql without an authentication token set, use the authenticated ' +
|
||||||
|
'`GitClient` by calling `GitClient.getAuthenticatedInstance()`.');
|
||||||
|
}
|
||||||
|
return [4 /*yield*/, this._graphql(typedGraphqlify.query(queryObject), params)];
|
||||||
|
case 1: return [2 /*return*/, (_a.sent())];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
/** Retrieve the login of the current user from Github. */
|
/** Retrieve the login of the current user from Github. */
|
||||||
GithubClient.prototype.getCurrentUser = function () {
|
GithubClient.prototype.getCurrentUser = function () {
|
||||||
return tslib.__awaiter(this, void 0, void 0, function () {
|
return tslib.__awaiter(this, void 0, void 0, function () {
|
||||||
|
@ -511,7 +516,7 @@ var GithubClient = /** @class */ (function (_super) {
|
||||||
if (this._currentUser !== null) {
|
if (this._currentUser !== null) {
|
||||||
return [2 /*return*/, this._currentUser];
|
return [2 /*return*/, this._currentUser];
|
||||||
}
|
}
|
||||||
return [4 /*yield*/, this.graphql.query({
|
return [4 /*yield*/, this.graphql({
|
||||||
viewer: {
|
viewer: {
|
||||||
login: typedGraphqlify.types.string,
|
login: typedGraphqlify.types.string,
|
||||||
}
|
}
|
||||||
|
@ -525,34 +530,6 @@ var GithubClient = /** @class */ (function (_super) {
|
||||||
};
|
};
|
||||||
return GithubClient;
|
return GithubClient;
|
||||||
}(Octokit));
|
}(Octokit));
|
||||||
/** A client for interacting with Github's GraphQL API. */
|
|
||||||
var GithubGraphqlClient = /** @class */ (function () {
|
|
||||||
function GithubGraphqlClient(token) {
|
|
||||||
/** The Github GraphQL (v4) API. */
|
|
||||||
this.graqhql = graphql.graphql;
|
|
||||||
// Set the default headers to include authorization with the provided token for all
|
|
||||||
// graphQL calls.
|
|
||||||
if (token) {
|
|
||||||
this.graqhql = this.graqhql.defaults({ headers: { authorization: "token " + token } });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/** Perform a query using Github's GraphQL API. */
|
|
||||||
GithubGraphqlClient.prototype.query = function (queryObject, params) {
|
|
||||||
if (params === void 0) { params = {}; }
|
|
||||||
return tslib.__awaiter(this, void 0, void 0, function () {
|
|
||||||
var queryString;
|
|
||||||
return tslib.__generator(this, function (_a) {
|
|
||||||
switch (_a.label) {
|
|
||||||
case 0:
|
|
||||||
queryString = typedGraphqlify.query(queryObject);
|
|
||||||
return [4 /*yield*/, this.graqhql(queryString, params)];
|
|
||||||
case 1: return [2 /*return*/, (_a.sent())];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return GithubGraphqlClient;
|
|
||||||
}());
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @license
|
* @license
|
||||||
|
@ -585,20 +562,19 @@ var GitCommandError = /** @class */ (function (_super) {
|
||||||
* the dev-infra configuration is loaded with its Github configuration.
|
* the dev-infra configuration is loaded with its Github configuration.
|
||||||
**/
|
**/
|
||||||
var GitClient = /** @class */ (function () {
|
var GitClient = /** @class */ (function () {
|
||||||
|
/**
|
||||||
|
* @param githubToken The github token used for authentication, if provided.
|
||||||
|
* @param _config The configuration, containing the github specific configuration.
|
||||||
|
* @param _projectRoot The full path to the root of the repository base.
|
||||||
|
*/
|
||||||
function GitClient(githubToken, _config, _projectRoot) {
|
function GitClient(githubToken, _config, _projectRoot) {
|
||||||
if (_config === void 0) { _config = getConfig(); }
|
if (_config === void 0) { _config = getConfig(); }
|
||||||
if (_projectRoot === void 0) { _projectRoot = getRepoBaseDir(); }
|
if (_projectRoot === void 0) { _projectRoot = getRepoBaseDir(); }
|
||||||
this.githubToken = githubToken;
|
this.githubToken = githubToken;
|
||||||
this._config = _config;
|
this._config = _config;
|
||||||
this._projectRoot = _projectRoot;
|
this._projectRoot = _projectRoot;
|
||||||
/** Short-hand for accessing the default remote configuration. */
|
/** Whether verbose logging of Git actions should be used. */
|
||||||
this.remoteConfig = this._config.github;
|
this.verboseLogging = true;
|
||||||
/** Octokit request parameters object for targeting the configured remote. */
|
|
||||||
this.remoteParams = { owner: this.remoteConfig.owner, repo: this.remoteConfig.name };
|
|
||||||
/** Git URL that resolves to the configured repository. */
|
|
||||||
this.repoGitUrl = getRepositoryGitUrl(this.remoteConfig, this.githubToken);
|
|
||||||
/** Instance of the authenticated Github octokit API. */
|
|
||||||
this.github = new GithubClient(this.githubToken);
|
|
||||||
/** The OAuth scopes available for the provided Github token. */
|
/** The OAuth scopes available for the provided Github token. */
|
||||||
this._cachedOauthScopes = null;
|
this._cachedOauthScopes = null;
|
||||||
/**
|
/**
|
||||||
|
@ -606,13 +582,51 @@ var GitClient = /** @class */ (function () {
|
||||||
* sanitizing the token from Git child process output.
|
* sanitizing the token from Git child process output.
|
||||||
*/
|
*/
|
||||||
this._githubTokenRegex = null;
|
this._githubTokenRegex = null;
|
||||||
|
/** Short-hand for accessing the default remote configuration. */
|
||||||
|
this.remoteConfig = this._config.github;
|
||||||
|
/** Octokit request parameters object for targeting the configured remote. */
|
||||||
|
this.remoteParams = { owner: this.remoteConfig.owner, repo: this.remoteConfig.name };
|
||||||
|
/** Instance of the authenticated Github octokit API. */
|
||||||
|
this.github = new GithubClient(this.githubToken);
|
||||||
// If a token has been specified (and is not empty), pass it to the Octokit API and
|
// If a token has been specified (and is not empty), pass it to the Octokit API and
|
||||||
// also create a regular expression that can be used for sanitizing Git command output
|
// also create a regular expression that can be used for sanitizing Git command output
|
||||||
// so that it does not print the token accidentally.
|
// so that it does not print the token accidentally.
|
||||||
if (githubToken != null) {
|
if (typeof githubToken === 'string') {
|
||||||
this._githubTokenRegex = new RegExp(githubToken, 'g');
|
this._githubTokenRegex = new RegExp(githubToken, 'g');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Static method to get the singleton instance of the unauthorized GitClient, creating it if it
|
||||||
|
* has not yet been created.
|
||||||
|
*/
|
||||||
|
GitClient.getInstance = function () {
|
||||||
|
if (!GitClient.unauthenticated) {
|
||||||
|
GitClient.unauthenticated = new GitClient(undefined);
|
||||||
|
}
|
||||||
|
return GitClient.unauthenticated;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Static method to get the singleton instance of the authenticated GitClient if it has been
|
||||||
|
* generated.
|
||||||
|
*/
|
||||||
|
GitClient.getAuthenticatedInstance = function () {
|
||||||
|
if (!GitClient.authenticated) {
|
||||||
|
throw Error('The authenticated GitClient has not yet been generated.');
|
||||||
|
}
|
||||||
|
return GitClient.authenticated;
|
||||||
|
};
|
||||||
|
/** Build the authenticated GitClient instance. */
|
||||||
|
GitClient.authenticateWithToken = function (token) {
|
||||||
|
if (GitClient.authenticated) {
|
||||||
|
throw Error('Cannot generate new authenticated GitClient after one has already been generated.');
|
||||||
|
}
|
||||||
|
GitClient.authenticated = new GitClient(token);
|
||||||
|
};
|
||||||
|
/** Set the verbose logging state of the GitClient instance. */
|
||||||
|
GitClient.prototype.setVerboseLoggingState = function (verbose) {
|
||||||
|
this.verboseLogging = verbose;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
/** Executes the given git command. Throws if the command fails. */
|
/** Executes the given git command. Throws if the command fails. */
|
||||||
GitClient.prototype.run = function (args, options) {
|
GitClient.prototype.run = function (args, options) {
|
||||||
var result = this.runGraceful(args, options);
|
var result = this.runGraceful(args, options);
|
||||||
|
@ -639,7 +653,7 @@ var GitClient = /** @class */ (function () {
|
||||||
// To improve the debugging experience in case something fails, we print all executed Git
|
// To improve the debugging experience in case something fails, we print all executed Git
|
||||||
// commands to better understand the git actions occuring. Depending on the command being
|
// commands to better understand the git actions occuring. Depending on the command being
|
||||||
// executed, this debugging information should be logged at different logging levels.
|
// executed, this debugging information should be logged at different logging levels.
|
||||||
var printFn = (!GitClient.LOG_COMMANDS || options.stdio === 'ignore') ? debug : info;
|
var printFn = (!this.verboseLogging || options.stdio === 'ignore') ? debug : info;
|
||||||
// Note that we do not want to print the token if it is contained in the command. It's common
|
// Note that we do not want to print the token if it is contained in the command. It's common
|
||||||
// to share errors with others if the tool failed, and we do not want to leak tokens.
|
// to share errors with others if the tool failed, and we do not want to leak tokens.
|
||||||
printFn('Executing: git', this.omitGithubTokenFromMessage(args.join(' ')));
|
printFn('Executing: git', this.omitGithubTokenFromMessage(args.join(' ')));
|
||||||
|
@ -655,6 +669,10 @@ var GitClient = /** @class */ (function () {
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
/** Git URL that resolves to the configured repository. */
|
||||||
|
GitClient.prototype.getRepoGitUrl = function () {
|
||||||
|
return getRepositoryGitUrl(this.remoteConfig, this.githubToken);
|
||||||
|
};
|
||||||
/** Whether the given branch contains the specified SHA. */
|
/** Whether the given branch contains the specified SHA. */
|
||||||
GitClient.prototype.hasCommit = function (branchName, sha) {
|
GitClient.prototype.hasCommit = function (branchName, sha) {
|
||||||
return this.run(['branch', branchName, '--contains', sha]).stdout !== '';
|
return this.run(['branch', branchName, '--contains', sha]).stdout !== '';
|
||||||
|
@ -760,11 +778,40 @@ var GitClient = /** @class */ (function () {
|
||||||
return scopes.split(',').map(function (scope) { return scope.trim(); });
|
return scopes.split(',').map(function (scope) { return scope.trim(); });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
/** Whether verbose logging of Git actions should be used. */
|
|
||||||
GitClient.LOG_COMMANDS = true;
|
|
||||||
return GitClient;
|
return GitClient;
|
||||||
}());
|
}());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google LLC All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
/** Sets up the `github-token` command option for the given Yargs instance. */
|
||||||
|
function addGithubTokenOption(yargs) {
|
||||||
|
return yargs
|
||||||
|
// 'github-token' is casted to 'githubToken' to properly set up typings to reflect the key in
|
||||||
|
// the Argv object being camelCase rather than kebob case due to the `camel-case-expansion`
|
||||||
|
// config: https://github.com/yargs/yargs-parser#camel-case-expansion
|
||||||
|
.option('github-token', {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Github token. If not set, token is retrieved from the environment variables.',
|
||||||
|
coerce: function (token) {
|
||||||
|
var githubToken = token || process.env.GITHUB_TOKEN || process.env.TOKEN;
|
||||||
|
if (!githubToken) {
|
||||||
|
error(red('No Github token set. Please set the `GITHUB_TOKEN` environment variable.'));
|
||||||
|
error(red('Alternatively, pass the `--github-token` command line flag.'));
|
||||||
|
error(yellow("You can generate a token here: " + GITHUB_TOKEN_GENERATE_URL));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
GitClient.authenticateWithToken(githubToken);
|
||||||
|
return githubToken;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.default('github-token', '', '<LOCAL TOKEN>');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @license
|
* @license
|
||||||
* Copyright Google LLC All Rights Reserved.
|
* Copyright Google LLC All Rights Reserved.
|
||||||
|
@ -1091,9 +1138,10 @@ function getLtsNpmDistTagOfMajor(major) {
|
||||||
|
|
||||||
/** The BaseModule to extend modules for caretaker checks from. */
|
/** The BaseModule to extend modules for caretaker checks from. */
|
||||||
class BaseModule {
|
class BaseModule {
|
||||||
constructor(git, config) {
|
constructor(config) {
|
||||||
this.git = git;
|
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
/** The singleton instance of the GitClient. */
|
||||||
|
this.git = GitClient.getAuthenticatedInstance();
|
||||||
/** The data for the module. */
|
/** The data for the module. */
|
||||||
this.data = this.retrieveData();
|
this.data = this.retrieveData();
|
||||||
}
|
}
|
||||||
|
@ -1332,7 +1380,7 @@ class GithubQueriesModule extends BaseModule {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
/** The results of the generated github query. */
|
/** The results of the generated github query. */
|
||||||
const queryResult = yield this.git.github.graphql.query(this.buildGraphqlQuery(queries));
|
const queryResult = yield this.git.github.graphql(this.buildGraphqlQuery(queries));
|
||||||
const results = Object.values(queryResult);
|
const results = Object.values(queryResult);
|
||||||
const { owner, name: repo } = this.git.remoteConfig;
|
const { owner, name: repo } = this.git.remoteConfig;
|
||||||
return results.map((result, i) => {
|
return results.map((result, i) => {
|
||||||
|
@ -1348,20 +1396,20 @@ class GithubQueriesModule extends BaseModule {
|
||||||
/** Build a Graphql query statement for the provided queries. */
|
/** Build a Graphql query statement for the provided queries. */
|
||||||
buildGraphqlQuery(queries) {
|
buildGraphqlQuery(queries) {
|
||||||
/** The query object for graphql. */
|
/** The query object for graphql. */
|
||||||
const graphQlQuery = {};
|
const graphqlQuery = {};
|
||||||
const { owner, name: repo } = this.git.remoteConfig;
|
const { owner, name: repo } = this.git.remoteConfig;
|
||||||
/** The Github search filter for the configured repository. */
|
/** The Github search filter for the configured repository. */
|
||||||
const repoFilter = `repo:${owner}/${repo}`;
|
const repoFilter = `repo:${owner}/${repo}`;
|
||||||
queries.forEach(({ name, query }) => {
|
queries.forEach(({ name, query }) => {
|
||||||
/** The name of the query, with spaces removed to match GraphQL requirements. */
|
/** The name of the query, with spaces removed to match Graphql requirements. */
|
||||||
const queryKey = typedGraphqlify.alias(name.replace(/ /g, ''), 'search');
|
const queryKey = typedGraphqlify.alias(name.replace(/ /g, ''), 'search');
|
||||||
graphQlQuery[queryKey] = typedGraphqlify.params({
|
graphqlQuery[queryKey] = typedGraphqlify.params({
|
||||||
type: 'ISSUE',
|
type: 'ISSUE',
|
||||||
first: MAX_RETURNED_ISSUES,
|
first: MAX_RETURNED_ISSUES,
|
||||||
query: `"${repoFilter} ${query.replace(/"/g, '\\"')}"`,
|
query: `"${repoFilter} ${query.replace(/"/g, '\\"')}"`,
|
||||||
}, Object.assign({}, GithubQueryResultFragment));
|
}, Object.assign({}, GithubQueryResultFragment));
|
||||||
});
|
});
|
||||||
return graphQlQuery;
|
return graphqlQuery;
|
||||||
}
|
}
|
||||||
printToTerminal() {
|
printToTerminal() {
|
||||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||||
|
@ -1470,16 +1518,14 @@ const moduleList = [
|
||||||
G3Module,
|
G3Module,
|
||||||
];
|
];
|
||||||
/** Check the status of services which Angular caretakers need to monitor. */
|
/** Check the status of services which Angular caretakers need to monitor. */
|
||||||
function checkServiceStatuses(githubToken) {
|
function checkServiceStatuses() {
|
||||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||||
|
// Set the verbose logging state of the GitClient.
|
||||||
|
GitClient.getAuthenticatedInstance().setVerboseLoggingState(false);
|
||||||
/** The configuration for the caretaker commands. */
|
/** The configuration for the caretaker commands. */
|
||||||
const config = getCaretakerConfig();
|
const config = getCaretakerConfig();
|
||||||
/** The GitClient for interacting with git and Github. */
|
|
||||||
const git = new GitClient(githubToken, config);
|
|
||||||
// Prevent logging of the git commands being executed during the check.
|
|
||||||
GitClient.LOG_COMMANDS = false;
|
|
||||||
/** List of instances of Caretaker Check modules */
|
/** List of instances of Caretaker Check modules */
|
||||||
const caretakerCheckModules = moduleList.map(module => new module(git, config));
|
const caretakerCheckModules = moduleList.map(module => new module(config));
|
||||||
// Module's `data` is casted as Promise<unknown> because the data types of the `module`'s `data`
|
// Module's `data` is casted as Promise<unknown> because the data types of the `module`'s `data`
|
||||||
// promises do not match typings, however our usage here is only to determine when the promise
|
// promises do not match typings, however our usage here is only to determine when the promise
|
||||||
// resolves.
|
// resolves.
|
||||||
|
@ -1502,9 +1548,9 @@ function builder(yargs) {
|
||||||
return addGithubTokenOption(yargs);
|
return addGithubTokenOption(yargs);
|
||||||
}
|
}
|
||||||
/** Handles the command. */
|
/** Handles the command. */
|
||||||
function handler({ githubToken }) {
|
function handler() {
|
||||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||||
yield checkServiceStatuses(githubToken);
|
yield checkServiceStatuses();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
/** yargs command module for checking status information for the repository */
|
/** yargs command module for checking status information for the repository */
|
||||||
|
@ -2836,8 +2882,8 @@ function getTargetBranchesForPr(prNumber) {
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
/** Repo owner and name for the github repository. */
|
/** Repo owner and name for the github repository. */
|
||||||
const { owner, name: repo } = config.github;
|
const { owner, name: repo } = config.github;
|
||||||
/** The git client to get a Github API service instance. */
|
/** The singleton instance of the GitClient. */
|
||||||
const git = new GitClient(undefined, config);
|
const git = GitClient.getInstance();
|
||||||
/** The validated merge config. */
|
/** The validated merge config. */
|
||||||
const { config: mergeConfig, errors } = yield loadAndValidateConfig(config, git.github);
|
const { config: mergeConfig, errors } = yield loadAndValidateConfig(config, git.github);
|
||||||
if (errors !== undefined) {
|
if (errors !== undefined) {
|
||||||
|
@ -2931,7 +2977,7 @@ function getPr(prSchema, prNumber, git) {
|
||||||
pullRequest: typedGraphqlify.params({ number: '$number' }, prSchema),
|
pullRequest: typedGraphqlify.params({ number: '$number' }, prSchema),
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
return [4 /*yield*/, git.github.graphql.query(PR_QUERY, { number: prNumber, owner: owner, name: name })];
|
return [4 /*yield*/, git.github.graphql(PR_QUERY, { number: prNumber, owner: owner, name: name })];
|
||||||
case 1:
|
case 1:
|
||||||
result = (_b.sent());
|
result = (_b.sent());
|
||||||
return [2 /*return*/, result.repository.pullRequest];
|
return [2 /*return*/, result.repository.pullRequest];
|
||||||
|
@ -2978,7 +3024,7 @@ function getPendingPrs(prSchema, git) {
|
||||||
owner: owner,
|
owner: owner,
|
||||||
name: name,
|
name: name,
|
||||||
};
|
};
|
||||||
return [4 /*yield*/, git.github.graphql.query(PRS_QUERY, params_1)];
|
return [4 /*yield*/, git.github.graphql(PRS_QUERY, params_1)];
|
||||||
case 2:
|
case 2:
|
||||||
results = _b.sent();
|
results = _b.sent();
|
||||||
prs.push.apply(prs, tslib.__spreadArray([], tslib.__read(results.repository.pullRequests.nodes)));
|
prs.push.apply(prs, tslib.__spreadArray([], tslib.__read(results.repository.pullRequests.nodes)));
|
||||||
|
@ -2998,7 +3044,7 @@ function getPendingPrs(prSchema, git) {
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
/* GraphQL schema for the response body for a pending PR. */
|
/* Graphql schema for the response body for a pending PR. */
|
||||||
const PR_SCHEMA = {
|
const PR_SCHEMA = {
|
||||||
state: typedGraphqlify.types.string,
|
state: typedGraphqlify.types.string,
|
||||||
maintainerCanModify: typedGraphqlify.types.boolean,
|
maintainerCanModify: typedGraphqlify.types.boolean,
|
||||||
|
@ -3037,8 +3083,8 @@ class MaintainerModifyAccessError extends Error {
|
||||||
*/
|
*/
|
||||||
function checkOutPullRequestLocally(prNumber, githubToken, opts = {}) {
|
function checkOutPullRequestLocally(prNumber, githubToken, opts = {}) {
|
||||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||||
/** Authenticated Git client for git and Github interactions. */
|
/** The singleton instance of the GitClient. */
|
||||||
const git = new GitClient(githubToken);
|
const git = GitClient.getAuthenticatedInstance();
|
||||||
// In order to preserve local changes, checkouts cannot occur if local changes are present in the
|
// In order to preserve local changes, checkouts cannot occur if local changes are present in the
|
||||||
// git environment. Checked before retrieving the PR to fail fast.
|
// git environment. Checked before retrieving the PR to fail fast.
|
||||||
if (git.hasLocalChanges()) {
|
if (git.hasLocalChanges()) {
|
||||||
|
@ -3132,7 +3178,7 @@ const CheckoutCommandModule = {
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
/* GraphQL schema for the response body for each pending PR. */
|
/* Graphql schema for the response body for each pending PR. */
|
||||||
const PR_SCHEMA$1 = {
|
const PR_SCHEMA$1 = {
|
||||||
headRef: {
|
headRef: {
|
||||||
name: typedGraphqlify.types.string,
|
name: typedGraphqlify.types.string,
|
||||||
|
@ -3160,9 +3206,10 @@ function processPr(pr) {
|
||||||
/** Name of a temporary local branch that is used for checking conflicts. **/
|
/** Name of a temporary local branch that is used for checking conflicts. **/
|
||||||
const tempWorkingBranch = '__NgDevRepoBaseAfterChange__';
|
const tempWorkingBranch = '__NgDevRepoBaseAfterChange__';
|
||||||
/** Checks if the provided PR will cause new conflicts in other pending PRs. */
|
/** Checks if the provided PR will cause new conflicts in other pending PRs. */
|
||||||
function discoverNewConflictsForPr(newPrNumber, updatedAfter, config = getConfig()) {
|
function discoverNewConflictsForPr(newPrNumber, updatedAfter) {
|
||||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||||
const git = new GitClient();
|
/** The singleton instance of the GitClient. */
|
||||||
|
const git = GitClient.getAuthenticatedInstance();
|
||||||
// If there are any local changes in the current repository state, the
|
// If there are any local changes in the current repository state, the
|
||||||
// check cannot run as it needs to move between branches.
|
// check cannot run as it needs to move between branches.
|
||||||
if (git.hasLocalChanges()) {
|
if (git.hasLocalChanges()) {
|
||||||
|
@ -3486,7 +3533,7 @@ function loadAndValidatePullRequest(_a, prNumber, ignoreNonFatalFailures) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
/* GraphQL schema for the response body the requested pull request. */
|
/* Graphql schema for the response body the requested pull request. */
|
||||||
var PR_SCHEMA$2 = {
|
var PR_SCHEMA$2 = {
|
||||||
url: typedGraphqlify.types.string,
|
url: typedGraphqlify.types.string,
|
||||||
number: typedGraphqlify.types.number,
|
number: typedGraphqlify.types.number,
|
||||||
|
@ -3723,7 +3770,7 @@ var MergeStrategy = /** @class */ (function () {
|
||||||
});
|
});
|
||||||
// Fetch all target branches with a single command. We don't want to fetch them
|
// Fetch all target branches with a single command. We don't want to fetch them
|
||||||
// individually as that could cause an unnecessary slow-down.
|
// individually as that could cause an unnecessary slow-down.
|
||||||
this.git.run(tslib.__spreadArray(tslib.__spreadArray(['fetch', '-q', '-f', this.git.repoGitUrl], tslib.__read(fetchRefspecs)), tslib.__read(extraRefspecs)));
|
this.git.run(tslib.__spreadArray(tslib.__spreadArray(['fetch', '-q', '-f', this.git.getRepoGitUrl()], tslib.__read(fetchRefspecs)), tslib.__read(extraRefspecs)));
|
||||||
};
|
};
|
||||||
/** Pushes the given target branches upstream. */
|
/** Pushes the given target branches upstream. */
|
||||||
MergeStrategy.prototype.pushTargetBranchesUpstream = function (names) {
|
MergeStrategy.prototype.pushTargetBranchesUpstream = function (names) {
|
||||||
|
@ -3734,7 +3781,7 @@ var MergeStrategy = /** @class */ (function () {
|
||||||
});
|
});
|
||||||
// Push all target branches with a single command if we don't run in dry-run mode.
|
// Push all target branches with a single command if we don't run in dry-run mode.
|
||||||
// We don't want to push them individually as that could cause an unnecessary slow-down.
|
// We don't want to push them individually as that could cause an unnecessary slow-down.
|
||||||
this.git.run(tslib.__spreadArray(['push', this.git.repoGitUrl], tslib.__read(pushRefspecs)));
|
this.git.run(tslib.__spreadArray(['push', this.git.getRepoGitUrl()], tslib.__read(pushRefspecs)));
|
||||||
};
|
};
|
||||||
return MergeStrategy;
|
return MergeStrategy;
|
||||||
}());
|
}());
|
||||||
|
@ -4206,7 +4253,7 @@ var PullRequestMergeTask = /** @class */ (function () {
|
||||||
* @param projectRoot Path to the local Git project that is used for merging.
|
* @param projectRoot Path to the local Git project that is used for merging.
|
||||||
* @param config Configuration for merging pull requests.
|
* @param config Configuration for merging pull requests.
|
||||||
*/
|
*/
|
||||||
function mergePullRequest(prNumber, githubToken, flags) {
|
function mergePullRequest(prNumber, flags) {
|
||||||
return tslib.__awaiter(this, void 0, void 0, function () {
|
return tslib.__awaiter(this, void 0, void 0, function () {
|
||||||
/** Performs the merge and returns whether it was successful or not. */
|
/** Performs the merge and returns whether it was successful or not. */
|
||||||
function performMerge(ignoreFatalErrors) {
|
function performMerge(ignoreFatalErrors) {
|
||||||
|
@ -4322,7 +4369,7 @@ function mergePullRequest(prNumber, githubToken, flags) {
|
||||||
// Set the environment variable to skip all git commit hooks triggered by husky. We are unable to
|
// Set the environment variable to skip all git commit hooks triggered by husky. We are unable to
|
||||||
// rely on `--no-verify` as some hooks still run, notably the `prepare-commit-msg` hook.
|
// rely on `--no-verify` as some hooks still run, notably the `prepare-commit-msg` hook.
|
||||||
process.env['HUSKY'] = '0';
|
process.env['HUSKY'] = '0';
|
||||||
return [4 /*yield*/, createPullRequestMergeTask(githubToken, flags)];
|
return [4 /*yield*/, createPullRequestMergeTask(flags)];
|
||||||
case 1:
|
case 1:
|
||||||
api = _a.sent();
|
api = _a.sent();
|
||||||
return [4 /*yield*/, performMerge(false)];
|
return [4 /*yield*/, performMerge(false)];
|
||||||
|
@ -4343,15 +4390,14 @@ function mergePullRequest(prNumber, githubToken, flags) {
|
||||||
* and optional explicit configuration. An explicit configuration can be specified
|
* and optional explicit configuration. An explicit configuration can be specified
|
||||||
* when the merge script is used outside of a `ng-dev` configured repository.
|
* when the merge script is used outside of a `ng-dev` configured repository.
|
||||||
*/
|
*/
|
||||||
function createPullRequestMergeTask(githubToken, flags) {
|
function createPullRequestMergeTask(flags) {
|
||||||
return tslib.__awaiter(this, void 0, void 0, function () {
|
return tslib.__awaiter(this, void 0, void 0, function () {
|
||||||
var projectRoot, devInfraConfig, git, _a, config, errors;
|
var devInfraConfig, git, _a, config, errors;
|
||||||
return tslib.__generator(this, function (_b) {
|
return tslib.__generator(this, function (_b) {
|
||||||
switch (_b.label) {
|
switch (_b.label) {
|
||||||
case 0:
|
case 0:
|
||||||
projectRoot = getRepoBaseDir();
|
|
||||||
devInfraConfig = getConfig();
|
devInfraConfig = getConfig();
|
||||||
git = new GitClient(githubToken, devInfraConfig, projectRoot);
|
git = GitClient.getAuthenticatedInstance();
|
||||||
return [4 /*yield*/, loadAndValidateConfig(devInfraConfig, git.github)];
|
return [4 /*yield*/, loadAndValidateConfig(devInfraConfig, git.github)];
|
||||||
case 1:
|
case 1:
|
||||||
_a = _b.sent(), config = _a.config, errors = _a.errors;
|
_a = _b.sent(), config = _a.config, errors = _a.errors;
|
||||||
|
@ -4396,11 +4442,11 @@ function builder$6(yargs) {
|
||||||
}
|
}
|
||||||
/** Handles the command. */
|
/** Handles the command. */
|
||||||
function handler$6(_a) {
|
function handler$6(_a) {
|
||||||
var pr = _a.pr, githubToken = _a.githubToken, branchPrompt = _a.branchPrompt;
|
var pr = _a.pr, branchPrompt = _a.branchPrompt;
|
||||||
return tslib.__awaiter(this, void 0, void 0, function () {
|
return tslib.__awaiter(this, void 0, void 0, function () {
|
||||||
return tslib.__generator(this, function (_b) {
|
return tslib.__generator(this, function (_b) {
|
||||||
switch (_b.label) {
|
switch (_b.label) {
|
||||||
case 0: return [4 /*yield*/, mergePullRequest(pr, githubToken, { branchPrompt: branchPrompt })];
|
case 0: return [4 /*yield*/, mergePullRequest(pr, { branchPrompt: branchPrompt })];
|
||||||
case 1:
|
case 1:
|
||||||
_b.sent();
|
_b.sent();
|
||||||
return [2 /*return*/];
|
return [2 /*return*/];
|
||||||
|
@ -4423,7 +4469,7 @@ var MergeCommandModule = {
|
||||||
* Use of this source code is governed by an MIT-style license that can be
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
/* GraphQL schema for the response body for each pending PR. */
|
/* Graphql schema for the response body for each pending PR. */
|
||||||
const PR_SCHEMA$3 = {
|
const PR_SCHEMA$3 = {
|
||||||
state: typedGraphqlify.types.string,
|
state: typedGraphqlify.types.string,
|
||||||
maintainerCanModify: typedGraphqlify.types.boolean,
|
maintainerCanModify: typedGraphqlify.types.boolean,
|
||||||
|
@ -4450,7 +4496,8 @@ const PR_SCHEMA$3 = {
|
||||||
*/
|
*/
|
||||||
function rebasePr(prNumber, githubToken, config = getConfig()) {
|
function rebasePr(prNumber, githubToken, config = getConfig()) {
|
||||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||||
const git = new GitClient(githubToken);
|
/** The singleton instance of the GitClient. */
|
||||||
|
const git = GitClient.getAuthenticatedInstance();
|
||||||
// TODO: Rely on a common assertNoLocalChanges function.
|
// TODO: Rely on a common assertNoLocalChanges function.
|
||||||
if (git.hasLocalChanges()) {
|
if (git.hasLocalChanges()) {
|
||||||
error('Cannot perform rebase of PR with local changes.');
|
error('Cannot perform rebase of PR with local changes.');
|
||||||
|
@ -5747,7 +5794,7 @@ class ReleaseAction {
|
||||||
return this._cachedForkRepo;
|
return this._cachedForkRepo;
|
||||||
}
|
}
|
||||||
const { owner, name } = this.git.remoteConfig;
|
const { owner, name } = this.git.remoteConfig;
|
||||||
const result = yield this.git.github.graphql.query(findOwnedForksOfRepoQuery, { owner, name });
|
const result = yield this.git.github.graphql(findOwnedForksOfRepoQuery, { owner, name });
|
||||||
const forks = result.repository.forks.nodes;
|
const forks = result.repository.forks.nodes;
|
||||||
if (forks.length === 0) {
|
if (forks.length === 0) {
|
||||||
error(red(' ✘ Unable to find fork for currently authenticated user.'));
|
error(red(' ✘ Unable to find fork for currently authenticated user.'));
|
||||||
|
@ -5800,7 +5847,7 @@ class ReleaseAction {
|
||||||
pushHeadToRemoteBranch(branchName) {
|
pushHeadToRemoteBranch(branchName) {
|
||||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||||
// Push the local `HEAD` to the remote branch in the configured project.
|
// Push the local `HEAD` to the remote branch in the configured project.
|
||||||
this.git.run(['push', this.git.repoGitUrl, `HEAD:refs/heads/${branchName}`]);
|
this.git.run(['push', this.git.getRepoGitUrl(), `HEAD:refs/heads/${branchName}`]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
@ -5915,7 +5962,7 @@ class ReleaseAction {
|
||||||
/** Checks out an upstream branch with a detached head. */
|
/** Checks out an upstream branch with a detached head. */
|
||||||
checkoutUpstreamBranch(branchName) {
|
checkoutUpstreamBranch(branchName) {
|
||||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||||
this.git.run(['fetch', '-q', this.git.repoGitUrl, branchName]);
|
this.git.run(['fetch', '-q', this.git.getRepoGitUrl(), branchName]);
|
||||||
this.git.run(['checkout', 'FETCH_HEAD', '--detach']);
|
this.git.run(['checkout', 'FETCH_HEAD', '--detach']);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -6551,8 +6598,8 @@ class ReleaseTool {
|
||||||
this._github = _github;
|
this._github = _github;
|
||||||
this._githubToken = _githubToken;
|
this._githubToken = _githubToken;
|
||||||
this._projectRoot = _projectRoot;
|
this._projectRoot = _projectRoot;
|
||||||
/** Client for interacting with the Github API and the local Git command. */
|
/** The singleton instance of the GitClient. */
|
||||||
this._git = new GitClient(this._githubToken, { github: this._github }, this._projectRoot);
|
this._git = GitClient.getAuthenticatedInstance();
|
||||||
/** The previous git commit to return back to after the release tool runs. */
|
/** The previous git commit to return back to after the release tool runs. */
|
||||||
this.previousGitBranchOrRevision = this._git.getCurrentBranchOrRevision();
|
this.previousGitBranchOrRevision = this._git.getCurrentBranchOrRevision();
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,8 @@ export async function getTargetBranchesForPr(prNumber: number) {
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
/** Repo owner and name for the github repository. */
|
/** Repo owner and name for the github repository. */
|
||||||
const {owner, name: repo} = config.github;
|
const {owner, name: repo} = config.github;
|
||||||
/** The git client to get a Github API service instance. */
|
/** The singleton instance of the GitClient. */
|
||||||
const git = new GitClient(undefined, config);
|
const git = GitClient.getInstance();
|
||||||
/** The validated merge config. */
|
/** The validated merge config. */
|
||||||
const {config: mergeConfig, errors} = await loadAndValidateConfig(config, git.github);
|
const {config: mergeConfig, errors} = await loadAndValidateConfig(config, git.github);
|
||||||
if (errors !== undefined) {
|
if (errors !== undefined) {
|
||||||
|
|
|
@ -6,31 +6,31 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {types as graphQLTypes} from 'typed-graphqlify';
|
import {types as graphqlTypes} from 'typed-graphqlify';
|
||||||
|
|
||||||
import {info} from '../../utils/console';
|
import {info} from '../../utils/console';
|
||||||
import {addTokenToGitHttpsUrl} from '../../utils/git/github-urls';
|
import {addTokenToGitHttpsUrl} from '../../utils/git/github-urls';
|
||||||
import {GitClient} from '../../utils/git/index';
|
import {GitClient} from '../../utils/git/index';
|
||||||
import {getPr} from '../../utils/github';
|
import {getPr} from '../../utils/github';
|
||||||
|
|
||||||
/* GraphQL schema for the response body for a pending PR. */
|
/* Graphql schema for the response body for a pending PR. */
|
||||||
const PR_SCHEMA = {
|
const PR_SCHEMA = {
|
||||||
state: graphQLTypes.string,
|
state: graphqlTypes.string,
|
||||||
maintainerCanModify: graphQLTypes.boolean,
|
maintainerCanModify: graphqlTypes.boolean,
|
||||||
viewerDidAuthor: graphQLTypes.boolean,
|
viewerDidAuthor: graphqlTypes.boolean,
|
||||||
headRefOid: graphQLTypes.string,
|
headRefOid: graphqlTypes.string,
|
||||||
headRef: {
|
headRef: {
|
||||||
name: graphQLTypes.string,
|
name: graphqlTypes.string,
|
||||||
repository: {
|
repository: {
|
||||||
url: graphQLTypes.string,
|
url: graphqlTypes.string,
|
||||||
nameWithOwner: graphQLTypes.string,
|
nameWithOwner: graphqlTypes.string,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
baseRef: {
|
baseRef: {
|
||||||
name: graphQLTypes.string,
|
name: graphqlTypes.string,
|
||||||
repository: {
|
repository: {
|
||||||
url: graphQLTypes.string,
|
url: graphqlTypes.string,
|
||||||
nameWithOwner: graphQLTypes.string,
|
nameWithOwner: graphqlTypes.string,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -62,8 +62,8 @@ export interface PullRequestCheckoutOptions {
|
||||||
*/
|
*/
|
||||||
export async function checkOutPullRequestLocally(
|
export async function checkOutPullRequestLocally(
|
||||||
prNumber: number, githubToken: string, opts: PullRequestCheckoutOptions = {}) {
|
prNumber: number, githubToken: string, opts: PullRequestCheckoutOptions = {}) {
|
||||||
/** Authenticated Git client for git and Github interactions. */
|
/** The singleton instance of the GitClient. */
|
||||||
const git = new GitClient(githubToken);
|
const git = GitClient.getAuthenticatedInstance();
|
||||||
|
|
||||||
// In order to preserve local changes, checkouts cannot occur if local changes are present in the
|
// In order to preserve local changes, checkouts cannot occur if local changes are present in the
|
||||||
// git environment. Checked before retrieving the PR to fail fast.
|
// git environment. Checked before retrieving the PR to fail fast.
|
||||||
|
|
|
@ -7,38 +7,37 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Bar} from 'cli-progress';
|
import {Bar} from 'cli-progress';
|
||||||
import {types as graphQLTypes} from 'typed-graphqlify';
|
import {types as graphqlTypes} from 'typed-graphqlify';
|
||||||
|
|
||||||
import {getConfig, NgDevConfig} from '../../utils/config';
|
|
||||||
import {error, info} from '../../utils/console';
|
import {error, info} from '../../utils/console';
|
||||||
import {GitClient} from '../../utils/git/index';
|
import {GitClient} from '../../utils/git/index';
|
||||||
import {getPendingPrs} from '../../utils/github';
|
import {getPendingPrs} from '../../utils/github';
|
||||||
import {exec} from '../../utils/shelljs';
|
import {exec} from '../../utils/shelljs';
|
||||||
|
|
||||||
|
|
||||||
/* GraphQL schema for the response body for each pending PR. */
|
/* Graphql schema for the response body for each pending PR. */
|
||||||
const PR_SCHEMA = {
|
const PR_SCHEMA = {
|
||||||
headRef: {
|
headRef: {
|
||||||
name: graphQLTypes.string,
|
name: graphqlTypes.string,
|
||||||
repository: {
|
repository: {
|
||||||
url: graphQLTypes.string,
|
url: graphqlTypes.string,
|
||||||
nameWithOwner: graphQLTypes.string,
|
nameWithOwner: graphqlTypes.string,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
baseRef: {
|
baseRef: {
|
||||||
name: graphQLTypes.string,
|
name: graphqlTypes.string,
|
||||||
repository: {
|
repository: {
|
||||||
url: graphQLTypes.string,
|
url: graphqlTypes.string,
|
||||||
nameWithOwner: graphQLTypes.string,
|
nameWithOwner: graphqlTypes.string,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
updatedAt: graphQLTypes.string,
|
updatedAt: graphqlTypes.string,
|
||||||
number: graphQLTypes.number,
|
number: graphqlTypes.number,
|
||||||
mergeable: graphQLTypes.string,
|
mergeable: graphqlTypes.string,
|
||||||
title: graphQLTypes.string,
|
title: graphqlTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Pull Request response from Github GraphQL query */
|
/* Pull Request response from Github Graphql query */
|
||||||
type RawPullRequest = typeof PR_SCHEMA;
|
type RawPullRequest = typeof PR_SCHEMA;
|
||||||
|
|
||||||
/** Convert raw Pull Request response from Github to usable Pull Request object. */
|
/** Convert raw Pull Request response from Github to usable Pull Request object. */
|
||||||
|
@ -53,9 +52,9 @@ type PullRequest = ReturnType<typeof processPr>;
|
||||||
const tempWorkingBranch = '__NgDevRepoBaseAfterChange__';
|
const tempWorkingBranch = '__NgDevRepoBaseAfterChange__';
|
||||||
|
|
||||||
/** Checks if the provided PR will cause new conflicts in other pending PRs. */
|
/** Checks if the provided PR will cause new conflicts in other pending PRs. */
|
||||||
export async function discoverNewConflictsForPr(
|
export async function discoverNewConflictsForPr(newPrNumber: number, updatedAfter: number) {
|
||||||
newPrNumber: number, updatedAfter: number, config: Pick<NgDevConfig, 'github'> = getConfig()) {
|
/** The singleton instance of the GitClient. */
|
||||||
const git = new GitClient();
|
const git = GitClient.getAuthenticatedInstance();
|
||||||
// If there are any local changes in the current repository state, the
|
// If there are any local changes in the current repository state, the
|
||||||
// check cannot run as it needs to move between branches.
|
// check cannot run as it needs to move between branches.
|
||||||
if (git.hasLocalChanges()) {
|
if (git.hasLocalChanges()) {
|
||||||
|
|
|
@ -37,8 +37,8 @@ function builder(yargs: Argv) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Handles the command. */
|
/** Handles the command. */
|
||||||
async function handler({pr, githubToken, branchPrompt}: Arguments<MergeCommandOptions>) {
|
async function handler({pr, branchPrompt}: Arguments<MergeCommandOptions>) {
|
||||||
await mergePullRequest(pr, githubToken, {branchPrompt});
|
await mergePullRequest(pr, {branchPrompt});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** yargs command module describing the command. */
|
/** yargs command module describing the command. */
|
||||||
|
|
|
@ -29,13 +29,12 @@ import {MergeResult, MergeStatus, PullRequestMergeTask, PullRequestMergeTaskFlag
|
||||||
* @param projectRoot Path to the local Git project that is used for merging.
|
* @param projectRoot Path to the local Git project that is used for merging.
|
||||||
* @param config Configuration for merging pull requests.
|
* @param config Configuration for merging pull requests.
|
||||||
*/
|
*/
|
||||||
export async function mergePullRequest(
|
export async function mergePullRequest(prNumber: number, flags: PullRequestMergeTaskFlags) {
|
||||||
prNumber: number, githubToken: string, flags: PullRequestMergeTaskFlags) {
|
|
||||||
// Set the environment variable to skip all git commit hooks triggered by husky. We are unable to
|
// Set the environment variable to skip all git commit hooks triggered by husky. We are unable to
|
||||||
// rely on `--no-verify` as some hooks still run, notably the `prepare-commit-msg` hook.
|
// rely on `--no-verify` as some hooks still run, notably the `prepare-commit-msg` hook.
|
||||||
process.env['HUSKY'] = '0';
|
process.env['HUSKY'] = '0';
|
||||||
|
|
||||||
const api = await createPullRequestMergeTask(githubToken, flags);
|
const api = await createPullRequestMergeTask(flags);
|
||||||
|
|
||||||
// Perform the merge. Force mode can be activated through a command line flag.
|
// Perform the merge. Force mode can be activated through a command line flag.
|
||||||
// Alternatively, if the merge fails with non-fatal failures, the script
|
// Alternatively, if the merge fails with non-fatal failures, the script
|
||||||
|
@ -127,10 +126,10 @@ export async function mergePullRequest(
|
||||||
* and optional explicit configuration. An explicit configuration can be specified
|
* and optional explicit configuration. An explicit configuration can be specified
|
||||||
* when the merge script is used outside of a `ng-dev` configured repository.
|
* when the merge script is used outside of a `ng-dev` configured repository.
|
||||||
*/
|
*/
|
||||||
async function createPullRequestMergeTask(githubToken: string, flags: PullRequestMergeTaskFlags) {
|
async function createPullRequestMergeTask(flags: PullRequestMergeTaskFlags) {
|
||||||
const projectRoot = getRepoBaseDir();
|
|
||||||
const devInfraConfig = getConfig();
|
const devInfraConfig = getConfig();
|
||||||
const git = new GitClient(githubToken, devInfraConfig, projectRoot);
|
/** The singleton instance of the GitClient. */
|
||||||
|
const git = GitClient.getAuthenticatedInstance();
|
||||||
const {config, errors} = await loadAndValidateConfig(devInfraConfig, git.github);
|
const {config, errors} = await loadAndValidateConfig(devInfraConfig, git.github);
|
||||||
|
|
||||||
if (errors) {
|
if (errors) {
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {params, types as graphQLTypes} from 'typed-graphqlify';
|
import {params, types as graphqlTypes} from 'typed-graphqlify';
|
||||||
import {Commit, parseCommitMessage} from '../../commit-message/parse';
|
import {Commit, parseCommitMessage} from '../../commit-message/parse';
|
||||||
import {red, warn} from '../../utils/console';
|
import {red, warn} from '../../utils/console';
|
||||||
|
|
||||||
|
@ -133,28 +133,28 @@ export async function loadAndValidatePullRequest(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* GraphQL schema for the response body the requested pull request. */
|
/* Graphql schema for the response body the requested pull request. */
|
||||||
const PR_SCHEMA = {
|
const PR_SCHEMA = {
|
||||||
url: graphQLTypes.string,
|
url: graphqlTypes.string,
|
||||||
number: graphQLTypes.number,
|
number: graphqlTypes.number,
|
||||||
// Only the last 100 commits from a pull request are obtained as we likely will never see a pull
|
// Only the last 100 commits from a pull request are obtained as we likely will never see a pull
|
||||||
// requests with more than 100 commits.
|
// requests with more than 100 commits.
|
||||||
commits: params({last: 100}, {
|
commits: params({last: 100}, {
|
||||||
totalCount: graphQLTypes.number,
|
totalCount: graphqlTypes.number,
|
||||||
nodes: [{
|
nodes: [{
|
||||||
commit: {
|
commit: {
|
||||||
status: {
|
status: {
|
||||||
state: graphQLTypes.oneOf(['FAILURE', 'PENDING', 'SUCCESS'] as const),
|
state: graphqlTypes.oneOf(['FAILURE', 'PENDING', 'SUCCESS'] as const),
|
||||||
},
|
},
|
||||||
message: graphQLTypes.string,
|
message: graphqlTypes.string,
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
}),
|
}),
|
||||||
baseRefName: graphQLTypes.string,
|
baseRefName: graphqlTypes.string,
|
||||||
title: graphQLTypes.string,
|
title: graphqlTypes.string,
|
||||||
labels: params({first: 100}, {
|
labels: params({first: 100}, {
|
||||||
nodes: [{
|
nodes: [{
|
||||||
name: graphQLTypes.string,
|
name: graphqlTypes.string,
|
||||||
}]
|
}]
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
@ -163,7 +163,7 @@ const PR_SCHEMA = {
|
||||||
|
|
||||||
/** Fetches a pull request from Github. Returns null if an error occurred. */
|
/** Fetches a pull request from Github. Returns null if an error occurred. */
|
||||||
async function fetchPullRequestFromGithub(
|
async function fetchPullRequestFromGithub(
|
||||||
git: GitClient, prNumber: number): Promise<typeof PR_SCHEMA|null> {
|
git: GitClient<true>, prNumber: number): Promise<typeof PR_SCHEMA|null> {
|
||||||
try {
|
try {
|
||||||
const x = await getPr(PR_SCHEMA, prNumber, git);
|
const x = await getPr(PR_SCHEMA, prNumber, git);
|
||||||
return x;
|
return x;
|
||||||
|
|
|
@ -37,7 +37,7 @@ const COMMIT_HEADER_SEPARATOR = '\n\n';
|
||||||
* is properly set, but a notable downside is that PRs cannot use fixup or squash commits.
|
* is properly set, but a notable downside is that PRs cannot use fixup or squash commits.
|
||||||
*/
|
*/
|
||||||
export class GithubApiMergeStrategy extends MergeStrategy {
|
export class GithubApiMergeStrategy extends MergeStrategy {
|
||||||
constructor(git: GitClient, private _config: GithubApiMergeStrategyConfig) {
|
constructor(git: GitClient<true>, private _config: GithubApiMergeStrategyConfig) {
|
||||||
super(git);
|
super(git);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ export const TEMP_PR_HEAD_BRANCH = 'merge_pr_head';
|
||||||
* merges it into the determined target branches.
|
* merges it into the determined target branches.
|
||||||
*/
|
*/
|
||||||
export abstract class MergeStrategy {
|
export abstract class MergeStrategy {
|
||||||
constructor(protected git: GitClient) {}
|
constructor(protected git: GitClient<true>) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepares a merge of the given pull request. The strategy by default will
|
* Prepares a merge of the given pull request. The strategy by default will
|
||||||
|
@ -124,7 +124,8 @@ export abstract class MergeStrategy {
|
||||||
});
|
});
|
||||||
// Fetch all target branches with a single command. We don't want to fetch them
|
// Fetch all target branches with a single command. We don't want to fetch them
|
||||||
// individually as that could cause an unnecessary slow-down.
|
// individually as that could cause an unnecessary slow-down.
|
||||||
this.git.run(['fetch', '-q', '-f', this.git.repoGitUrl, ...fetchRefspecs, ...extraRefspecs]);
|
this.git.run(
|
||||||
|
['fetch', '-q', '-f', this.git.getRepoGitUrl(), ...fetchRefspecs, ...extraRefspecs]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Pushes the given target branches upstream. */
|
/** Pushes the given target branches upstream. */
|
||||||
|
@ -135,6 +136,6 @@ export abstract class MergeStrategy {
|
||||||
});
|
});
|
||||||
// Push all target branches with a single command if we don't run in dry-run mode.
|
// Push all target branches with a single command if we don't run in dry-run mode.
|
||||||
// We don't want to push them individually as that could cause an unnecessary slow-down.
|
// We don't want to push them individually as that could cause an unnecessary slow-down.
|
||||||
this.git.run(['push', this.git.repoGitUrl, ...pushRefspecs]);
|
this.git.run(['push', this.git.getRepoGitUrl(), ...pushRefspecs]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,7 @@ export class PullRequestMergeTask {
|
||||||
private flags: PullRequestMergeTaskFlags;
|
private flags: PullRequestMergeTaskFlags;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public config: MergeConfigWithRemote, public git: GitClient,
|
public config: MergeConfigWithRemote, public git: GitClient<true>,
|
||||||
flags: Partial<PullRequestMergeTaskFlags>) {
|
flags: Partial<PullRequestMergeTaskFlags>) {
|
||||||
// Update flags property with the provided flags values as patches to the default flag values.
|
// Update flags property with the provided flags values as patches to the default flag values.
|
||||||
this.flags = {...defaultPullRequestMergeTaskFlags, ...flags};
|
this.flags = {...defaultPullRequestMergeTaskFlags, ...flags};
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {types as graphQLTypes} from 'typed-graphqlify';
|
import {types as graphqlTypes} from 'typed-graphqlify';
|
||||||
|
|
||||||
import {Commit} from '../../commit-message/parse';
|
import {Commit} from '../../commit-message/parse';
|
||||||
import {getCommitsInRange} from '../../commit-message/utils';
|
import {getCommitsInRange} from '../../commit-message/utils';
|
||||||
|
@ -16,24 +16,24 @@ import {addTokenToGitHttpsUrl} from '../../utils/git/github-urls';
|
||||||
import {GitClient} from '../../utils/git/index';
|
import {GitClient} from '../../utils/git/index';
|
||||||
import {getPr} from '../../utils/github';
|
import {getPr} from '../../utils/github';
|
||||||
|
|
||||||
/* GraphQL schema for the response body for each pending PR. */
|
/* Graphql schema for the response body for each pending PR. */
|
||||||
const PR_SCHEMA = {
|
const PR_SCHEMA = {
|
||||||
state: graphQLTypes.string,
|
state: graphqlTypes.string,
|
||||||
maintainerCanModify: graphQLTypes.boolean,
|
maintainerCanModify: graphqlTypes.boolean,
|
||||||
viewerDidAuthor: graphQLTypes.boolean,
|
viewerDidAuthor: graphqlTypes.boolean,
|
||||||
headRefOid: graphQLTypes.string,
|
headRefOid: graphqlTypes.string,
|
||||||
headRef: {
|
headRef: {
|
||||||
name: graphQLTypes.string,
|
name: graphqlTypes.string,
|
||||||
repository: {
|
repository: {
|
||||||
url: graphQLTypes.string,
|
url: graphqlTypes.string,
|
||||||
nameWithOwner: graphQLTypes.string,
|
nameWithOwner: graphqlTypes.string,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
baseRef: {
|
baseRef: {
|
||||||
name: graphQLTypes.string,
|
name: graphqlTypes.string,
|
||||||
repository: {
|
repository: {
|
||||||
url: graphQLTypes.string,
|
url: graphqlTypes.string,
|
||||||
nameWithOwner: graphQLTypes.string,
|
nameWithOwner: graphqlTypes.string,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -44,7 +44,8 @@ const PR_SCHEMA = {
|
||||||
*/
|
*/
|
||||||
export async function rebasePr(
|
export async function rebasePr(
|
||||||
prNumber: number, githubToken: string, config: Pick<NgDevConfig, 'github'> = getConfig()) {
|
prNumber: number, githubToken: string, config: Pick<NgDevConfig, 'github'> = getConfig()) {
|
||||||
const git = new GitClient(githubToken);
|
/** The singleton instance of the GitClient. */
|
||||||
|
const git = GitClient.getAuthenticatedInstance();
|
||||||
// TODO: Rely on a common assertNoLocalChanges function.
|
// TODO: Rely on a common assertNoLocalChanges function.
|
||||||
if (git.hasLocalChanges()) {
|
if (git.hasLocalChanges()) {
|
||||||
error('Cannot perform rebase of PR with local changes.');
|
error('Cannot perform rebase of PR with local changes.');
|
||||||
|
|
|
@ -49,7 +49,7 @@ export interface ReleaseActionConstructor<T extends ReleaseAction = ReleaseActio
|
||||||
/** Whether the release action is currently active. */
|
/** Whether the release action is currently active. */
|
||||||
isActive(active: ActiveReleaseTrains): Promise<boolean>;
|
isActive(active: ActiveReleaseTrains): Promise<boolean>;
|
||||||
/** Constructs a release action. */
|
/** Constructs a release action. */
|
||||||
new(...args: [ActiveReleaseTrains, GitClient, ReleaseConfig, string]): T;
|
new(...args: [ActiveReleaseTrains, GitClient<true>, ReleaseConfig, string]): T;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -76,7 +76,7 @@ export abstract class ReleaseAction {
|
||||||
private _cachedForkRepo: GithubRepo|null = null;
|
private _cachedForkRepo: GithubRepo|null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected active: ActiveReleaseTrains, protected git: GitClient,
|
protected active: ActiveReleaseTrains, protected git: GitClient<true>,
|
||||||
protected config: ReleaseConfig, protected projectDir: string) {}
|
protected config: ReleaseConfig, protected projectDir: string) {}
|
||||||
|
|
||||||
/** Updates the version in the project top-level `package.json` file. */
|
/** Updates the version in the project top-level `package.json` file. */
|
||||||
|
@ -182,7 +182,7 @@ export abstract class ReleaseAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
const {owner, name} = this.git.remoteConfig;
|
const {owner, name} = this.git.remoteConfig;
|
||||||
const result = await this.git.github.graphql.query(findOwnedForksOfRepoQuery, {owner, name});
|
const result = await this.git.github.graphql(findOwnedForksOfRepoQuery, {owner, name});
|
||||||
const forks = result.repository.forks.nodes;
|
const forks = result.repository.forks.nodes;
|
||||||
|
|
||||||
if (forks.length === 0) {
|
if (forks.length === 0) {
|
||||||
|
@ -232,7 +232,7 @@ export abstract class ReleaseAction {
|
||||||
/** Pushes the current Git `HEAD` to the given remote branch in the configured project. */
|
/** Pushes the current Git `HEAD` to the given remote branch in the configured project. */
|
||||||
protected async pushHeadToRemoteBranch(branchName: string) {
|
protected async pushHeadToRemoteBranch(branchName: string) {
|
||||||
// Push the local `HEAD` to the remote branch in the configured project.
|
// Push the local `HEAD` to the remote branch in the configured project.
|
||||||
this.git.run(['push', this.git.repoGitUrl, `HEAD:refs/heads/${branchName}`]);
|
this.git.run(['push', this.git.getRepoGitUrl(), `HEAD:refs/heads/${branchName}`]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -360,7 +360,7 @@ export abstract class ReleaseAction {
|
||||||
|
|
||||||
/** Checks out an upstream branch with a detached head. */
|
/** Checks out an upstream branch with a detached head. */
|
||||||
protected async checkoutUpstreamBranch(branchName: string) {
|
protected async checkoutUpstreamBranch(branchName: string) {
|
||||||
this.git.run(['fetch', '-q', this.git.repoGitUrl, branchName]);
|
this.git.run(['fetch', '-q', this.git.getRepoGitUrl(), branchName]);
|
||||||
this.git.run(['checkout', 'FETCH_HEAD', '--detach']);
|
this.git.run(['checkout', 'FETCH_HEAD', '--detach']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,8 +28,8 @@ export enum CompletionState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ReleaseTool {
|
export class ReleaseTool {
|
||||||
/** Client for interacting with the Github API and the local Git command. */
|
/** The singleton instance of the GitClient. */
|
||||||
private _git = new GitClient(this._githubToken, {github: this._github}, this._projectRoot);
|
private _git = GitClient.getAuthenticatedInstance();
|
||||||
/** The previous git commit to return back to after the release tool runs. */
|
/** The previous git commit to return back to after the release tool runs. */
|
||||||
private previousGitBranchOrRevision = this._git.getCurrentBranchOrRevision();
|
private previousGitBranchOrRevision = this._git.getCurrentBranchOrRevision();
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,8 @@ const THIRTY_SECONDS_IN_MS = 30000;
|
||||||
export type PullRequestState = 'merged'|'closed'|'open';
|
export type PullRequestState = 'merged'|'closed'|'open';
|
||||||
|
|
||||||
/** Gets whether a given pull request has been merged. */
|
/** Gets whether a given pull request has been merged. */
|
||||||
export async function getPullRequestState(api: GitClient, id: number): Promise<PullRequestState> {
|
export async function getPullRequestState(
|
||||||
|
api: GitClient<boolean>, id: number): Promise<PullRequestState> {
|
||||||
const {data} = await api.github.pulls.get({...api.remoteParams, pull_number: id});
|
const {data} = await api.github.pulls.get({...api.remoteParams, pull_number: id});
|
||||||
if (data.merged) {
|
if (data.merged) {
|
||||||
return 'merged';
|
return 'merged';
|
||||||
|
@ -38,7 +39,7 @@ export async function getPullRequestState(api: GitClient, id: number): Promise<P
|
||||||
* the merge is not fast-forward, Github does not consider the PR as merged and instead
|
* the merge is not fast-forward, Github does not consider the PR as merged and instead
|
||||||
* shows the PR as closed. See for example: https://github.com/angular/angular/pull/37918.
|
* shows the PR as closed. See for example: https://github.com/angular/angular/pull/37918.
|
||||||
*/
|
*/
|
||||||
async function isPullRequestClosedWithAssociatedCommit(api: GitClient, id: number) {
|
async function isPullRequestClosedWithAssociatedCommit(api: GitClient<boolean>, id: number) {
|
||||||
const request =
|
const request =
|
||||||
api.github.issues.listEvents.endpoint.merge({...api.remoteParams, issue_number: id});
|
api.github.issues.listEvents.endpoint.merge({...api.remoteParams, issue_number: id});
|
||||||
const events: Octokit.IssuesListEventsResponse = await api.github.paginate(request);
|
const events: Octokit.IssuesListEventsResponse = await api.github.paginate(request);
|
||||||
|
@ -72,7 +73,7 @@ async function isPullRequestClosedWithAssociatedCommit(api: GitClient, id: numbe
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Checks whether the specified commit is closing the given pull request. */
|
/** Checks whether the specified commit is closing the given pull request. */
|
||||||
async function isCommitClosingPullRequest(api: GitClient, sha: string, id: number) {
|
async function isCommitClosingPullRequest(api: GitClient<boolean>, sha: string, id: number) {
|
||||||
const {data} = await api.github.repos.getCommit({...api.remoteParams, ref: sha});
|
const {data} = await api.github.repos.getCommit({...api.remoteParams, ref: sha});
|
||||||
// Matches the closing keyword supported in commit messages. See:
|
// Matches the closing keyword supported in commit messages. See:
|
||||||
// https://docs.github.com/en/enterprise/2.16/user/github/managing-your-work-on-github/closing-issues-using-keywords.
|
// https://docs.github.com/en/enterprise/2.16/user/github/managing-your-work-on-github/closing-issues-using-keywords.
|
||||||
|
|
|
@ -38,7 +38,7 @@ export function getLocalChangelogFilePath(projectDir: string): string {
|
||||||
/** Release note generation. */
|
/** Release note generation. */
|
||||||
export class ReleaseNotes {
|
export class ReleaseNotes {
|
||||||
/** An instance of GitClient. */
|
/** An instance of GitClient. */
|
||||||
private git = new GitClient();
|
private git = GitClient.getInstance();
|
||||||
/** The github configuration. */
|
/** The github configuration. */
|
||||||
private readonly github = getConfig().github;
|
private readonly github = getConfig().github;
|
||||||
/** The configuration for the release notes generation. */
|
/** The configuration for the release notes generation. */
|
||||||
|
|
|
@ -33,7 +33,7 @@ export const testTmpDir: string = process.env['TEST_TMPDIR']!;
|
||||||
/** Interface describing a test release action. */
|
/** Interface describing a test release action. */
|
||||||
export interface TestReleaseAction<T extends ReleaseAction = ReleaseAction> {
|
export interface TestReleaseAction<T extends ReleaseAction = ReleaseAction> {
|
||||||
instance: T;
|
instance: T;
|
||||||
gitClient: VirtualGitClient;
|
gitClient: VirtualGitClient<boolean>;
|
||||||
repo: GithubTestingRepo;
|
repo: GithubTestingRepo;
|
||||||
fork: GithubTestingRepo;
|
fork: GithubTestingRepo;
|
||||||
testTmpDir: string;
|
testTmpDir: string;
|
||||||
|
@ -44,7 +44,7 @@ export interface TestReleaseAction<T extends ReleaseAction = ReleaseAction> {
|
||||||
/** Gets necessary test mocks for running a release action. */
|
/** Gets necessary test mocks for running a release action. */
|
||||||
export function getTestingMocksForReleaseAction() {
|
export function getTestingMocksForReleaseAction() {
|
||||||
const githubConfig = {owner: 'angular', name: 'dev-infra-test'};
|
const githubConfig = {owner: 'angular', name: 'dev-infra-test'};
|
||||||
const gitClient = new VirtualGitClient(undefined, {github: githubConfig}, testTmpDir);
|
const gitClient = VirtualGitClient.getAuthenticatedInstance({github: githubConfig});
|
||||||
const releaseConfig: ReleaseConfig = {
|
const releaseConfig: ReleaseConfig = {
|
||||||
npmPackages: [
|
npmPackages: [
|
||||||
'@angular/pkg1',
|
'@angular/pkg1',
|
||||||
|
|
|
@ -37,6 +37,6 @@ export function getRepositoryGitUrl(config: GithubConfig, githubToken?: string):
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gets a Github URL that refers to a list of recent commits within a specified branch. */
|
/** Gets a Github URL that refers to a list of recent commits within a specified branch. */
|
||||||
export function getListCommitsInBranchUrl({remoteParams}: GitClient, branchName: string) {
|
export function getListCommitsInBranchUrl({remoteParams}: GitClient<boolean>, branchName: string) {
|
||||||
return `https://github.com/${remoteParams.owner}/${remoteParams.repo}/commits/${branchName}`;
|
return `https://github.com/${remoteParams.owner}/${remoteParams.repo}/commits/${branchName}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Argv} from 'yargs';
|
import {Argv} from 'yargs';
|
||||||
|
|
||||||
import {error, red, yellow} from '../console';
|
import {error, red, yellow} from '../console';
|
||||||
|
|
||||||
import {GITHUB_TOKEN_GENERATE_URL} from './github-urls';
|
import {GITHUB_TOKEN_GENERATE_URL} from './github-urls';
|
||||||
|
import {GitClient} from './index';
|
||||||
|
|
||||||
export type ArgvWithGithubToken = Argv<{githubToken: string}>;
|
export type ArgvWithGithubToken = Argv<{githubToken: string}>;
|
||||||
|
|
||||||
|
@ -29,6 +32,7 @@ export function addGithubTokenOption(yargs: Argv): ArgvWithGithubToken {
|
||||||
error(yellow(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`));
|
error(yellow(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
GitClient.authenticateWithToken(githubToken);
|
||||||
return githubToken;
|
return githubToken;
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,6 +11,12 @@ import * as Octokit from '@octokit/rest';
|
||||||
import {RequestParameters} from '@octokit/types';
|
import {RequestParameters} from '@octokit/types';
|
||||||
import {query, types} from 'typed-graphqlify';
|
import {query, types} from 'typed-graphqlify';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object representation of a Graphql Query to be used as a response type and
|
||||||
|
* to generate a Graphql query string.
|
||||||
|
*/
|
||||||
|
export type GraphqlQueryObject = Parameters<typeof query>[1];
|
||||||
|
|
||||||
/** Interface describing a Github repository. */
|
/** Interface describing a Github repository. */
|
||||||
export interface GithubRepo {
|
export interface GithubRepo {
|
||||||
/** Owner login of the repository. */
|
/** Owner login of the repository. */
|
||||||
|
@ -26,6 +32,9 @@ export class GithubApiRequestError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Error for failed Github API requests. */
|
||||||
|
export class GithubGraphqlClientError extends Error {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Github client for interacting with the Github APIs.
|
* A Github client for interacting with the Github APIs.
|
||||||
*
|
*
|
||||||
|
@ -33,13 +42,15 @@ export class GithubApiRequestError extends Error {
|
||||||
* would provide value from memoized style responses.
|
* would provide value from memoized style responses.
|
||||||
**/
|
**/
|
||||||
export class GithubClient extends Octokit {
|
export class GithubClient extends Octokit {
|
||||||
/** The Github GraphQL (v4) API. */
|
|
||||||
graphql: GithubGraphqlClient;
|
|
||||||
|
|
||||||
/** The current user based on checking against the Github API. */
|
/** The current user based on checking against the Github API. */
|
||||||
private _currentUser: string|null = null;
|
private _currentUser: string|null = null;
|
||||||
|
/** The graphql instance with authentication set during construction. */
|
||||||
|
private _graphql = graphql.defaults({headers: {authorization: `token ${this.token}`}});
|
||||||
|
|
||||||
constructor(token?: string) {
|
/**
|
||||||
|
* @param token The github authentication token for Github Rest and Graphql API requests.
|
||||||
|
*/
|
||||||
|
constructor(private token?: string) {
|
||||||
// Pass in authentication token to base Octokit class.
|
// Pass in authentication token to base Octokit class.
|
||||||
super({auth: token});
|
super({auth: token});
|
||||||
|
|
||||||
|
@ -49,8 +60,22 @@ export class GithubClient extends Octokit {
|
||||||
throw new GithubApiRequestError(error.status, error.message);
|
throw new GithubApiRequestError(error.status, error.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create authenticated graphql client.
|
// Note: The prototype must be set explictly as Github's Octokit class is a non-standard class
|
||||||
this.graphql = new GithubGraphqlClient(token);
|
// definition which adjusts the prototype chain.
|
||||||
|
// See:
|
||||||
|
// https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work
|
||||||
|
// https://github.com/octokit/rest.js/blob/7b51cee4a22b6e52adcdca011f93efdffa5df998/lib/constructor.js
|
||||||
|
Object.setPrototypeOf(this, GithubClient.prototype);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Perform a query using Github's Graphql API. */
|
||||||
|
async graphql<T extends GraphqlQueryObject>(queryObject: T, params: RequestParameters = {}) {
|
||||||
|
if (this.token === undefined) {
|
||||||
|
throw new GithubGraphqlClientError(
|
||||||
|
'Cannot query via graphql without an authentication token set, use the authenticated ' +
|
||||||
|
'`GitClient` by calling `GitClient.getAuthenticatedInstance()`.');
|
||||||
|
}
|
||||||
|
return (await this._graphql(query(queryObject), params)) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Retrieve the login of the current user from Github. */
|
/** Retrieve the login of the current user from Github. */
|
||||||
|
@ -59,7 +84,7 @@ export class GithubClient extends Octokit {
|
||||||
if (this._currentUser !== null) {
|
if (this._currentUser !== null) {
|
||||||
return this._currentUser;
|
return this._currentUser;
|
||||||
}
|
}
|
||||||
const result = await this.graphql.query({
|
const result = await this.graphql({
|
||||||
viewer: {
|
viewer: {
|
||||||
login: types.string,
|
login: types.string,
|
||||||
}
|
}
|
||||||
|
@ -67,29 +92,3 @@ export class GithubClient extends Octokit {
|
||||||
return this._currentUser = result.viewer.login;
|
return this._currentUser = result.viewer.login;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* An object representation of a GraphQL Query to be used as a response type and
|
|
||||||
* to generate a GraphQL query string.
|
|
||||||
*/
|
|
||||||
export type GraphQLQueryObject = Parameters<typeof query>[1];
|
|
||||||
|
|
||||||
/** A client for interacting with Github's GraphQL API. */
|
|
||||||
export class GithubGraphqlClient {
|
|
||||||
/** The Github GraphQL (v4) API. */
|
|
||||||
private graqhql = graphql;
|
|
||||||
|
|
||||||
constructor(token?: string) {
|
|
||||||
// Set the default headers to include authorization with the provided token for all
|
|
||||||
// graphQL calls.
|
|
||||||
if (token) {
|
|
||||||
this.graqhql = this.graqhql.defaults({headers: {authorization: `token ${token}`}});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Perform a query using Github's GraphQL API. */
|
|
||||||
async query<T extends GraphQLQueryObject>(queryObject: T, params: RequestParameters = {}) {
|
|
||||||
const queryString = query(queryObject);
|
|
||||||
return (await this.graqhql(queryString, params)) as T;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import * as Octokit from '@octokit/rest';
|
||||||
import {spawnSync, SpawnSyncOptions, SpawnSyncReturns} from 'child_process';
|
import {spawnSync, SpawnSyncOptions, SpawnSyncReturns} from 'child_process';
|
||||||
import {Options as SemVerOptions, parse, SemVer} from 'semver';
|
import {Options as SemVerOptions, parse, SemVer} from 'semver';
|
||||||
|
|
||||||
import {getConfig, getRepoBaseDir, NgDevConfig} from '../config';
|
import {getConfig, getRepoBaseDir} from '../config';
|
||||||
import {debug, info, yellow} from '../console';
|
import {debug, info, yellow} from '../console';
|
||||||
import {DryRunError, isDryRun} from '../dry-run';
|
import {DryRunError, isDryRun} from '../dry-run';
|
||||||
import {GithubClient} from './github';
|
import {GithubClient} from './github';
|
||||||
|
@ -26,7 +26,7 @@ export type OAuthScopeTestFunction = (scopes: string[], missing: string[]) => vo
|
||||||
|
|
||||||
/** Error for failed Git commands. */
|
/** Error for failed Git commands. */
|
||||||
export class GitCommandError extends Error {
|
export class GitCommandError extends Error {
|
||||||
constructor(client: GitClient, public args: string[]) {
|
constructor(client: GitClient<boolean>, public args: string[]) {
|
||||||
// Errors are not guaranteed to be caught. To ensure that we don't
|
// Errors are not guaranteed to be caught. To ensure that we don't
|
||||||
// accidentally leak the Github token that might be used in a command,
|
// accidentally leak the Github token that might be used in a command,
|
||||||
// we sanitize the command that will be part of the error message.
|
// we sanitize the command that will be part of the error message.
|
||||||
|
@ -43,18 +43,49 @@ export class GitCommandError extends Error {
|
||||||
* `config`: The dev-infra configuration containing information about the remote. By default
|
* `config`: The dev-infra configuration containing information about the remote. By default
|
||||||
* the dev-infra configuration is loaded with its Github configuration.
|
* the dev-infra configuration is loaded with its Github configuration.
|
||||||
**/
|
**/
|
||||||
export class GitClient {
|
export class GitClient<Authenticated extends boolean> {
|
||||||
/** Whether verbose logging of Git actions should be used. */
|
/*************************************************
|
||||||
static LOG_COMMANDS = true;
|
* Singleton definition and configuration. *
|
||||||
/** Short-hand for accessing the default remote configuration. */
|
*************************************************/
|
||||||
remoteConfig = this._config.github;
|
/** The singleton instance of the authenticated GitClient. */
|
||||||
/** Octokit request parameters object for targeting the configured remote. */
|
private static authenticated: GitClient<true>;
|
||||||
remoteParams = {owner: this.remoteConfig.owner, repo: this.remoteConfig.name};
|
/** The singleton instance of the unauthenticated GitClient. */
|
||||||
/** Git URL that resolves to the configured repository. */
|
private static unauthenticated: GitClient<false>;
|
||||||
repoGitUrl = getRepositoryGitUrl(this.remoteConfig, this.githubToken);
|
|
||||||
/** Instance of the authenticated Github octokit API. */
|
|
||||||
github = new GithubClient(this.githubToken);
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static method to get the singleton instance of the unauthorized GitClient, creating it if it
|
||||||
|
* has not yet been created.
|
||||||
|
*/
|
||||||
|
static getInstance() {
|
||||||
|
if (!GitClient.unauthenticated) {
|
||||||
|
GitClient.unauthenticated = new GitClient(undefined);
|
||||||
|
}
|
||||||
|
return GitClient.unauthenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static method to get the singleton instance of the authenticated GitClient if it has been
|
||||||
|
* generated.
|
||||||
|
*/
|
||||||
|
static getAuthenticatedInstance() {
|
||||||
|
if (!GitClient.authenticated) {
|
||||||
|
throw Error('The authenticated GitClient has not yet been generated.');
|
||||||
|
}
|
||||||
|
return GitClient.authenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build the authenticated GitClient instance. */
|
||||||
|
static authenticateWithToken(token: string) {
|
||||||
|
if (GitClient.authenticated) {
|
||||||
|
throw Error(
|
||||||
|
'Cannot generate new authenticated GitClient after one has already been generated.');
|
||||||
|
}
|
||||||
|
GitClient.authenticated = new GitClient(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Whether verbose logging of Git actions should be used. */
|
||||||
|
private verboseLogging = true;
|
||||||
/** The OAuth scopes available for the provided Github token. */
|
/** The OAuth scopes available for the provided Github token. */
|
||||||
private _cachedOauthScopes: Promise<string[]>|null = null;
|
private _cachedOauthScopes: Promise<string[]>|null = null;
|
||||||
/**
|
/**
|
||||||
|
@ -62,18 +93,36 @@ export class GitClient {
|
||||||
* sanitizing the token from Git child process output.
|
* sanitizing the token from Git child process output.
|
||||||
*/
|
*/
|
||||||
private _githubTokenRegex: RegExp|null = null;
|
private _githubTokenRegex: RegExp|null = null;
|
||||||
|
/** Short-hand for accessing the default remote configuration. */
|
||||||
|
remoteConfig = this._config.github;
|
||||||
|
/** Octokit request parameters object for targeting the configured remote. */
|
||||||
|
remoteParams = {owner: this.remoteConfig.owner, repo: this.remoteConfig.name};
|
||||||
|
/** Instance of the authenticated Github octokit API. */
|
||||||
|
github = new GithubClient(this.githubToken);
|
||||||
|
|
||||||
constructor(
|
/**
|
||||||
public githubToken?: string, private _config: Pick<NgDevConfig, 'github'> = getConfig(),
|
* @param githubToken The github token used for authentication, if provided.
|
||||||
private _projectRoot = getRepoBaseDir()) {
|
* @param _config The configuration, containing the github specific configuration.
|
||||||
|
* @param _projectRoot The full path to the root of the repository base.
|
||||||
|
*/
|
||||||
|
protected constructor(public githubToken:
|
||||||
|
Authenticated extends true? string: undefined,
|
||||||
|
private _config = getConfig(),
|
||||||
|
private _projectRoot = getRepoBaseDir()) {
|
||||||
// If a token has been specified (and is not empty), pass it to the Octokit API and
|
// If a token has been specified (and is not empty), pass it to the Octokit API and
|
||||||
// also create a regular expression that can be used for sanitizing Git command output
|
// also create a regular expression that can be used for sanitizing Git command output
|
||||||
// so that it does not print the token accidentally.
|
// so that it does not print the token accidentally.
|
||||||
if (githubToken != null) {
|
if (typeof githubToken === 'string') {
|
||||||
this._githubTokenRegex = new RegExp(githubToken, 'g');
|
this._githubTokenRegex = new RegExp(githubToken, 'g');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Set the verbose logging state of the GitClient instance. */
|
||||||
|
setVerboseLoggingState(verbose: boolean): this {
|
||||||
|
this.verboseLogging = verbose;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/** Executes the given git command. Throws if the command fails. */
|
/** Executes the given git command. Throws if the command fails. */
|
||||||
run(args: string[], options?: SpawnSyncOptions): Omit<SpawnSyncReturns<string>, 'status'> {
|
run(args: string[], options?: SpawnSyncOptions): Omit<SpawnSyncReturns<string>, 'status'> {
|
||||||
const result = this.runGraceful(args, options);
|
const result = this.runGraceful(args, options);
|
||||||
|
@ -102,7 +151,7 @@ export class GitClient {
|
||||||
// To improve the debugging experience in case something fails, we print all executed Git
|
// To improve the debugging experience in case something fails, we print all executed Git
|
||||||
// commands to better understand the git actions occuring. Depending on the command being
|
// commands to better understand the git actions occuring. Depending on the command being
|
||||||
// executed, this debugging information should be logged at different logging levels.
|
// executed, this debugging information should be logged at different logging levels.
|
||||||
const printFn = (!GitClient.LOG_COMMANDS || options.stdio === 'ignore') ? debug : info;
|
const printFn = (!this.verboseLogging || options.stdio === 'ignore') ? debug : info;
|
||||||
// Note that we do not want to print the token if it is contained in the command. It's common
|
// Note that we do not want to print the token if it is contained in the command. It's common
|
||||||
// to share errors with others if the tool failed, and we do not want to leak tokens.
|
// to share errors with others if the tool failed, and we do not want to leak tokens.
|
||||||
printFn('Executing: git', this.omitGithubTokenFromMessage(args.join(' ')));
|
printFn('Executing: git', this.omitGithubTokenFromMessage(args.join(' ')));
|
||||||
|
@ -126,6 +175,11 @@ export class GitClient {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Git URL that resolves to the configured repository. */
|
||||||
|
getRepoGitUrl() {
|
||||||
|
return getRepositoryGitUrl(this.remoteConfig, this.githubToken);
|
||||||
|
}
|
||||||
|
|
||||||
/** Whether the given branch contains the specified SHA. */
|
/** Whether the given branch contains the specified SHA. */
|
||||||
hasCommit(branchName: string, sha: string): boolean {
|
hasCommit(branchName: string, sha: string): boolean {
|
||||||
return this.run(['branch', branchName, '--contains', sha]).stdout !== '';
|
return this.run(['branch', branchName, '--contains', sha]).stdout !== '';
|
||||||
|
|
|
@ -11,10 +11,10 @@ import {params, types} from 'typed-graphqlify';
|
||||||
import {GitClient} from './git/index';
|
import {GitClient} from './git/index';
|
||||||
|
|
||||||
/** Get a PR from github */
|
/** Get a PR from github */
|
||||||
export async function getPr<PrSchema>(prSchema: PrSchema, prNumber: number, git: GitClient) {
|
export async function getPr<PrSchema>(prSchema: PrSchema, prNumber: number, git: GitClient<true>) {
|
||||||
/** The owner and name of the repository */
|
/** The owner and name of the repository */
|
||||||
const {owner, name} = git.remoteConfig;
|
const {owner, name} = git.remoteConfig;
|
||||||
/** The GraphQL query object to get a the PR */
|
/** The Graphql query object to get a the PR */
|
||||||
const PR_QUERY = params(
|
const PR_QUERY = params(
|
||||||
{
|
{
|
||||||
$number: 'Int!', // The PR number
|
$number: 'Int!', // The PR number
|
||||||
|
@ -27,15 +27,15 @@ export async function getPr<PrSchema>(prSchema: PrSchema, prNumber: number, git:
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = (await git.github.graphql.query(PR_QUERY, {number: prNumber, owner, name}));
|
const result = (await git.github.graphql(PR_QUERY, {number: prNumber, owner, name}));
|
||||||
return result.repository.pullRequest;
|
return result.repository.pullRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get all pending PRs from github */
|
/** Get all pending PRs from github */
|
||||||
export async function getPendingPrs<PrSchema>(prSchema: PrSchema, git: GitClient) {
|
export async function getPendingPrs<PrSchema>(prSchema: PrSchema, git: GitClient<true>) {
|
||||||
/** The owner and name of the repository */
|
/** The owner and name of the repository */
|
||||||
const {owner, name} = git.remoteConfig;
|
const {owner, name} = git.remoteConfig;
|
||||||
/** The GraphQL query object to get a page of pending PRs */
|
/** The Graphql query object to get a page of pending PRs */
|
||||||
const PRS_QUERY = params(
|
const PRS_QUERY = params(
|
||||||
{
|
{
|
||||||
$first: 'Int', // How many entries to get with each request
|
$first: 'Int', // How many entries to get with each request
|
||||||
|
@ -75,7 +75,7 @@ export async function getPendingPrs<PrSchema>(prSchema: PrSchema, git: GitClient
|
||||||
owner,
|
owner,
|
||||||
name,
|
name,
|
||||||
};
|
};
|
||||||
const results = await git.github.graphql.query(PRS_QUERY, params) as typeof PRS_QUERY;
|
const results = await git.github.graphql(PRS_QUERY, params) as typeof PRS_QUERY;
|
||||||
prs.push(...results.repository.pullRequests.nodes);
|
prs.push(...results.repository.pullRequests.nodes);
|
||||||
hasNextPage = results.repository.pullRequests.pageInfo.hasNextPage;
|
hasNextPage = results.repository.pullRequests.pageInfo.hasNextPage;
|
||||||
cursor = results.repository.pullRequests.pageInfo.endCursor;
|
cursor = results.repository.pullRequests.pageInfo.endCursor;
|
||||||
|
|
|
@ -58,7 +58,21 @@ export interface Commit {
|
||||||
* Virtual git client that mocks Git commands and keeps track of the repository state
|
* Virtual git client that mocks Git commands and keeps track of the repository state
|
||||||
* in memory. This allows for convenient test assertions with Git interactions.
|
* in memory. This allows for convenient test assertions with Git interactions.
|
||||||
*/
|
*/
|
||||||
export class VirtualGitClient extends GitClient {
|
export class VirtualGitClient<Authenticated extends boolean> extends GitClient<Authenticated> {
|
||||||
|
static getInstance(config = mockNgDevConfig, tmpDir = testTmpDir): VirtualGitClient<false> {
|
||||||
|
return new VirtualGitClient(undefined, config, tmpDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getAuthenticatedInstance(config = mockNgDevConfig, tmpDir = testTmpDir):
|
||||||
|
VirtualGitClient<true> {
|
||||||
|
return new VirtualGitClient('abc123', config, tmpDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(token: Authenticated extends true? string: undefined, config: NgDevConfig,
|
||||||
|
tmpDir: string) {
|
||||||
|
super(token, config, tmpDir);
|
||||||
|
}
|
||||||
|
|
||||||
/** Current Git HEAD that has been previously fetched. */
|
/** Current Git HEAD that has been previously fetched. */
|
||||||
fetchHeadRef: RemoteRef|null = null;
|
fetchHeadRef: RemoteRef|null = null;
|
||||||
/** List of known branches in the repository. */
|
/** List of known branches in the repository. */
|
||||||
|
@ -190,10 +204,10 @@ export class VirtualGitClient extends GitClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
export function installVirtualGitClientSpies() {
|
||||||
* Builds a Virtual Git Client instance with the provided config and set the temporary test
|
const authenticatedVirtualGitClient = VirtualGitClient.getAuthenticatedInstance();
|
||||||
* directory.
|
spyOn(GitClient, 'getAuthenticatedInstance').and.returnValue(authenticatedVirtualGitClient);
|
||||||
*/
|
|
||||||
export function buildVirtualGitClient(config = mockNgDevConfig, tmpDir = testTmpDir) {
|
const unauthenticatedVirtualGitClient = VirtualGitClient.getInstance();
|
||||||
return (new VirtualGitClient(undefined, config, tmpDir));
|
spyOn(GitClient, 'getInstance').and.returnValue(unauthenticatedVirtualGitClient);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,13 +27,13 @@ export function getBranchPushMatcher(options: BranchPushMatchParameters) {
|
||||||
const {targetRepo, targetBranch, baseBranch, baseRepo, expectedCommits} = options;
|
const {targetRepo, targetBranch, baseBranch, baseRepo, expectedCommits} = options;
|
||||||
return jasmine.objectContaining({
|
return jasmine.objectContaining({
|
||||||
remote: {
|
remote: {
|
||||||
repoUrl: `https://github.com/${targetRepo.owner}/${targetRepo.name}.git`,
|
repoUrl: `https://abc123@github.com/${targetRepo.owner}/${targetRepo.name}.git`,
|
||||||
name: `refs/heads/${targetBranch}`
|
name: `refs/heads/${targetBranch}`
|
||||||
},
|
},
|
||||||
head: jasmine.objectContaining({
|
head: jasmine.objectContaining({
|
||||||
newCommits: expectedCommits,
|
newCommits: expectedCommits,
|
||||||
ref: {
|
ref: {
|
||||||
repoUrl: `https://github.com/${baseRepo.owner}/${baseRepo.name}.git`,
|
repoUrl: `https://abc123@github.com/${baseRepo.owner}/${baseRepo.name}.git`,
|
||||||
name: baseBranch,
|
name: baseBranch,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue