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
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {installVirtualGitClientSpies} from '../../utils/testing';
|
||||
import {BaseModule} from './base';
|
||||
|
||||
/** Data mocking as the "retrieved data". */
|
||||
|
@ -23,17 +24,18 @@ describe('BaseModule', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
retrieveDataSpy = spyOn(ConcreteBaseModule.prototype, 'retrieveData');
|
||||
installVirtualGitClientSpies();
|
||||
});
|
||||
|
||||
it('begins retrieving data during construction', () => {
|
||||
new ConcreteBaseModule({} as any, {} as any);
|
||||
new ConcreteBaseModule({} as any);
|
||||
|
||||
expect(retrieveDataSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('makes the data available via the data attribute', async () => {
|
||||
retrieveDataSpy.and.callThrough();
|
||||
const module = new ConcreteBaseModule({} as any, {} as any);
|
||||
const module = new ConcreteBaseModule({} as any);
|
||||
|
||||
expect(await module.data).toBe(exampleData);
|
||||
});
|
||||
|
|
|
@ -11,11 +11,12 @@ import {CaretakerConfig} from '../config';
|
|||
|
||||
/** The BaseModule to extend modules for caretaker checks from. */
|
||||
export abstract class BaseModule<Data> {
|
||||
/** The singleton instance of the GitClient. */
|
||||
protected git = GitClient.getAuthenticatedInstance();
|
||||
/** The data for the module. */
|
||||
readonly data = this.retrieveData();
|
||||
|
||||
constructor(
|
||||
protected git: GitClient, protected config: NgDevConfig<{caretaker: CaretakerConfig}>) {}
|
||||
constructor(protected config: NgDevConfig<{caretaker: CaretakerConfig}>) {}
|
||||
|
||||
/** Asyncronously retrieve data for the module. */
|
||||
protected abstract retrieveData(): Promise<Data>;
|
||||
|
|
|
@ -23,15 +23,13 @@ const moduleList = [
|
|||
];
|
||||
|
||||
/** 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. */
|
||||
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 */
|
||||
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`
|
||||
// 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
|
||||
*/
|
||||
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 console from '../../utils/console';
|
||||
import {buildVirtualGitClient, mockNgDevConfig, VirtualGitClient} from '../../utils/testing';
|
||||
import {installVirtualGitClientSpies, mockNgDevConfig} from '../../utils/testing';
|
||||
|
||||
import {CiModule} from './ci';
|
||||
|
||||
|
@ -20,10 +20,9 @@ describe('CiModule', () => {
|
|||
let getBranchStatusFromCiSpy: jasmine.Spy;
|
||||
let infoSpy: jasmine.Spy;
|
||||
let debugSpy: jasmine.Spy;
|
||||
let virtualGitClient: VirtualGitClient;
|
||||
|
||||
beforeEach(() => {
|
||||
virtualGitClient = buildVirtualGitClient();
|
||||
installVirtualGitClientSpies();
|
||||
fetchActiveReleaseTrainsSpy = spyOn(versioning, 'fetchActiveReleaseTrains');
|
||||
getBranchStatusFromCiSpy = spyOn(CiModule.prototype, 'getBranchStatusFromCi' as any);
|
||||
infoSpy = spyOn(console, 'info');
|
||||
|
@ -34,7 +33,7 @@ describe('CiModule', () => {
|
|||
it('handles active rc train', async () => {
|
||||
const trains = buildMockActiveReleaseTrains(true);
|
||||
fetchActiveReleaseTrainsSpy.and.resolveTo(trains);
|
||||
const module = new CiModule(virtualGitClient, {caretaker: {}, ...mockNgDevConfig});
|
||||
const module = new CiModule({caretaker: {}, ...mockNgDevConfig});
|
||||
await module.data;
|
||||
|
||||
expect(getBranchStatusFromCiSpy).toHaveBeenCalledWith(trains.releaseCandidate.branchName);
|
||||
|
@ -46,7 +45,7 @@ describe('CiModule', () => {
|
|||
it('handles an inactive rc train', async () => {
|
||||
const trains = buildMockActiveReleaseTrains(false);
|
||||
fetchActiveReleaseTrainsSpy.and.resolveTo(trains);
|
||||
const module = new CiModule(virtualGitClient, {caretaker: {}, ...mockNgDevConfig});
|
||||
const module = new CiModule({caretaker: {}, ...mockNgDevConfig});
|
||||
await module.data;
|
||||
|
||||
expect(getBranchStatusFromCiSpy).toHaveBeenCalledWith(trains.latest.branchName);
|
||||
|
@ -58,7 +57,7 @@ describe('CiModule', () => {
|
|||
const trains = buildMockActiveReleaseTrains(false);
|
||||
fetchActiveReleaseTrainsSpy.and.resolveTo(trains);
|
||||
getBranchStatusFromCiSpy.and.returnValue('success');
|
||||
const module = new CiModule(virtualGitClient, {caretaker: {}, ...mockNgDevConfig});
|
||||
const module = new CiModule({caretaker: {}, ...mockNgDevConfig});
|
||||
const data = await module.data;
|
||||
|
||||
expect(data[0]).toEqual(
|
||||
|
@ -89,7 +88,7 @@ describe('CiModule', () => {
|
|||
]);
|
||||
fetchActiveReleaseTrainsSpy.and.resolveTo([]);
|
||||
|
||||
const module = new CiModule(virtualGitClient, {caretaker: {}, ...mockNgDevConfig});
|
||||
const module = new CiModule({caretaker: {}, ...mockNgDevConfig});
|
||||
Object.defineProperty(module, 'data', {value: fakeData});
|
||||
|
||||
await module.printToTerminal();
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* 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';
|
||||
|
||||
|
@ -23,8 +23,8 @@ function builder(yargs: Argv) {
|
|||
}
|
||||
|
||||
/** Handles the command. */
|
||||
async function handler({githubToken}: Arguments<CaretakerCheckOptions>) {
|
||||
await checkServiceStatuses(githubToken);
|
||||
async function handler() {
|
||||
await checkServiceStatuses();
|
||||
}
|
||||
|
||||
/** yargs command module for checking status information for the repository */
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
import {SpawnSyncReturns} from 'child_process';
|
||||
|
||||
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';
|
||||
|
||||
|
@ -18,10 +19,9 @@ describe('G3Module', () => {
|
|||
let getLatestShas: jasmine.Spy;
|
||||
let getDiffStats: jasmine.Spy;
|
||||
let infoSpy: jasmine.Spy;
|
||||
let virtualGitClient: VirtualGitClient;
|
||||
|
||||
beforeEach(() => {
|
||||
virtualGitClient = buildVirtualGitClient();
|
||||
installVirtualGitClientSpies();
|
||||
getG3FileIncludeAndExcludeLists =
|
||||
spyOn(G3Module.prototype, 'getG3FileIncludeAndExcludeLists' 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 () => {
|
||||
getG3FileIncludeAndExcludeLists.and.returnValue(null);
|
||||
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(await module.data).toBe(undefined);
|
||||
|
@ -42,7 +42,7 @@ describe('G3Module', () => {
|
|||
it('unless the branch shas are not able to be retrieved', async () => {
|
||||
getLatestShas.and.returnValue(null);
|
||||
getG3FileIncludeAndExcludeLists.and.returnValue({include: ['file1'], exclude: []});
|
||||
const module = new G3Module(virtualGitClient, {caretaker: {}, ...mockNgDevConfig});
|
||||
const module = new G3Module({caretaker: {}, ...mockNgDevConfig});
|
||||
|
||||
expect(getDiffStats).not.toHaveBeenCalled();
|
||||
expect(await module.data).toBe(undefined);
|
||||
|
@ -52,7 +52,7 @@ describe('G3Module', () => {
|
|||
getLatestShas.and.returnValue({g3: 'abc123', master: 'zxy987'});
|
||||
getG3FileIncludeAndExcludeLists.and.returnValue({include: ['project1/*'], exclude: []});
|
||||
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>> = {};
|
||||
if (args[0] === 'rev-list') {
|
||||
output.stdout = '3';
|
||||
|
@ -63,7 +63,7 @@ describe('G3Module', () => {
|
|||
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;
|
||||
|
||||
expect(insertions).toBe(12);
|
||||
|
@ -82,7 +82,7 @@ describe('G3Module', () => {
|
|||
commits: 2,
|
||||
});
|
||||
|
||||
const module = new G3Module(virtualGitClient, {caretaker: {}, ...mockNgDevConfig});
|
||||
const module = new G3Module({caretaker: {}, ...mockNgDevConfig});
|
||||
Object.defineProperty(module, 'data', {value: fakeData});
|
||||
await module.printToTerminal();
|
||||
|
||||
|
@ -98,7 +98,7 @@ describe('G3Module', () => {
|
|||
commits: 25,
|
||||
});
|
||||
|
||||
const module = new G3Module(virtualGitClient, {caretaker: {}, ...mockNgDevConfig});
|
||||
const module = new G3Module({caretaker: {}, ...mockNgDevConfig});
|
||||
Object.defineProperty(module, 'data', {value: fakeData});
|
||||
await module.printToTerminal();
|
||||
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as console from '../../utils/console';
|
||||
import {GithubGraphqlClient} from '../../utils/git/github';
|
||||
import {buildVirtualGitClient, mockNgDevConfig, VirtualGitClient} from '../../utils/testing';
|
||||
import {GithubClient} from '../../utils/git/github';
|
||||
import {installVirtualGitClientSpies, mockNgDevConfig} from '../../utils/testing';
|
||||
|
||||
import {GithubQueriesModule} from './github';
|
||||
|
||||
|
@ -16,37 +16,35 @@ describe('GithubQueriesModule', () => {
|
|||
let githubApiSpy: jasmine.Spy;
|
||||
let infoSpy: jasmine.Spy;
|
||||
let infoGroupSpy: jasmine.Spy;
|
||||
let virtualGitClient: VirtualGitClient;
|
||||
|
||||
beforeEach(() => {
|
||||
githubApiSpy = spyOn(GithubGraphqlClient.prototype, 'query')
|
||||
githubApiSpy = spyOn(GithubClient.prototype, 'graphql')
|
||||
.and.throwError(
|
||||
'The graphql query response must always be manually defined in a test.');
|
||||
virtualGitClient = buildVirtualGitClient();
|
||||
installVirtualGitClientSpies();
|
||||
infoGroupSpy = spyOn(console.info, 'group');
|
||||
infoSpy = spyOn(console, 'info');
|
||||
});
|
||||
|
||||
describe('gathering stats', () => {
|
||||
it('unless githubQueries are `undefined`', async () => {
|
||||
const module = new GithubQueriesModule(
|
||||
virtualGitClient, {...mockNgDevConfig, caretaker: {githubQueries: undefined}});
|
||||
const module =
|
||||
new GithubQueriesModule({...mockNgDevConfig, caretaker: {githubQueries: undefined}});
|
||||
|
||||
expect(await module.data).toBe(undefined);
|
||||
});
|
||||
|
||||
it('unless githubQueries are an empty array', async () => {
|
||||
const module = new GithubQueriesModule(
|
||||
virtualGitClient, {...mockNgDevConfig, caretaker: {githubQueries: []}});
|
||||
const module = new GithubQueriesModule({...mockNgDevConfig, caretaker: {githubQueries: []}});
|
||||
|
||||
expect(await module.data).toBe(undefined);
|
||||
});
|
||||
|
||||
it('for the requestd Github queries', async () => {
|
||||
it('for the requested Github queries', async () => {
|
||||
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,
|
||||
caretaker: {githubQueries: [{name: 'key name with spaces', query: 'issue: yes'}]}
|
||||
});
|
||||
|
@ -55,7 +53,7 @@ describe('GithubQueriesModule', () => {
|
|||
queryName: 'key name with spaces',
|
||||
count: 1,
|
||||
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});
|
||||
|
||||
await module.printToTerminal();
|
||||
|
@ -95,7 +93,7 @@ describe('GithubQueriesModule', () => {
|
|||
queryName: 'query1',
|
||||
count: 1,
|
||||
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',
|
||||
|
@ -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});
|
||||
|
||||
await module.printToTerminal();
|
||||
|
@ -114,7 +112,7 @@ describe('GithubQueriesModule', () => {
|
|||
expect(infoSpy).toHaveBeenCalledWith('query1 1');
|
||||
expect(infoGroupSpy)
|
||||
.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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -56,7 +56,7 @@ export class GithubQueriesModule extends BaseModule<GithubQueryResults|void> {
|
|||
}
|
||||
|
||||
/** 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 {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. */
|
||||
private buildGraphqlQuery(queries: NonNullable<CaretakerConfig['githubQueries']>) {
|
||||
/** The query object for graphql. */
|
||||
const graphQlQuery: GithubQueryResult = {};
|
||||
const graphqlQuery: GithubQueryResult = {};
|
||||
const {owner, name: repo} = this.git.remoteConfig;
|
||||
/** The Github search filter for the configured repository. */
|
||||
const repoFilter = `repo:${owner}/${repo}`;
|
||||
|
||||
|
||||
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');
|
||||
graphQlQuery[queryKey] = params(
|
||||
graphqlQuery[queryKey] = params(
|
||||
{
|
||||
type: 'ISSUE',
|
||||
first: MAX_RETURNED_ISSUES,
|
||||
|
@ -92,7 +92,7 @@ export class GithubQueriesModule extends BaseModule<GithubQueryResults|void> {
|
|||
{...GithubQueryResultFragment});
|
||||
});
|
||||
|
||||
return graphQlQuery;
|
||||
return graphqlQuery;
|
||||
}
|
||||
|
||||
async printToTerminal() {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import * as console from '../../utils/console';
|
||||
import {buildVirtualGitClient, mockNgDevConfig, VirtualGitClient} from '../../utils/testing';
|
||||
import {installVirtualGitClientSpies, mockNgDevConfig} from '../../utils/testing';
|
||||
|
||||
import {services, ServicesModule} from './services';
|
||||
|
||||
|
@ -16,20 +16,19 @@ describe('ServicesModule', () => {
|
|||
let getStatusFromStandardApiSpy: jasmine.Spy;
|
||||
let infoSpy: jasmine.Spy;
|
||||
let infoGroupSpy: jasmine.Spy;
|
||||
let virtualGitClient: VirtualGitClient;
|
||||
|
||||
services.splice(0, Infinity, {url: 'fakeStatus.com/api.json', name: 'Service Name'});
|
||||
|
||||
beforeEach(() => {
|
||||
getStatusFromStandardApiSpy = spyOn(ServicesModule.prototype, 'getStatusFromStandardApi');
|
||||
virtualGitClient = buildVirtualGitClient();
|
||||
installVirtualGitClientSpies();
|
||||
infoGroupSpy = spyOn(console.info, 'group');
|
||||
infoSpy = spyOn(console, 'info');
|
||||
});
|
||||
|
||||
describe('gathering status', () => {
|
||||
it('for each of the services', async () => {
|
||||
new ServicesModule(virtualGitClient, {caretaker: {}, ...mockNgDevConfig});
|
||||
new ServicesModule({caretaker: {}, ...mockNgDevConfig});
|
||||
|
||||
expect(getStatusFromStandardApiSpy)
|
||||
.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});
|
||||
await module.printToTerminal();
|
||||
|
||||
|
|
|
@ -405,36 +405,6 @@ function getListCommitsInBranchUrl(_a, 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
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
|
@ -477,6 +447,14 @@ var GithubApiRequestError = /** @class */ (function (_super) {
|
|||
}
|
||||
return GithubApiRequestError;
|
||||
}(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.
|
||||
*
|
||||
|
@ -485,21 +463,48 @@ var GithubApiRequestError = /** @class */ (function (_super) {
|
|||
**/
|
||||
var GithubClient = /** @class */ (function (_super) {
|
||||
tslib.__extends(GithubClient, _super);
|
||||
/**
|
||||
* @param token The github authentication token for Github Rest and Graphql API requests.
|
||||
*/
|
||||
function GithubClient(token) {
|
||||
var _this =
|
||||
// Pass in authentication token to base Octokit class.
|
||||
_super.call(this, { auth: token }) || this;
|
||||
_this.token = token;
|
||||
/** The current user based on checking against the Github API. */
|
||||
_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) {
|
||||
// Wrap API errors in a known error class. This allows us to
|
||||
// expect Github API errors better and in a non-ambiguous way.
|
||||
throw new GithubApiRequestError(error.status, error.message);
|
||||
});
|
||||
// Create authenticated graphql client.
|
||||
_this.graphql = new GithubGraphqlClient(token);
|
||||
// Note: The prototype must be set explictly as Github's Octokit class is a non-standard class
|
||||
// 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;
|
||||
}
|
||||
/** 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. */
|
||||
GithubClient.prototype.getCurrentUser = function () {
|
||||
return tslib.__awaiter(this, void 0, void 0, function () {
|
||||
|
@ -511,7 +516,7 @@ var GithubClient = /** @class */ (function (_super) {
|
|||
if (this._currentUser !== null) {
|
||||
return [2 /*return*/, this._currentUser];
|
||||
}
|
||||
return [4 /*yield*/, this.graphql.query({
|
||||
return [4 /*yield*/, this.graphql({
|
||||
viewer: {
|
||||
login: typedGraphqlify.types.string,
|
||||
}
|
||||
|
@ -525,34 +530,6 @@ var GithubClient = /** @class */ (function (_super) {
|
|||
};
|
||||
return GithubClient;
|
||||
}(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
|
||||
|
@ -585,20 +562,19 @@ var GitCommandError = /** @class */ (function (_super) {
|
|||
* the dev-infra configuration is loaded with its Github configuration.
|
||||
**/
|
||||
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) {
|
||||
if (_config === void 0) { _config = getConfig(); }
|
||||
if (_projectRoot === void 0) { _projectRoot = getRepoBaseDir(); }
|
||||
this.githubToken = githubToken;
|
||||
this._config = _config;
|
||||
this._projectRoot = _projectRoot;
|
||||
/** 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 };
|
||||
/** 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);
|
||||
/** Whether verbose logging of Git actions should be used. */
|
||||
this.verboseLogging = true;
|
||||
/** The OAuth scopes available for the provided Github token. */
|
||||
this._cachedOauthScopes = null;
|
||||
/**
|
||||
|
@ -606,13 +582,51 @@ var GitClient = /** @class */ (function () {
|
|||
* sanitizing the token from Git child process output.
|
||||
*/
|
||||
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
|
||||
// also create a regular expression that can be used for sanitizing Git command output
|
||||
// so that it does not print the token accidentally.
|
||||
if (githubToken != null) {
|
||||
if (typeof githubToken === 'string') {
|
||||
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. */
|
||||
GitClient.prototype.run = function (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
|
||||
// commands to better understand the git actions occuring. Depending on the command being
|
||||
// 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
|
||||
// to share errors with others if the tool failed, and we do not want to leak tokens.
|
||||
printFn('Executing: git', this.omitGithubTokenFromMessage(args.join(' ')));
|
||||
|
@ -655,6 +669,10 @@ var GitClient = /** @class */ (function () {
|
|||
}
|
||||
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. */
|
||||
GitClient.prototype.hasCommit = function (branchName, sha) {
|
||||
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(); });
|
||||
});
|
||||
};
|
||||
/** Whether verbose logging of Git actions should be used. */
|
||||
GitClient.LOG_COMMANDS = true;
|
||||
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
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
|
@ -1091,9 +1138,10 @@ function getLtsNpmDistTagOfMajor(major) {
|
|||
|
||||
/** The BaseModule to extend modules for caretaker checks from. */
|
||||
class BaseModule {
|
||||
constructor(git, config) {
|
||||
this.git = git;
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
/** The singleton instance of the GitClient. */
|
||||
this.git = GitClient.getAuthenticatedInstance();
|
||||
/** The data for the module. */
|
||||
this.data = this.retrieveData();
|
||||
}
|
||||
|
@ -1332,7 +1380,7 @@ class GithubQueriesModule extends BaseModule {
|
|||
return;
|
||||
}
|
||||
/** 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 { owner, name: repo } = this.git.remoteConfig;
|
||||
return results.map((result, i) => {
|
||||
|
@ -1348,20 +1396,20 @@ class GithubQueriesModule extends BaseModule {
|
|||
/** Build a Graphql query statement for the provided queries. */
|
||||
buildGraphqlQuery(queries) {
|
||||
/** The query object for graphql. */
|
||||
const graphQlQuery = {};
|
||||
const graphqlQuery = {};
|
||||
const { owner, name: repo } = this.git.remoteConfig;
|
||||
/** The Github search filter for the configured repository. */
|
||||
const repoFilter = `repo:${owner}/${repo}`;
|
||||
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');
|
||||
graphQlQuery[queryKey] = typedGraphqlify.params({
|
||||
graphqlQuery[queryKey] = typedGraphqlify.params({
|
||||
type: 'ISSUE',
|
||||
first: MAX_RETURNED_ISSUES,
|
||||
query: `"${repoFilter} ${query.replace(/"/g, '\\"')}"`,
|
||||
}, Object.assign({}, GithubQueryResultFragment));
|
||||
});
|
||||
return graphQlQuery;
|
||||
return graphqlQuery;
|
||||
}
|
||||
printToTerminal() {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
|
@ -1470,16 +1518,14 @@ const moduleList = [
|
|||
G3Module,
|
||||
];
|
||||
/** 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* () {
|
||||
// Set the verbose logging state of the GitClient.
|
||||
GitClient.getAuthenticatedInstance().setVerboseLoggingState(false);
|
||||
/** The configuration for the caretaker commands. */
|
||||
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 */
|
||||
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`
|
||||
// promises do not match typings, however our usage here is only to determine when the promise
|
||||
// resolves.
|
||||
|
@ -1502,9 +1548,9 @@ function builder(yargs) {
|
|||
return addGithubTokenOption(yargs);
|
||||
}
|
||||
/** Handles the command. */
|
||||
function handler({ githubToken }) {
|
||||
function handler() {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
yield checkServiceStatuses(githubToken);
|
||||
yield checkServiceStatuses();
|
||||
});
|
||||
}
|
||||
/** yargs command module for checking status information for the repository */
|
||||
|
@ -2836,8 +2882,8 @@ function getTargetBranchesForPr(prNumber) {
|
|||
const config = getConfig();
|
||||
/** Repo owner and name for the github repository. */
|
||||
const { owner, name: repo } = config.github;
|
||||
/** The git client to get a Github API service instance. */
|
||||
const git = new GitClient(undefined, config);
|
||||
/** The singleton instance of the GitClient. */
|
||||
const git = GitClient.getInstance();
|
||||
/** The validated merge config. */
|
||||
const { config: mergeConfig, errors } = yield loadAndValidateConfig(config, git.github);
|
||||
if (errors !== undefined) {
|
||||
|
@ -2931,7 +2977,7 @@ function getPr(prSchema, prNumber, git) {
|
|||
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:
|
||||
result = (_b.sent());
|
||||
return [2 /*return*/, result.repository.pullRequest];
|
||||
|
@ -2978,7 +3024,7 @@ function getPendingPrs(prSchema, git) {
|
|||
owner: owner,
|
||||
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:
|
||||
results = _b.sent();
|
||||
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
|
||||
* 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 = {
|
||||
state: typedGraphqlify.types.string,
|
||||
maintainerCanModify: typedGraphqlify.types.boolean,
|
||||
|
@ -3037,8 +3083,8 @@ class MaintainerModifyAccessError extends Error {
|
|||
*/
|
||||
function checkOutPullRequestLocally(prNumber, githubToken, opts = {}) {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
/** Authenticated Git client for git and Github interactions. */
|
||||
const git = new GitClient(githubToken);
|
||||
/** The singleton instance of the GitClient. */
|
||||
const git = GitClient.getAuthenticatedInstance();
|
||||
// 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.
|
||||
if (git.hasLocalChanges()) {
|
||||
|
@ -3132,7 +3178,7 @@ const CheckoutCommandModule = {
|
|||
* 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
|
||||
*/
|
||||
/* GraphQL schema for the response body for each pending PR. */
|
||||
/* Graphql schema for the response body for each pending PR. */
|
||||
const PR_SCHEMA$1 = {
|
||||
headRef: {
|
||||
name: typedGraphqlify.types.string,
|
||||
|
@ -3160,9 +3206,10 @@ function processPr(pr) {
|
|||
/** Name of a temporary local branch that is used for checking conflicts. **/
|
||||
const tempWorkingBranch = '__NgDevRepoBaseAfterChange__';
|
||||
/** 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* () {
|
||||
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
|
||||
// check cannot run as it needs to move between branches.
|
||||
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 = {
|
||||
url: typedGraphqlify.types.string,
|
||||
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
|
||||
// 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. */
|
||||
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.
|
||||
// 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;
|
||||
}());
|
||||
|
@ -4206,7 +4253,7 @@ var PullRequestMergeTask = /** @class */ (function () {
|
|||
* @param projectRoot Path to the local Git project that is used for merging.
|
||||
* @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 () {
|
||||
/** Performs the merge and returns whether it was successful or not. */
|
||||
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
|
||||
// rely on `--no-verify` as some hooks still run, notably the `prepare-commit-msg` hook.
|
||||
process.env['HUSKY'] = '0';
|
||||
return [4 /*yield*/, createPullRequestMergeTask(githubToken, flags)];
|
||||
return [4 /*yield*/, createPullRequestMergeTask(flags)];
|
||||
case 1:
|
||||
api = _a.sent();
|
||||
return [4 /*yield*/, performMerge(false)];
|
||||
|
@ -4343,15 +4390,14 @@ function mergePullRequest(prNumber, githubToken, flags) {
|
|||
* and optional explicit configuration. An explicit configuration can be specified
|
||||
* 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 () {
|
||||
var projectRoot, devInfraConfig, git, _a, config, errors;
|
||||
var devInfraConfig, git, _a, config, errors;
|
||||
return tslib.__generator(this, function (_b) {
|
||||
switch (_b.label) {
|
||||
case 0:
|
||||
projectRoot = getRepoBaseDir();
|
||||
devInfraConfig = getConfig();
|
||||
git = new GitClient(githubToken, devInfraConfig, projectRoot);
|
||||
git = GitClient.getAuthenticatedInstance();
|
||||
return [4 /*yield*/, loadAndValidateConfig(devInfraConfig, git.github)];
|
||||
case 1:
|
||||
_a = _b.sent(), config = _a.config, errors = _a.errors;
|
||||
|
@ -4396,11 +4442,11 @@ function builder$6(yargs) {
|
|||
}
|
||||
/** Handles the command. */
|
||||
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.__generator(this, function (_b) {
|
||||
switch (_b.label) {
|
||||
case 0: return [4 /*yield*/, mergePullRequest(pr, githubToken, { branchPrompt: branchPrompt })];
|
||||
case 0: return [4 /*yield*/, mergePullRequest(pr, { branchPrompt: branchPrompt })];
|
||||
case 1:
|
||||
_b.sent();
|
||||
return [2 /*return*/];
|
||||
|
@ -4423,7 +4469,7 @@ var MergeCommandModule = {
|
|||
* 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
|
||||
*/
|
||||
/* GraphQL schema for the response body for each pending PR. */
|
||||
/* Graphql schema for the response body for each pending PR. */
|
||||
const PR_SCHEMA$3 = {
|
||||
state: typedGraphqlify.types.string,
|
||||
maintainerCanModify: typedGraphqlify.types.boolean,
|
||||
|
@ -4450,7 +4496,8 @@ const PR_SCHEMA$3 = {
|
|||
*/
|
||||
function rebasePr(prNumber, githubToken, config = getConfig()) {
|
||||
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.
|
||||
if (git.hasLocalChanges()) {
|
||||
error('Cannot perform rebase of PR with local changes.');
|
||||
|
@ -5747,7 +5794,7 @@ class ReleaseAction {
|
|||
return this._cachedForkRepo;
|
||||
}
|
||||
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;
|
||||
if (forks.length === 0) {
|
||||
error(red(' ✘ Unable to find fork for currently authenticated user.'));
|
||||
|
@ -5800,7 +5847,7 @@ class ReleaseAction {
|
|||
pushHeadToRemoteBranch(branchName) {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
// 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. */
|
||||
checkoutUpstreamBranch(branchName) {
|
||||
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']);
|
||||
});
|
||||
}
|
||||
|
@ -6551,8 +6598,8 @@ class ReleaseTool {
|
|||
this._github = _github;
|
||||
this._githubToken = _githubToken;
|
||||
this._projectRoot = _projectRoot;
|
||||
/** Client for interacting with the Github API and the local Git command. */
|
||||
this._git = new GitClient(this._githubToken, { github: this._github }, this._projectRoot);
|
||||
/** The singleton instance of the GitClient. */
|
||||
this._git = GitClient.getAuthenticatedInstance();
|
||||
/** The previous git commit to return back to after the release tool runs. */
|
||||
this.previousGitBranchOrRevision = this._git.getCurrentBranchOrRevision();
|
||||
}
|
||||
|
|
|
@ -17,8 +17,8 @@ export async function getTargetBranchesForPr(prNumber: number) {
|
|||
const config = getConfig();
|
||||
/** Repo owner and name for the github repository. */
|
||||
const {owner, name: repo} = config.github;
|
||||
/** The git client to get a Github API service instance. */
|
||||
const git = new GitClient(undefined, config);
|
||||
/** The singleton instance of the GitClient. */
|
||||
const git = GitClient.getInstance();
|
||||
/** The validated merge config. */
|
||||
const {config: mergeConfig, errors} = await loadAndValidateConfig(config, git.github);
|
||||
if (errors !== undefined) {
|
||||
|
|
|
@ -6,31 +6,31 @@
|
|||
* 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 {addTokenToGitHttpsUrl} from '../../utils/git/github-urls';
|
||||
import {GitClient} from '../../utils/git/index';
|
||||
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 = {
|
||||
state: graphQLTypes.string,
|
||||
maintainerCanModify: graphQLTypes.boolean,
|
||||
viewerDidAuthor: graphQLTypes.boolean,
|
||||
headRefOid: graphQLTypes.string,
|
||||
state: graphqlTypes.string,
|
||||
maintainerCanModify: graphqlTypes.boolean,
|
||||
viewerDidAuthor: graphqlTypes.boolean,
|
||||
headRefOid: graphqlTypes.string,
|
||||
headRef: {
|
||||
name: graphQLTypes.string,
|
||||
name: graphqlTypes.string,
|
||||
repository: {
|
||||
url: graphQLTypes.string,
|
||||
nameWithOwner: graphQLTypes.string,
|
||||
url: graphqlTypes.string,
|
||||
nameWithOwner: graphqlTypes.string,
|
||||
},
|
||||
},
|
||||
baseRef: {
|
||||
name: graphQLTypes.string,
|
||||
name: graphqlTypes.string,
|
||||
repository: {
|
||||
url: graphQLTypes.string,
|
||||
nameWithOwner: graphQLTypes.string,
|
||||
url: graphqlTypes.string,
|
||||
nameWithOwner: graphqlTypes.string,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -62,8 +62,8 @@ export interface PullRequestCheckoutOptions {
|
|||
*/
|
||||
export async function checkOutPullRequestLocally(
|
||||
prNumber: number, githubToken: string, opts: PullRequestCheckoutOptions = {}) {
|
||||
/** Authenticated Git client for git and Github interactions. */
|
||||
const git = new GitClient(githubToken);
|
||||
/** The singleton instance of the GitClient. */
|
||||
const git = GitClient.getAuthenticatedInstance();
|
||||
|
||||
// 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.
|
||||
|
|
|
@ -7,38 +7,37 @@
|
|||
*/
|
||||
|
||||
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 {GitClient} from '../../utils/git/index';
|
||||
import {getPendingPrs} from '../../utils/github';
|
||||
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 = {
|
||||
headRef: {
|
||||
name: graphQLTypes.string,
|
||||
name: graphqlTypes.string,
|
||||
repository: {
|
||||
url: graphQLTypes.string,
|
||||
nameWithOwner: graphQLTypes.string,
|
||||
url: graphqlTypes.string,
|
||||
nameWithOwner: graphqlTypes.string,
|
||||
},
|
||||
},
|
||||
baseRef: {
|
||||
name: graphQLTypes.string,
|
||||
name: graphqlTypes.string,
|
||||
repository: {
|
||||
url: graphQLTypes.string,
|
||||
nameWithOwner: graphQLTypes.string,
|
||||
url: graphqlTypes.string,
|
||||
nameWithOwner: graphqlTypes.string,
|
||||
},
|
||||
},
|
||||
updatedAt: graphQLTypes.string,
|
||||
number: graphQLTypes.number,
|
||||
mergeable: graphQLTypes.string,
|
||||
title: graphQLTypes.string,
|
||||
updatedAt: graphqlTypes.string,
|
||||
number: graphqlTypes.number,
|
||||
mergeable: graphqlTypes.string,
|
||||
title: graphqlTypes.string,
|
||||
};
|
||||
|
||||
/* Pull Request response from Github GraphQL query */
|
||||
/* Pull Request response from Github Graphql query */
|
||||
type RawPullRequest = typeof PR_SCHEMA;
|
||||
|
||||
/** Convert raw Pull Request response from Github to usable Pull Request object. */
|
||||
|
@ -53,9 +52,9 @@ type PullRequest = ReturnType<typeof processPr>;
|
|||
const tempWorkingBranch = '__NgDevRepoBaseAfterChange__';
|
||||
|
||||
/** Checks if the provided PR will cause new conflicts in other pending PRs. */
|
||||
export async function discoverNewConflictsForPr(
|
||||
newPrNumber: number, updatedAfter: number, config: Pick<NgDevConfig, 'github'> = getConfig()) {
|
||||
const git = new GitClient();
|
||||
export async function discoverNewConflictsForPr(newPrNumber: number, updatedAfter: number) {
|
||||
/** The singleton instance of the GitClient. */
|
||||
const git = GitClient.getAuthenticatedInstance();
|
||||
// If there are any local changes in the current repository state, the
|
||||
// check cannot run as it needs to move between branches.
|
||||
if (git.hasLocalChanges()) {
|
||||
|
|
|
@ -37,8 +37,8 @@ function builder(yargs: Argv) {
|
|||
}
|
||||
|
||||
/** Handles the command. */
|
||||
async function handler({pr, githubToken, branchPrompt}: Arguments<MergeCommandOptions>) {
|
||||
await mergePullRequest(pr, githubToken, {branchPrompt});
|
||||
async function handler({pr, branchPrompt}: Arguments<MergeCommandOptions>) {
|
||||
await mergePullRequest(pr, {branchPrompt});
|
||||
}
|
||||
|
||||
/** 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 config Configuration for merging pull requests.
|
||||
*/
|
||||
export async function mergePullRequest(
|
||||
prNumber: number, githubToken: string, flags: PullRequestMergeTaskFlags) {
|
||||
export async function mergePullRequest(prNumber: number, flags: PullRequestMergeTaskFlags) {
|
||||
// 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.
|
||||
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.
|
||||
// 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
|
||||
* when the merge script is used outside of a `ng-dev` configured repository.
|
||||
*/
|
||||
async function createPullRequestMergeTask(githubToken: string, flags: PullRequestMergeTaskFlags) {
|
||||
const projectRoot = getRepoBaseDir();
|
||||
async function createPullRequestMergeTask(flags: PullRequestMergeTaskFlags) {
|
||||
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);
|
||||
|
||||
if (errors) {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* 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 {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 = {
|
||||
url: graphQLTypes.string,
|
||||
number: graphQLTypes.number,
|
||||
url: graphqlTypes.string,
|
||||
number: graphqlTypes.number,
|
||||
// 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.
|
||||
commits: params({last: 100}, {
|
||||
totalCount: graphQLTypes.number,
|
||||
totalCount: graphqlTypes.number,
|
||||
nodes: [{
|
||||
commit: {
|
||||
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,
|
||||
title: graphQLTypes.string,
|
||||
baseRefName: graphqlTypes.string,
|
||||
title: graphqlTypes.string,
|
||||
labels: params({first: 100}, {
|
||||
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. */
|
||||
async function fetchPullRequestFromGithub(
|
||||
git: GitClient, prNumber: number): Promise<typeof PR_SCHEMA|null> {
|
||||
git: GitClient<true>, prNumber: number): Promise<typeof PR_SCHEMA|null> {
|
||||
try {
|
||||
const x = await getPr(PR_SCHEMA, prNumber, git);
|
||||
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.
|
||||
*/
|
||||
export class GithubApiMergeStrategy extends MergeStrategy {
|
||||
constructor(git: GitClient, private _config: GithubApiMergeStrategyConfig) {
|
||||
constructor(git: GitClient<true>, private _config: GithubApiMergeStrategyConfig) {
|
||||
super(git);
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ export const TEMP_PR_HEAD_BRANCH = 'merge_pr_head';
|
|||
* merges it into the determined target branches.
|
||||
*/
|
||||
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
|
||||
|
@ -124,7 +124,8 @@ export abstract class MergeStrategy {
|
|||
});
|
||||
// Fetch all target branches with a single command. We don't want to fetch them
|
||||
// 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. */
|
||||
|
@ -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.
|
||||
// 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;
|
||||
|
||||
constructor(
|
||||
public config: MergeConfigWithRemote, public git: GitClient,
|
||||
public config: MergeConfigWithRemote, public git: GitClient<true>,
|
||||
flags: Partial<PullRequestMergeTaskFlags>) {
|
||||
// Update flags property with the provided flags values as patches to the default flag values.
|
||||
this.flags = {...defaultPullRequestMergeTaskFlags, ...flags};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* 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 {getCommitsInRange} from '../../commit-message/utils';
|
||||
|
@ -16,24 +16,24 @@ import {addTokenToGitHttpsUrl} from '../../utils/git/github-urls';
|
|||
import {GitClient} from '../../utils/git/index';
|
||||
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 = {
|
||||
state: graphQLTypes.string,
|
||||
maintainerCanModify: graphQLTypes.boolean,
|
||||
viewerDidAuthor: graphQLTypes.boolean,
|
||||
headRefOid: graphQLTypes.string,
|
||||
state: graphqlTypes.string,
|
||||
maintainerCanModify: graphqlTypes.boolean,
|
||||
viewerDidAuthor: graphqlTypes.boolean,
|
||||
headRefOid: graphqlTypes.string,
|
||||
headRef: {
|
||||
name: graphQLTypes.string,
|
||||
name: graphqlTypes.string,
|
||||
repository: {
|
||||
url: graphQLTypes.string,
|
||||
nameWithOwner: graphQLTypes.string,
|
||||
url: graphqlTypes.string,
|
||||
nameWithOwner: graphqlTypes.string,
|
||||
},
|
||||
},
|
||||
baseRef: {
|
||||
name: graphQLTypes.string,
|
||||
name: graphqlTypes.string,
|
||||
repository: {
|
||||
url: graphQLTypes.string,
|
||||
nameWithOwner: graphQLTypes.string,
|
||||
url: graphqlTypes.string,
|
||||
nameWithOwner: graphqlTypes.string,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -44,7 +44,8 @@ const PR_SCHEMA = {
|
|||
*/
|
||||
export async function rebasePr(
|
||||
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.
|
||||
if (git.hasLocalChanges()) {
|
||||
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. */
|
||||
isActive(active: ActiveReleaseTrains): Promise<boolean>;
|
||||
/** 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;
|
||||
|
||||
constructor(
|
||||
protected active: ActiveReleaseTrains, protected git: GitClient,
|
||||
protected active: ActiveReleaseTrains, protected git: GitClient<true>,
|
||||
protected config: ReleaseConfig, protected projectDir: string) {}
|
||||
|
||||
/** 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 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;
|
||||
|
||||
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. */
|
||||
protected async pushHeadToRemoteBranch(branchName: string) {
|
||||
// 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. */
|
||||
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']);
|
||||
}
|
||||
|
||||
|
|
|
@ -28,8 +28,8 @@ export enum CompletionState {
|
|||
}
|
||||
|
||||
export class ReleaseTool {
|
||||
/** Client for interacting with the Github API and the local Git command. */
|
||||
private _git = new GitClient(this._githubToken, {github: this._github}, this._projectRoot);
|
||||
/** The singleton instance of the GitClient. */
|
||||
private _git = GitClient.getAuthenticatedInstance();
|
||||
/** The previous git commit to return back to after the release tool runs. */
|
||||
private previousGitBranchOrRevision = this._git.getCurrentBranchOrRevision();
|
||||
|
||||
|
|
|
@ -16,7 +16,8 @@ const THIRTY_SECONDS_IN_MS = 30000;
|
|||
export type PullRequestState = 'merged'|'closed'|'open';
|
||||
|
||||
/** 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});
|
||||
if (data.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
|
||||
* 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 =
|
||||
api.github.issues.listEvents.endpoint.merge({...api.remoteParams, issue_number: id});
|
||||
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. */
|
||||
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});
|
||||
// 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.
|
||||
|
|
|
@ -38,7 +38,7 @@ export function getLocalChangelogFilePath(projectDir: string): string {
|
|||
/** Release note generation. */
|
||||
export class ReleaseNotes {
|
||||
/** An instance of GitClient. */
|
||||
private git = new GitClient();
|
||||
private git = GitClient.getInstance();
|
||||
/** The github configuration. */
|
||||
private readonly github = getConfig().github;
|
||||
/** 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. */
|
||||
export interface TestReleaseAction<T extends ReleaseAction = ReleaseAction> {
|
||||
instance: T;
|
||||
gitClient: VirtualGitClient;
|
||||
gitClient: VirtualGitClient<boolean>;
|
||||
repo: GithubTestingRepo;
|
||||
fork: GithubTestingRepo;
|
||||
testTmpDir: string;
|
||||
|
@ -44,7 +44,7 @@ export interface TestReleaseAction<T extends ReleaseAction = ReleaseAction> {
|
|||
/** Gets necessary test mocks for running a release action. */
|
||||
export function getTestingMocksForReleaseAction() {
|
||||
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 = {
|
||||
npmPackages: [
|
||||
'@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. */
|
||||
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}`;
|
||||
}
|
||||
|
|
|
@ -7,8 +7,11 @@
|
|||
*/
|
||||
|
||||
import {Argv} from 'yargs';
|
||||
|
||||
import {error, red, yellow} from '../console';
|
||||
|
||||
import {GITHUB_TOKEN_GENERATE_URL} from './github-urls';
|
||||
import {GitClient} from './index';
|
||||
|
||||
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}`));
|
||||
process.exit(1);
|
||||
}
|
||||
GitClient.authenticateWithToken(githubToken);
|
||||
return githubToken;
|
||||
},
|
||||
})
|
||||
|
|
|
@ -11,6 +11,12 @@ import * as Octokit from '@octokit/rest';
|
|||
import {RequestParameters} from '@octokit/types';
|
||||
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. */
|
||||
export interface GithubRepo {
|
||||
/** 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.
|
||||
*
|
||||
|
@ -33,13 +42,15 @@ export class GithubApiRequestError extends Error {
|
|||
* would provide value from memoized style responses.
|
||||
**/
|
||||
export class GithubClient extends Octokit {
|
||||
/** The Github GraphQL (v4) API. */
|
||||
graphql: GithubGraphqlClient;
|
||||
|
||||
/** The current user based on checking against the Github API. */
|
||||
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.
|
||||
super({auth: token});
|
||||
|
||||
|
@ -49,8 +60,22 @@ export class GithubClient extends Octokit {
|
|||
throw new GithubApiRequestError(error.status, error.message);
|
||||
});
|
||||
|
||||
// Create authenticated graphql client.
|
||||
this.graphql = new GithubGraphqlClient(token);
|
||||
// Note: The prototype must be set explictly as Github's Octokit class is a non-standard class
|
||||
// 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. */
|
||||
|
@ -59,7 +84,7 @@ export class GithubClient extends Octokit {
|
|||
if (this._currentUser !== null) {
|
||||
return this._currentUser;
|
||||
}
|
||||
const result = await this.graphql.query({
|
||||
const result = await this.graphql({
|
||||
viewer: {
|
||||
login: types.string,
|
||||
}
|
||||
|
@ -67,29 +92,3 @@ export class GithubClient extends Octokit {
|
|||
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 {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 {DryRunError, isDryRun} from '../dry-run';
|
||||
import {GithubClient} from './github';
|
||||
|
@ -26,7 +26,7 @@ export type OAuthScopeTestFunction = (scopes: string[], missing: string[]) => vo
|
|||
|
||||
/** Error for failed Git commands. */
|
||||
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
|
||||
// accidentally leak the Github token that might be used in a command,
|
||||
// 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
|
||||
* the dev-infra configuration is loaded with its Github configuration.
|
||||
**/
|
||||
export class GitClient {
|
||||
/** Whether verbose logging of Git actions should be used. */
|
||||
static LOG_COMMANDS = true;
|
||||
/** 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};
|
||||
/** Git URL that resolves to the configured repository. */
|
||||
repoGitUrl = getRepositoryGitUrl(this.remoteConfig, this.githubToken);
|
||||
/** Instance of the authenticated Github octokit API. */
|
||||
github = new GithubClient(this.githubToken);
|
||||
export class GitClient<Authenticated extends boolean> {
|
||||
/*************************************************
|
||||
* Singleton definition and configuration. *
|
||||
*************************************************/
|
||||
/** The singleton instance of the authenticated GitClient. */
|
||||
private static authenticated: GitClient<true>;
|
||||
/** The singleton instance of the unauthenticated GitClient. */
|
||||
private static unauthenticated: GitClient<false>;
|
||||
|
||||
/**
|
||||
* 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. */
|
||||
private _cachedOauthScopes: Promise<string[]>|null = null;
|
||||
/**
|
||||
|
@ -62,18 +93,36 @@ export class GitClient {
|
|||
* sanitizing the token from Git child process output.
|
||||
*/
|
||||
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(),
|
||||
private _projectRoot = getRepoBaseDir()) {
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
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
|
||||
// also create a regular expression that can be used for sanitizing Git command output
|
||||
// so that it does not print the token accidentally.
|
||||
if (githubToken != null) {
|
||||
if (typeof githubToken === 'string') {
|
||||
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. */
|
||||
run(args: string[], options?: SpawnSyncOptions): Omit<SpawnSyncReturns<string>, 'status'> {
|
||||
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
|
||||
// commands to better understand the git actions occuring. Depending on the command being
|
||||
// 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
|
||||
// to share errors with others if the tool failed, and we do not want to leak tokens.
|
||||
printFn('Executing: git', this.omitGithubTokenFromMessage(args.join(' ')));
|
||||
|
@ -126,6 +175,11 @@ export class GitClient {
|
|||
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. */
|
||||
hasCommit(branchName: string, sha: string): boolean {
|
||||
return this.run(['branch', branchName, '--contains', sha]).stdout !== '';
|
||||
|
|
|
@ -11,10 +11,10 @@ import {params, types} from 'typed-graphqlify';
|
|||
import {GitClient} from './git/index';
|
||||
|
||||
/** 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 */
|
||||
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(
|
||||
{
|
||||
$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;
|
||||
}
|
||||
|
||||
/** 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 */
|
||||
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(
|
||||
{
|
||||
$first: 'Int', // How many entries to get with each request
|
||||
|
@ -75,7 +75,7 @@ export async function getPendingPrs<PrSchema>(prSchema: PrSchema, git: GitClient
|
|||
owner,
|
||||
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);
|
||||
hasNextPage = results.repository.pullRequests.pageInfo.hasNextPage;
|
||||
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
|
||||
* 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. */
|
||||
fetchHeadRef: RemoteRef|null = null;
|
||||
/** List of known branches in the repository. */
|
||||
|
@ -190,10 +204,10 @@ export class VirtualGitClient extends GitClient {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builds a Virtual Git Client instance with the provided config and set the temporary test
|
||||
* directory.
|
||||
*/
|
||||
export function buildVirtualGitClient(config = mockNgDevConfig, tmpDir = testTmpDir) {
|
||||
return (new VirtualGitClient(undefined, config, tmpDir));
|
||||
export function installVirtualGitClientSpies() {
|
||||
const authenticatedVirtualGitClient = VirtualGitClient.getAuthenticatedInstance();
|
||||
spyOn(GitClient, 'getAuthenticatedInstance').and.returnValue(authenticatedVirtualGitClient);
|
||||
|
||||
const unauthenticatedVirtualGitClient = VirtualGitClient.getInstance();
|
||||
spyOn(GitClient, 'getInstance').and.returnValue(unauthenticatedVirtualGitClient);
|
||||
}
|
||||
|
|
|
@ -27,13 +27,13 @@ export function getBranchPushMatcher(options: BranchPushMatchParameters) {
|
|||
const {targetRepo, targetBranch, baseBranch, baseRepo, expectedCommits} = options;
|
||||
return jasmine.objectContaining({
|
||||
remote: {
|
||||
repoUrl: `https://github.com/${targetRepo.owner}/${targetRepo.name}.git`,
|
||||
repoUrl: `https://abc123@github.com/${targetRepo.owner}/${targetRepo.name}.git`,
|
||||
name: `refs/heads/${targetBranch}`
|
||||
},
|
||||
head: jasmine.objectContaining({
|
||||
newCommits: expectedCommits,
|
||||
ref: {
|
||||
repoUrl: `https://github.com/${baseRepo.owner}/${baseRepo.name}.git`,
|
||||
repoUrl: `https://abc123@github.com/${baseRepo.owner}/${baseRepo.name}.git`,
|
||||
name: baseBranch,
|
||||
},
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue