diff --git a/dev-infra/caretaker/check/cli.ts b/dev-infra/caretaker/check/cli.ts index 8e50c7f261..1988381423 100644 --- a/dev-infra/caretaker/check/cli.ts +++ b/dev-infra/caretaker/check/cli.ts @@ -8,7 +8,7 @@ import {Arguments, Argv, CommandModule} from 'yargs'; -import {addGithubTokenFlag} from '../../utils/yargs'; +import {addGithubTokenOption} from '../../utils/git/github-yargs'; import {checkServiceStatuses} from './check'; @@ -17,12 +17,9 @@ export interface CaretakerCheckOptions { githubToken: string; } -/** URL to the Github page where personal access tokens can be generated. */ -export const GITHUB_TOKEN_GENERATE_URL = `https://github.com/settings/tokens`; - /** Builds the command. */ function builder(yargs: Argv) { - return addGithubTokenFlag(yargs); + return addGithubTokenOption(yargs); } /** Handles the command. */ diff --git a/dev-infra/pr/checkout/cli.ts b/dev-infra/pr/checkout/cli.ts index 0aabc8f5c6..1ea44bd937 100644 --- a/dev-infra/pr/checkout/cli.ts +++ b/dev-infra/pr/checkout/cli.ts @@ -8,7 +8,7 @@ import {Arguments, Argv, CommandModule} from 'yargs'; -import {addGithubTokenFlag} from '../../utils/yargs'; +import {addGithubTokenOption} from '../../utils/git/github-yargs'; import {checkOutPullRequestLocally} from '../common/checkout-pr'; export interface CheckoutOptions { @@ -18,7 +18,7 @@ export interface CheckoutOptions { /** Builds the checkout pull request command. */ function builder(yargs: Argv) { - return addGithubTokenFlag(yargs).positional('prNumber', {type: 'number', demandOption: true}); + return addGithubTokenOption(yargs).positional('prNumber', {type: 'number', demandOption: true}); } /** Handles the checkout pull request command. */ diff --git a/dev-infra/pr/common/checkout-pr.ts b/dev-infra/pr/common/checkout-pr.ts index 5fd8a4a36f..dcb7be2e68 100644 --- a/dev-infra/pr/common/checkout-pr.ts +++ b/dev-infra/pr/common/checkout-pr.ts @@ -7,10 +7,10 @@ */ import {types as graphQLTypes} from 'typed-graphqlify'; -import {URL} from 'url'; import {info} from '../../utils/console'; import {GitClient} from '../../utils/git'; +import {addTokenToGitHttpsUrl} from '../../utils/git/github-urls'; import {getPr} from '../../utils/github'; /* GraphQL schema for the response body for a pending PR. */ @@ -83,7 +83,7 @@ export async function checkOutPullRequestLocally( /** The full ref for the repository and branch the PR came from. */ const fullHeadRef = `${pr.headRef.repository.nameWithOwner}:${headRefName}`; /** The full URL path of the repository the PR came from with github token as authentication. */ - const headRefUrl = addAuthenticationToUrl(pr.headRef.repository.url, githubToken); + const headRefUrl = addTokenToGitHttpsUrl(pr.headRef.repository.url, githubToken); // Note: Since we use a detached head for rebasing the PR and therefore do not have // remote-tracking branches configured, we need to set our expected ref and SHA. This // allows us to use `--force-with-lease` for the detached head while ensuring that we @@ -126,10 +126,3 @@ export async function checkOutPullRequestLocally( } }; } - -/** Adds the provided token as username to the provided url. */ -function addAuthenticationToUrl(urlString: string, token: string) { - const url = new URL(urlString); - url.username = token; - return url.toString(); -} diff --git a/dev-infra/pr/merge/cli.ts b/dev-infra/pr/merge/cli.ts index d5b6ac4f46..0c2600351a 100644 --- a/dev-infra/pr/merge/cli.ts +++ b/dev-infra/pr/merge/cli.ts @@ -8,7 +8,7 @@ import {Arguments, Argv} from 'yargs'; -import {addGithubTokenFlag} from '../../utils/yargs'; +import {addGithubTokenOption} from '../../utils/git/github-yargs'; import {mergePullRequest} from './index'; @@ -20,7 +20,7 @@ export interface MergeCommandOptions { /** Builds the options for the merge command. */ export function buildMergeCommand(yargs: Argv): Argv { - return addGithubTokenFlag(yargs).help().strict().positional( + return addGithubTokenOption(yargs).help().strict().positional( 'pr-number', {demandOption: true, type: 'number'}); } diff --git a/dev-infra/pr/merge/index.ts b/dev-infra/pr/merge/index.ts index 3f53b3d85e..d6e1af255d 100644 --- a/dev-infra/pr/merge/index.ts +++ b/dev-infra/pr/merge/index.ts @@ -11,12 +11,11 @@ import {getConfig, getRepoBaseDir} from '../../utils/config'; import {error, green, info, promptConfirm, red, yellow} from '../../utils/console'; import {GitClient} from '../../utils/git'; import {GithubApiRequestError} from '../../utils/git/github'; -import {GITHUB_TOKEN_GENERATE_URL} from '../../utils/yargs'; +import {GITHUB_TOKEN_GENERATE_URL} from '../../utils/git/github-urls'; import {loadAndValidateConfig, MergeConfigWithRemote} from './config'; import {MergeResult, MergeStatus, PullRequestMergeTask} from './task'; - /** * Merges a given pull request based on labels configured in the given merge configuration. * Pull requests can be merged with different strategies such as the Github API merge diff --git a/dev-infra/pr/rebase/cli.ts b/dev-infra/pr/rebase/cli.ts index 98c44bf7f5..6b1c0c05f1 100644 --- a/dev-infra/pr/rebase/cli.ts +++ b/dev-infra/pr/rebase/cli.ts @@ -8,7 +8,7 @@ import {Arguments, Argv} from 'yargs'; -import {addGithubTokenFlag} from '../../utils/yargs'; +import {addGithubTokenOption} from '../../utils/git/github-yargs'; import {rebasePr} from './index'; @@ -20,7 +20,7 @@ export interface RebaseCommandOptions { /** Builds the rebase pull request command. */ export function buildRebaseCommand(yargs: Argv): Argv { - return addGithubTokenFlag(yargs).positional('prNumber', {type: 'number', demandOption: true}); + return addGithubTokenOption(yargs).positional('prNumber', {type: 'number', demandOption: true}); } /** Handles the rebase pull request command. */ diff --git a/dev-infra/pr/rebase/index.ts b/dev-infra/pr/rebase/index.ts index 149de6513a..b79abb2a10 100644 --- a/dev-infra/pr/rebase/index.ts +++ b/dev-infra/pr/rebase/index.ts @@ -12,6 +12,7 @@ import {URL} from 'url'; import {getConfig, NgDevConfig} from '../../utils/config'; import {error, info, promptConfirm} from '../../utils/console'; import {GitClient} from '../../utils/git'; +import {addTokenToGitHttpsUrl} from '../../utils/git/github-urls'; import {getPr} from '../../utils/github'; /* GraphQL schema for the response body for each pending PR. */ @@ -61,8 +62,8 @@ export async function rebasePr( const baseRefName = pr.baseRef.name; const fullHeadRef = `${pr.headRef.repository.nameWithOwner}:${headRefName}`; const fullBaseRef = `${pr.baseRef.repository.nameWithOwner}:${baseRefName}`; - const headRefUrl = addAuthenticationToUrl(pr.headRef.repository.url, githubToken); - const baseRefUrl = addAuthenticationToUrl(pr.baseRef.repository.url, githubToken); + const headRefUrl = addTokenToGitHttpsUrl(pr.headRef.repository.url, githubToken); + const baseRefUrl = addTokenToGitHttpsUrl(pr.baseRef.repository.url, githubToken); // Note: Since we use a detached head for rebasing the PR and therefore do not have // remote-tracking branches configured, we need to set our expected ref and SHA. This @@ -140,10 +141,3 @@ export async function rebasePr( git.runGraceful(['checkout', previousBranchOrRevision], {stdio: 'ignore'}); } } - -/** Adds the provided token as username to the provided url. */ -function addAuthenticationToUrl(urlString: string, token: string) { - const url = new URL(urlString); - url.username = token; - return url.toString(); -} diff --git a/dev-infra/utils/git/github-urls.ts b/dev-infra/utils/git/github-urls.ts new file mode 100644 index 0000000000..1e6a3735f7 --- /dev/null +++ b/dev-infra/utils/git/github-urls.ts @@ -0,0 +1,36 @@ +/** + * @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 + */ + + +import {URL} from 'url'; +import {GithubConfig} from '../config'; + +/** URL to the Github page where personal access tokens can be managed. */ +export const GITHUB_TOKEN_SETTINGS_URL = `https://github.com/settings/tokens`; + +/** URL to the Github page where personal access tokens can be generated. */ +export const GITHUB_TOKEN_GENERATE_URL = `https://github.com/settings/tokens/new`; + +/** Adds the provided token to the given Github HTTPs remote url. */ +export function addTokenToGitHttpsUrl(githubHttpsUrl: string, token: string) { + const url = new URL(githubHttpsUrl); + url.username = token; + return url.toString(); +} + +/** Gets the repository Git URL for the given github config. */ +export function getRepositoryGitUrl(config: GithubConfig, githubToken?: string): string { + if (config.useSsh) { + return `git@github.com:${config.owner}/${config.name}.git`; + } + const baseHttpUrl = `https://github.com/${config.owner}/${config.name}.git`; + if (githubToken !== undefined) { + return addTokenToGitHttpsUrl(baseHttpUrl, githubToken); + } + return baseHttpUrl; +} diff --git a/dev-infra/utils/yargs.ts b/dev-infra/utils/git/github-yargs.ts similarity index 82% rename from dev-infra/utils/yargs.ts rename to dev-infra/utils/git/github-yargs.ts index bc42985aea..ecd615d57e 100644 --- a/dev-infra/utils/yargs.ts +++ b/dev-infra/utils/git/github-yargs.ts @@ -7,11 +7,13 @@ */ import {Argv} from 'yargs'; -import {error, red, yellow} from './console'; +import {error, red, yellow} from '../console'; +import {GITHUB_TOKEN_GENERATE_URL} from './github-urls'; export type ArgvWithGithubToken = Argv<{githubToken: string}>; -export function addGithubTokenFlag(yargs: Argv): ArgvWithGithubToken { +/** Sets up the `github-token` command option for the given Yargs instance. */ +export function addGithubTokenOption(yargs: Argv): ArgvWithGithubToken { 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` @@ -32,6 +34,3 @@ export function addGithubTokenFlag(yargs: Argv): ArgvWithGithubToken { }) .default('github-token' as 'githubToken', '', ''); } - -/** URL to the Github page where personal access tokens can be generated. */ -export const GITHUB_TOKEN_GENERATE_URL = 'https://github.com/settings/tokens/new'; diff --git a/dev-infra/utils/git/github.ts b/dev-infra/utils/git/github.ts index cea6979cd7..158bd66775 100644 --- a/dev-infra/utils/git/github.ts +++ b/dev-infra/utils/git/github.ts @@ -84,7 +84,6 @@ class GithubGraphqlClient { } } - /** Perform a query using Github's GraphQL API. */ async query(queryObject: T, params: RequestParameters = {}) { const queryString = query(queryObject); diff --git a/dev-infra/utils/git/index.ts b/dev-infra/utils/git/index.ts index e5f6a1dbf3..c533c75575 100644 --- a/dev-infra/utils/git/index.ts +++ b/dev-infra/utils/git/index.ts @@ -12,6 +12,7 @@ import {spawnSync, SpawnSyncOptions, SpawnSyncReturns} from 'child_process'; import {getConfig, getRepoBaseDir, NgDevConfig} from '../config'; import {info, yellow} from '../console'; import {GithubClient} from './github'; +import {getRepositoryGitUrl, GITHUB_TOKEN_GENERATE_URL, GITHUB_TOKEN_SETTINGS_URL} from './github-urls'; /** Github response type extended to include the `x-oauth-scopes` headers presence. */ type RateLimitResponseWithOAuthScopeHeader = Octokit.Response&{ @@ -45,11 +46,8 @@ export class GitClient { remoteConfig = this._config.github; /** Octokit request parameters object for targeting the configured remote. */ remoteParams = {owner: this.remoteConfig.owner, repo: this.remoteConfig.name}; - /** URL that resolves to the configured repository. */ - repoGitUrl = this.remoteConfig.useSsh ? - `git@github.com:${this.remoteConfig.owner}/${this.remoteConfig.name}.git` : - `https://${this._githubToken}@github.com/${this.remoteConfig.owner}/${ - this.remoteConfig.name}.git`; + /** 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); @@ -191,8 +189,8 @@ export class GitClient { `The provided does not have required permissions due to missing scope(s): ` + `${yellow(missingScopes.join(', '))}\n\n` + `Update the token in use at:\n` + - ` https://github.com/settings/tokens\n\n` + - `Alternatively, a new token can be created at: https://github.com/settings/tokens/new\n`; + ` ${GITHUB_TOKEN_SETTINGS_URL}\n\n` + + `Alternatively, a new token can be created at: ${GITHUB_TOKEN_GENERATE_URL}\n`; return {error}; } @@ -202,12 +200,12 @@ export class GitClient { **/ private async getAuthScopesForToken() { // If the OAuth scopes have already been loaded, return the Promise containing them. - if (this._oauthScopes !== null) { - return this._oauthScopes; + if (this._cachedOauthScopes !== null) { + return this._cachedOauthScopes; } // OAuth scopes are loaded via the /rate_limit endpoint to prevent // usage of a request against that rate_limit for this lookup. - return this._oauthScopes = this.github.rateLimit.get().then(_response => { + return this._cachedOauthScopes = this.github.rateLimit.get().then(_response => { const response = _response as RateLimitResponseWithOAuthScopeHeader; const scopes: string = response.headers['x-oauth-scopes'] || ''; return scopes.split(',').map(scope => scope.trim());