2020-10-26 22:42:23 -04:00
#!/usr/bin/env node
2020-10-01 19:06:56 -04:00
'use strict' ;
function _interopDefault ( ex ) { return ( ex && ( typeof ex === 'object' ) && 'default' in ex ) ? ex [ 'default' ] : ex ; }
var yargs = require ( 'yargs' ) ;
var tslib = require ( 'tslib' ) ;
2021-04-21 02:38:13 -04:00
var chalk = require ( 'chalk' ) ;
2020-10-01 19:06:56 -04:00
var fs = require ( 'fs' ) ;
var inquirer = require ( 'inquirer' ) ;
var path = require ( 'path' ) ;
var child _process = require ( 'child_process' ) ;
2021-04-01 19:17:04 -04:00
var semver = require ( 'semver' ) ;
2020-10-01 19:06:56 -04:00
var graphql = require ( '@octokit/graphql' ) ;
var Octokit = require ( '@octokit/rest' ) ;
var typedGraphqlify = require ( 'typed-graphqlify' ) ;
2021-04-12 17:50:50 -04:00
var url = require ( 'url' ) ;
2020-10-01 19:06:56 -04:00
var fetch = _interopDefault ( require ( 'node-fetch' ) ) ;
var multimatch = require ( 'multimatch' ) ;
var yaml = require ( 'yaml' ) ;
2021-03-19 15:11:21 -04:00
var conventionalCommitsParser = require ( 'conventional-commits-parser' ) ;
2021-03-24 19:17:15 -04:00
var gitCommits _ = require ( 'git-raw-commits' ) ;
2020-10-01 19:06:56 -04:00
var cliProgress = require ( 'cli-progress' ) ;
var os = require ( 'os' ) ;
2021-04-12 17:50:50 -04:00
var shelljs = require ( 'shelljs' ) ;
2020-10-01 19:06:56 -04:00
var minimatch = require ( 'minimatch' ) ;
var ora = require ( 'ora' ) ;
2021-04-19 14:58:32 -04:00
var ejs = require ( 'ejs' ) ;
2020-10-01 19:06:56 -04:00
var glob = require ( 'glob' ) ;
var ts = require ( 'typescript' ) ;
/ * *
* @ 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
* /
/** Whether ts-node has been installed and is available to ng-dev. */
function isTsNodeAvailable ( ) {
try {
require . resolve ( 'ts-node' ) ;
return true ;
}
catch ( _a ) {
return false ;
}
}
/ * *
* @ 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
* /
/ * *
* The filename expected for creating the ng - dev config , without the file
* extension to allow either a typescript or javascript file to be used .
* /
var CONFIG _FILE _PATH = '.ng-dev/config' ;
/** The configuration for ng-dev. */
var cachedConfig = null ;
/ * *
* The filename expected for local user config , without the file extension to allow a typescript ,
* javascript or json file to be used .
* /
var USER _CONFIG _FILE _PATH = '.ng-dev.user' ;
/** The local user configuration for ng-dev. */
var userConfig = null ;
2021-04-12 17:50:50 -04:00
function getConfig ( baseDir ) {
2020-10-01 19:06:56 -04:00
// If the global config is not defined, load it from the file system.
if ( cachedConfig === null ) {
2021-04-30 12:19:10 -04:00
baseDir = baseDir || GitClient . getInstance ( ) . baseDir ;
2020-10-01 19:06:56 -04:00
// The full path to the configuration file.
2021-04-12 17:50:50 -04:00
var configPath = path . join ( baseDir , CONFIG _FILE _PATH ) ;
2020-10-01 19:06:56 -04:00
// Read the configuration and validate it before caching it for the future.
cachedConfig = validateCommonConfig ( readConfigFile ( configPath ) ) ;
}
// Return a clone of the cached global config to ensure that a new instance of the config
// is returned each time, preventing unexpected effects of modifications to the config object.
return tslib . _ _assign ( { } , cachedConfig ) ;
}
/** Validate the common configuration has been met for the ng-dev command. */
function validateCommonConfig ( config ) {
var errors = [ ] ;
// Validate the github configuration.
if ( config . github === undefined ) {
errors . push ( "Github repository not configured. Set the \"github\" option." ) ;
}
else {
if ( config . github . name === undefined ) {
errors . push ( "\"github.name\" is not defined" ) ;
}
if ( config . github . owner === undefined ) {
errors . push ( "\"github.owner\" is not defined" ) ;
}
}
assertNoErrors ( errors ) ;
return config ;
}
/ * *
* Resolves and reads the specified configuration file , optionally returning an empty object if the
* configuration file cannot be read .
* /
function readConfigFile ( configPath , returnEmptyObjectOnError ) {
if ( returnEmptyObjectOnError === void 0 ) { returnEmptyObjectOnError = false ; }
2021-01-14 14:03:58 -05:00
// If the `.ts` extension has not been set up already, and a TypeScript based
2020-10-01 19:06:56 -04:00
// version of the given configuration seems to exist, set up `ts-node` if available.
if ( require . extensions [ '.ts' ] === undefined && fs . existsSync ( configPath + ".ts" ) &&
isTsNodeAvailable ( ) ) {
// Ensure the module target is set to `commonjs`. This is necessary because the
// dev-infra tool runs in NodeJS which does not support ES modules by default.
// Additionally, set the `dir` option to the directory that contains the configuration
// file. This allows for custom compiler options (such as `--strict`).
require ( 'ts-node' ) . register ( { dir : path . dirname ( configPath ) , transpileOnly : true , compilerOptions : { module : 'commonjs' } } ) ;
}
try {
return require ( configPath ) ;
}
catch ( e ) {
if ( returnEmptyObjectOnError ) {
debug ( "Could not read configuration file at " + configPath + ", returning empty object instead." ) ;
debug ( e ) ;
return { } ;
}
error ( "Could not read configuration file at " + configPath + "." ) ;
error ( e ) ;
process . exit ( 1 ) ;
}
}
/ * *
* Asserts the provided array of error messages is empty . If any errors are in the array ,
* logs the errors and exit the process as a failure .
* /
function assertNoErrors ( errors ) {
var e _1 , _a ;
if ( errors . length == 0 ) {
return ;
}
error ( "Errors discovered while loading configuration file:" ) ;
try {
for ( var errors _1 = tslib . _ _values ( errors ) , errors _1 _1 = errors _1 . next ( ) ; ! errors _1 _1 . done ; errors _1 _1 = errors _1 . next ( ) ) {
var err = errors _1 _1 . value ;
error ( " - " + err ) ;
}
}
catch ( e _1 _1 ) { e _1 = { error : e _1 _1 } ; }
finally {
try {
if ( errors _1 _1 && ! errors _1 _1 . done && ( _a = errors _1 . return ) ) _a . call ( errors _1 ) ;
}
finally { if ( e _1 ) throw e _1 . error ; }
}
process . exit ( 1 ) ;
}
/ * *
* Get the local user configuration from the file system , returning the already loaded copy if it is
* defined .
*
* @ returns The user configuration object , or an empty object if no user configuration file is
* present . The object is an untyped object as there are no required user configurations .
* /
function getUserConfig ( ) {
// If the global config is not defined, load it from the file system.
if ( userConfig === null ) {
2021-04-12 17:50:50 -04:00
var git = GitClient . getInstance ( ) ;
2020-10-01 19:06:56 -04:00
// The full path to the configuration file.
2021-04-12 17:50:50 -04:00
var configPath = path . join ( git . baseDir , USER _CONFIG _FILE _PATH ) ;
2020-10-01 19:06:56 -04:00
// Set the global config object.
userConfig = readConfigFile ( configPath , true ) ;
}
// Return a clone of the user config to ensure that a new instance of the config is returned
// each time, preventing unexpected effects of modifications to the config object.
return tslib . _ _assign ( { } , userConfig ) ;
}
2021-04-01 16:49:09 -04:00
/ * *
* @ 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
* /
/** Whether the current environment is in dry run mode. */
function isDryRun ( ) {
return process . env [ 'DRY_RUN' ] !== undefined ;
}
/** Error to be thrown when a function or method is called in dryRun mode and shouldn't be. */
var DryRunError = /** @class */ ( function ( _super ) {
tslib . _ _extends ( DryRunError , _super ) ;
function DryRunError ( ) {
var _this = _super . call ( this , 'Cannot call this function in dryRun mode.' ) || this ;
// Set the prototype explicitly because in ES5, the prototype is accidentally lost due to
// a limitation in down-leveling.
// https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work.
Object . setPrototypeOf ( _this , DryRunError . prototype ) ;
return _this ;
}
return DryRunError ;
} ( Error ) ) ;
2020-10-01 19:06:56 -04:00
/ * *
* @ 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
* /
/** Error for failed Github API requests. */
var GithubApiRequestError = /** @class */ ( function ( _super ) {
tslib . _ _extends ( GithubApiRequestError , _super ) ;
function GithubApiRequestError ( status , message ) {
var _this = _super . call ( this , message ) || this ;
_this . status = status ;
return _this ;
}
return GithubApiRequestError ;
} ( Error ) ) ;
2021-04-08 15:34:55 -04:00
/** 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 ) ) ;
2020-10-01 19:06:56 -04:00
/ * *
* A Github client for interacting with the Github APIs .
*
* Additionally , provides convenience methods for actions which require multiple requests , or
* would provide value from memoized style responses .
* * /
var GithubClient = /** @class */ ( function ( _super ) {
tslib . _ _extends ( GithubClient , _super ) ;
2021-04-08 15:34:55 -04:00
/ * *
* @ param token The github authentication token for Github Rest and Graphql API requests .
* /
2020-10-01 19:06:56 -04:00
function GithubClient ( token ) {
2020-10-28 13:47:22 -04:00
var _this =
2020-10-01 19:06:56 -04:00
// Pass in authentication token to base Octokit class.
_super . call ( this , { auth : token } ) || this ;
2021-04-08 15:34:55 -04:00
_this . token = token ;
2020-10-01 19:06:56 -04:00
/** The current user based on checking against the Github API. */
_this . _currentUser = null ;
2021-04-08 15:34:55 -04:00
/** The graphql instance with authentication set during construction. */
_this . _graphql = graphql . graphql . defaults ( { headers : { authorization : "token " + _this . token } } ) ;
2020-10-01 19:06:56 -04:00
_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 ) ;
} ) ;
2021-04-08 15:34:55 -04:00
// 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 ) ;
2020-10-01 19:06:56 -04:00
return _this ;
}
2021-04-08 15:34:55 -04:00
/** 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()`.' ) ;
}
2021-04-21 08:23:57 -04:00
return [ 4 /*yield*/ , this . _graphql ( typedGraphqlify . query ( queryObject ) . toString ( ) , params ) ] ;
2021-04-08 15:34:55 -04:00
case 1 : return [ 2 /*return*/ , ( _a . sent ( ) ) ] ;
}
} ) ;
} ) ;
} ;
2020-10-01 19:06:56 -04:00
/** Retrieve the login of the current user from Github. */
GithubClient . prototype . getCurrentUser = function ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
var result ;
return tslib . _ _generator ( this , function ( _a ) {
switch ( _a . label ) {
case 0 :
// If the current user has already been retrieved return the current user value again.
if ( this . _currentUser !== null ) {
return [ 2 /*return*/ , this . _currentUser ] ;
}
2021-04-08 15:34:55 -04:00
return [ 4 /*yield*/ , this . graphql ( {
2020-10-01 19:06:56 -04:00
viewer : {
login : typedGraphqlify . types . string ,
}
} ) ] ;
case 1 :
result = _a . sent ( ) ;
return [ 2 /*return*/ , this . _currentUser = result . viewer . login ] ;
}
} ) ;
} ) ;
} ;
return GithubClient ;
} ( Octokit ) ) ;
2021-04-12 17:50:50 -04:00
/ * *
* @ 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
* /
/** URL to the Github page where personal access tokens can be managed. */
var GITHUB _TOKEN _SETTINGS _URL = 'https://github.com/settings/tokens' ;
/** URL to the Github page where personal access tokens can be generated. */
var GITHUB _TOKEN _GENERATE _URL = 'https://github.com/settings/tokens/new' ;
/** Adds the provided token to the given Github HTTPs remote url. */
function addTokenToGitHttpsUrl ( githubHttpsUrl , token ) {
var url$1 = new url . URL ( githubHttpsUrl ) ;
url$1 . username = token ;
return url$1 . href ;
}
/** Gets the repository Git URL for the given github config. */
function getRepositoryGitUrl ( config , githubToken ) {
if ( config . useSsh ) {
return "git@github.com:" + config . owner + "/" + config . name + ".git" ;
}
var baseHttpUrl = "https://github.com/" + config . owner + "/" + config . name + ".git" ;
if ( githubToken !== undefined ) {
return addTokenToGitHttpsUrl ( baseHttpUrl , githubToken ) ;
}
return baseHttpUrl ;
}
/** Gets a Github URL that refers to a list of recent commits within a specified branch. */
function getListCommitsInBranchUrl ( _a , branchName ) {
var remoteParams = _a . remoteParams ;
return "https://github.com/" + remoteParams . owner + "/" + remoteParams . repo + "/commits/" + branchName ;
}
2020-10-01 19:06:56 -04:00
/ * *
* @ 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
* /
/** Error for failed Git commands. */
var GitCommandError = /** @class */ ( function ( _super ) {
tslib . _ _extends ( GitCommandError , _super ) ;
function GitCommandError ( client , args ) {
2020-10-28 13:47:22 -04:00
var _this =
2020-10-01 19:06:56 -04:00
// 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.
_super . call ( this , "Command failed: git " + client . omitGithubTokenFromMessage ( args . join ( ' ' ) ) ) || this ;
_this . args = args ;
return _this ;
}
return GitCommandError ;
} ( Error ) ) ;
/ * *
* Common client for performing Git interactions with a given remote .
*
* Takes in two optional arguments :
* ` githubToken ` : the token used for authentication in Github interactions , by default empty
* allowing readonly actions .
* ` config ` : The dev - infra configuration containing information about the remote . By default
* the dev - infra configuration is loaded with its Github configuration .
* * /
var GitClient = /** @class */ ( function ( ) {
2021-04-08 15:34:55 -04:00
/ * *
* @ param githubToken The github token used for authentication , if provided .
* @ param _config The configuration , containing the github specific configuration .
2021-04-12 17:50:50 -04:00
* @ param baseDir The full path to the root of the repository base .
2021-04-08 15:34:55 -04:00
* /
2021-04-12 17:50:50 -04:00
function GitClient ( githubToken , config , baseDir ) {
2020-10-01 19:06:56 -04:00
this . githubToken = githubToken ;
/** The OAuth scopes available for the provided Github token. */
this . _cachedOauthScopes = null ;
/ * *
* Regular expression that matches the provided Github token . Used for
* sanitizing the token from Git child process output .
* /
this . _githubTokenRegex = null ;
2021-04-12 17:50:50 -04:00
/** Instance of the Github octokit API. */
2021-04-08 15:34:55 -04:00
this . github = new GithubClient ( this . githubToken ) ;
2021-04-12 17:50:50 -04:00
this . baseDir = baseDir || this . determineBaseDir ( ) ;
this . config = config || getConfig ( this . baseDir ) ;
this . remoteConfig = this . config . github ;
this . remoteParams = { owner : this . remoteConfig . owner , repo : this . remoteConfig . name } ;
2020-10-01 19:06:56 -04:00
// 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.
2021-04-08 15:34:55 -04:00
if ( typeof githubToken === 'string' ) {
2020-10-01 19:06:56 -04:00
this . _githubTokenRegex = new RegExp ( githubToken , 'g' ) ;
}
}
2021-04-08 15:34:55 -04:00
/ * *
* 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 ) ;
} ;
2021-04-30 12:33:40 -04:00
/** Set the verbose logging state of the GitClient class. */
GitClient . setVerboseLoggingState = function ( verbose ) {
2021-04-08 15:34:55 -04:00
this . verboseLogging = verbose ;
} ;
2020-10-01 19:06:56 -04:00
/** Executes the given git command. Throws if the command fails. */
GitClient . prototype . run = function ( args , options ) {
var result = this . runGraceful ( args , options ) ;
if ( result . status !== 0 ) {
throw new GitCommandError ( this , args ) ;
}
// Omit `status` from the type so that it's obvious that the status is never
// non-zero as explained in the method description.
return result ;
} ;
/ * *
* Spawns a given Git command process . Does not throw if the command fails . Additionally ,
* if there is any stderr output , the output will be printed . This makes it easier to
* info failed commands .
* /
GitClient . prototype . runGraceful = function ( args , options ) {
if ( options === void 0 ) { options = { } ; }
2021-04-01 16:49:09 -04:00
/** The git command to be run. */
var gitCommand = args [ 0 ] ;
if ( isDryRun ( ) && gitCommand === 'push' ) {
debug ( "\"git push\" is not able to be run in dryRun mode." ) ;
throw new DryRunError ( ) ;
}
2020-10-01 19:06:56 -04:00
// To improve the debugging experience in case something fails, we print all executed Git
2021-04-30 12:33:40 -04:00
// commands at the DEBUG level to better understand the git actions occuring. Verbose logging,
// always logging at the INFO level, can be enabled either by setting the verboseLogging
// property on the GitClient class or the options object provided to the method.
var printFn = ( GitClient . verboseLogging || options . verboseLogging ) ? info : debug ;
2020-10-01 19:06:56 -04:00
// 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 ( ' ' ) ) ) ;
2021-04-12 17:50:50 -04:00
var result = child _process . spawnSync ( 'git' , args , tslib . _ _assign ( tslib . _ _assign ( { cwd : this . baseDir , stdio : 'pipe' } , options ) , {
2020-10-01 19:06:56 -04:00
// Encoding is always `utf8` and not overridable. This ensures that this method
// always returns `string` as output instead of buffers.
encoding : 'utf8' } ) ) ;
if ( result . stderr !== null ) {
// Git sometimes prints the command if it failed. This means that it could
// potentially leak the Github token used for accessing the remote. To avoid
// printing a token, we sanitize the string before printing the stderr output.
process . stderr . write ( this . omitGithubTokenFromMessage ( result . stderr ) ) ;
}
return result ;
} ;
2021-04-08 15:34:55 -04:00
/** Git URL that resolves to the configured repository. */
GitClient . prototype . getRepoGitUrl = function ( ) {
return getRepositoryGitUrl ( this . remoteConfig , this . githubToken ) ;
} ;
2020-10-01 19:06:56 -04:00
/** Whether the given branch contains the specified SHA. */
GitClient . prototype . hasCommit = function ( branchName , sha ) {
return this . run ( [ 'branch' , branchName , '--contains' , sha ] ) . stdout !== '' ;
} ;
/** Gets the currently checked out branch or revision. */
GitClient . prototype . getCurrentBranchOrRevision = function ( ) {
var branchName = this . run ( [ 'rev-parse' , '--abbrev-ref' , 'HEAD' ] ) . stdout . trim ( ) ;
// If no branch name could be resolved. i.e. `HEAD` has been returned, then Git
// is currently in a detached state. In those cases, we just want to return the
// currently checked out revision/SHA.
if ( branchName === 'HEAD' ) {
return this . run ( [ 'rev-parse' , 'HEAD' ] ) . stdout . trim ( ) ;
}
return branchName ;
} ;
/** Gets whether the current Git repository has uncommitted changes. */
GitClient . prototype . hasUncommittedChanges = function ( ) {
return this . runGraceful ( [ 'diff-index' , '--quiet' , 'HEAD' ] ) . status !== 0 ;
} ;
/** Whether the repo has any local changes. */
GitClient . prototype . hasLocalChanges = function ( ) {
return this . runGraceful ( [ 'diff-index' , '--quiet' , 'HEAD' ] ) . status !== 0 ;
} ;
/** Sanitizes a given message by omitting the provided Github token if present. */
GitClient . prototype . omitGithubTokenFromMessage = function ( value ) {
// If no token has been defined (i.e. no token regex), we just return the
// value as is. There is no secret value that needs to be omitted.
if ( this . _githubTokenRegex === null ) {
return value ;
}
return value . replace ( this . _githubTokenRegex , '<TOKEN>' ) ;
} ;
/ * *
* Checks out a requested branch or revision , optionally cleaning the state of the repository
* before attempting the checking . Returns a boolean indicating whether the branch or revision
* was cleanly checked out .
* /
GitClient . prototype . checkout = function ( branchOrRevision , cleanState ) {
if ( cleanState ) {
// Abort any outstanding ams.
this . runGraceful ( [ 'am' , '--abort' ] , { stdio : 'ignore' } ) ;
// Abort any outstanding cherry-picks.
this . runGraceful ( [ 'cherry-pick' , '--abort' ] , { stdio : 'ignore' } ) ;
// Abort any outstanding rebases.
this . runGraceful ( [ 'rebase' , '--abort' ] , { stdio : 'ignore' } ) ;
// Clear any changes in the current repo.
this . runGraceful ( [ 'reset' , '--hard' ] , { stdio : 'ignore' } ) ;
}
return this . runGraceful ( [ 'checkout' , branchOrRevision ] , { stdio : 'ignore' } ) . status === 0 ;
} ;
2021-04-01 19:17:04 -04:00
/** Gets the latest git tag on the current branch that matches SemVer. */
GitClient . prototype . getLatestSemverTag = function ( ) {
var semVerOptions = { loose : true } ;
var tags = this . runGraceful ( [ 'tag' , '--sort=-committerdate' , '--merged' ] ) . stdout . split ( '\n' ) ;
var latestTag = tags . find ( function ( tag ) { return semver . parse ( tag , semVerOptions ) ; } ) ;
if ( latestTag === undefined ) {
throw new Error ( "Unable to find a SemVer matching tag on \"" + this . getCurrentBranchOrRevision ( ) + "\"" ) ;
}
return new semver . SemVer ( latestTag , semVerOptions ) ;
} ;
2021-04-12 17:50:50 -04:00
/** Retrieve a list of all files in the repostitory changed since the provided shaOrRef. */
GitClient . prototype . allChangesFilesSince = function ( shaOrRef ) {
if ( shaOrRef === void 0 ) { shaOrRef = 'HEAD' ; }
return Array . from ( new Set ( tslib . _ _spreadArray ( tslib . _ _spreadArray ( [ ] , tslib . _ _read ( gitOutputAsArray ( this . runGraceful ( [ 'diff' , '--name-only' , '--diff-filter=d' , shaOrRef ] ) ) ) ) , tslib . _ _read ( gitOutputAsArray ( this . runGraceful ( [ 'ls-files' , '--others' , '--exclude-standard' ] ) ) ) ) ) ) ;
} ;
/** Retrieve a list of all files currently staged in the repostitory. */
GitClient . prototype . allStagedFiles = function ( ) {
return gitOutputAsArray ( this . runGraceful ( [ 'diff' , '--name-only' , '--diff-filter=ACM' , '--staged' ] ) ) ;
} ;
/** Retrieve a list of all files tracked in the repostitory. */
GitClient . prototype . allFiles = function ( ) {
return gitOutputAsArray ( this . runGraceful ( [ 'ls-files' ] ) ) ;
} ;
2020-10-01 19:06:56 -04:00
/ * *
* Assert the GitClient instance is using a token with permissions for the all of the
* provided OAuth scopes .
* /
GitClient . prototype . hasOauthScopes = function ( testFn ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
var scopes , missingScopes , error ;
return tslib . _ _generator ( this , function ( _a ) {
switch ( _a . label ) {
case 0 : return [ 4 /*yield*/ , this . getAuthScopesForToken ( ) ] ;
case 1 :
scopes = _a . sent ( ) ;
missingScopes = [ ] ;
// Test Github OAuth scopes and collect missing ones.
testFn ( scopes , missingScopes ) ;
// If no missing scopes are found, return true to indicate all OAuth Scopes are available.
if ( missingScopes . length === 0 ) {
return [ 2 /*return*/ , true ] ;
}
error = "The provided <TOKEN> does not have required permissions due to missing scope(s): " +
( yellow ( missingScopes . join ( ', ' ) ) + "\n\n" ) +
"Update the token in use at:\n" +
( " " + GITHUB _TOKEN _SETTINGS _URL + "\n\n" ) +
( "Alternatively, a new token can be created at: " + GITHUB _TOKEN _GENERATE _URL + "\n" ) ;
return [ 2 /*return*/ , { error : error } ] ;
}
} ) ;
} ) ;
} ;
/ * *
* Retrieve the OAuth scopes for the loaded Github token .
* * /
GitClient . prototype . getAuthScopesForToken = function ( ) {
// If the OAuth scopes have already been loaded, return the Promise containing them.
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 . _cachedOauthScopes = this . github . rateLimit . get ( ) . then ( function ( _response ) {
var response = _response ;
var scopes = response . headers [ 'x-oauth-scopes' ] || '' ;
return scopes . split ( ',' ) . map ( function ( scope ) { return scope . trim ( ) ; } ) ;
} ) ;
} ;
2021-04-12 17:50:50 -04:00
GitClient . prototype . determineBaseDir = function ( ) {
var _a = this . runGraceful ( [ 'rev-parse' , '--show-toplevel' ] ) , stdout = _a . stdout , stderr = _a . stderr , status = _a . status ;
if ( status !== 0 ) {
throw Error ( "Unable to find the path to the base directory of the repository.\n" +
"Was the command run from inside of the repo?\n\n" +
( "ERROR:\n " + stderr ) ) ;
}
return stdout . trim ( ) ;
} ;
2021-04-30 12:33:40 -04:00
/** Whether verbose logging of Git actions should be used. */
GitClient . verboseLogging = false ;
2020-10-01 19:06:56 -04:00
return GitClient ;
} ( ) ) ;
2021-04-12 17:50:50 -04:00
/ * *
* Takes the output from ` GitClient.run ` and ` GitClient.runGraceful ` and returns an array of strings
* for each new line . Git commands typically return multiple output values for a command a set of
* strings separated by new lines .
*
2021-04-18 05:43:49 -04:00
* Note : This is specifically created as a locally available function for usage as convenience
* utility within ` GitClient ` ' s methods to create outputs as array .
2021-04-12 17:50:50 -04:00
* /
function gitOutputAsArray ( gitCommandResult ) {
return gitCommandResult . stdout . split ( '\n' ) . map ( function ( x ) { return x . trim ( ) ; } ) . filter ( function ( x ) { return ! ! x ; } ) ;
}
/ * *
* @ 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
* /
/** Reexport of chalk colors for convenient access. */
var red = chalk . red ;
var green = chalk . green ;
var yellow = chalk . yellow ;
var bold = chalk . bold ;
var blue = chalk . blue ;
/** Prompts the user with a confirmation question and a specified message. */
function promptConfirm ( message , defaultValue ) {
if ( defaultValue === void 0 ) { defaultValue = false ; }
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
return tslib . _ _generator ( this , function ( _a ) {
switch ( _a . label ) {
case 0 : return [ 4 /*yield*/ , inquirer . prompt ( {
type : 'confirm' ,
name : 'result' ,
message : message ,
default : defaultValue ,
} ) ] ;
case 1 : return [ 2 /*return*/ , ( _a . sent ( ) )
. result ] ;
}
} ) ;
} ) ;
}
2021-04-19 14:58:32 -04:00
/** Prompts the user for one line of input. */
function promptInput ( message ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
return tslib . _ _generator ( this , function ( _a ) {
switch ( _a . label ) {
case 0 : return [ 4 /*yield*/ , inquirer . prompt ( { type : 'input' , name : 'result' , message : message } ) ] ;
case 1 : return [ 2 /*return*/ , ( _a . sent ( ) ) . result ] ;
}
} ) ;
} ) ;
}
2021-04-12 17:50:50 -04:00
/ * *
* Supported levels for logging functions .
*
* Levels are mapped to numbers to represent a hierarchy of logging levels .
* /
var LOG _LEVELS ;
( function ( LOG _LEVELS ) {
LOG _LEVELS [ LOG _LEVELS [ "SILENT" ] = 0 ] = "SILENT" ;
LOG _LEVELS [ LOG _LEVELS [ "ERROR" ] = 1 ] = "ERROR" ;
LOG _LEVELS [ LOG _LEVELS [ "WARN" ] = 2 ] = "WARN" ;
LOG _LEVELS [ LOG _LEVELS [ "LOG" ] = 3 ] = "LOG" ;
LOG _LEVELS [ LOG _LEVELS [ "INFO" ] = 4 ] = "INFO" ;
LOG _LEVELS [ LOG _LEVELS [ "DEBUG" ] = 5 ] = "DEBUG" ;
} ) ( LOG _LEVELS || ( LOG _LEVELS = { } ) ) ;
/** Default log level for the tool. */
var DEFAULT _LOG _LEVEL = LOG _LEVELS . INFO ;
/** Write to the console for at INFO logging level */
var info = buildLogLevelFunction ( function ( ) { return console . info ; } , LOG _LEVELS . INFO ) ;
/** Write to the console for at ERROR logging level */
var error = buildLogLevelFunction ( function ( ) { return console . error ; } , LOG _LEVELS . ERROR ) ;
/** Write to the console for at DEBUG logging level */
var debug = buildLogLevelFunction ( function ( ) { return console . debug ; } , LOG _LEVELS . DEBUG ) ;
/** Write to the console for at LOG logging level */
// tslint:disable-next-line: no-console
var log = buildLogLevelFunction ( function ( ) { return console . log ; } , LOG _LEVELS . LOG ) ;
/** Write to the console for at WARN logging level */
var warn = buildLogLevelFunction ( function ( ) { return console . warn ; } , LOG _LEVELS . WARN ) ;
/** Build an instance of a logging function for the provided level. */
function buildLogLevelFunction ( loadCommand , level ) {
/** Write to stdout for the LOG_LEVEL. */
var loggingFunction = function ( ) {
var text = [ ] ;
for ( var _i = 0 ; _i < arguments . length ; _i ++ ) {
text [ _i ] = arguments [ _i ] ;
}
runConsoleCommand . apply ( void 0 , tslib . _ _spreadArray ( [ loadCommand , level ] , tslib . _ _read ( text ) ) ) ;
} ;
/** Start a group at the LOG_LEVEL, optionally starting it as collapsed. */
loggingFunction . group = function ( text , collapsed ) {
if ( collapsed === void 0 ) { collapsed = false ; }
var command = collapsed ? console . groupCollapsed : console . group ;
runConsoleCommand ( function ( ) { return command ; } , level , text ) ;
} ;
/** End the group at the LOG_LEVEL. */
loggingFunction . groupEnd = function ( ) {
runConsoleCommand ( function ( ) { return console . groupEnd ; } , level ) ;
} ;
return loggingFunction ;
}
/ * *
* Run the console command provided , if the environments logging level greater than the
* provided logging level .
*
* The loadCommand takes in a function which is called to retrieve the console . * function
* to allow for jasmine spies to still work in testing . Without this method of retrieval
* the console . * function , the function is saved into the closure of the created logging
* function before jasmine can spy .
* /
function runConsoleCommand ( loadCommand , logLevel ) {
var text = [ ] ;
for ( var _i = 2 ; _i < arguments . length ; _i ++ ) {
text [ _i - 2 ] = arguments [ _i ] ;
}
if ( getLogLevel ( ) >= logLevel ) {
loadCommand ( ) . apply ( void 0 , tslib . _ _spreadArray ( [ ] , tslib . _ _read ( text ) ) ) ;
}
printToLogFile . apply ( void 0 , tslib . _ _spreadArray ( [ logLevel ] , tslib . _ _read ( text ) ) ) ;
}
/ * *
* Retrieve the log level from environment variables , if the value found
* based on the LOG _LEVEL environment variable is undefined , return the default
* logging level .
* /
function getLogLevel ( ) {
var logLevelEnvValue = ( process . env [ "LOG_LEVEL" ] || '' ) . toUpperCase ( ) ;
var logLevel = LOG _LEVELS [ logLevelEnvValue ] ;
if ( logLevel === undefined ) {
return DEFAULT _LOG _LEVEL ;
}
return logLevel ;
}
/** All text to write to the log file. */
var LOGGED _TEXT = '' ;
/** Whether file logging as been enabled. */
var FILE _LOGGING _ENABLED = false ;
/ * *
* The number of columns used in the prepended log level information on each line of the logging
* output file .
* /
var LOG _LEVEL _COLUMNS = 7 ;
/ * *
* Enable writing the logged outputs to the log file on process exit , sets initial lines from the
* command execution , containing information about the timing and command parameters .
*
* This is expected to be called only once during a command run , and should be called by the
* middleware of yargs to enable the file logging before the rest of the command parsing and
* response is executed .
* /
function captureLogOutputForCommand ( argv ) {
if ( FILE _LOGGING _ENABLED ) {
throw Error ( '`captureLogOutputForCommand` cannot be called multiple times' ) ;
}
var git = GitClient . getInstance ( ) ;
/** The date time used for timestamping when the command was invoked. */
var now = new Date ( ) ;
/** Header line to separate command runs in log files. */
var headerLine = Array ( 100 ) . fill ( '#' ) . join ( '' ) ;
LOGGED _TEXT += headerLine + "\nCommand: " + argv . $0 + " " + argv . _ . join ( ' ' ) + "\nRan at: " + now + "\n" ;
// On process exit, write the logged output to the appropriate log files
process . on ( 'exit' , function ( code ) {
LOGGED _TEXT += headerLine + "\n" ;
LOGGED _TEXT += "Command ran in " + ( new Date ( ) . getTime ( ) - now . getTime ( ) ) + "ms\n" ;
LOGGED _TEXT += "Exit Code: " + code + "\n" ;
/** Path to the log file location. */
var logFilePath = path . join ( git . baseDir , '.ng-dev.log' ) ;
// Strip ANSI escape codes from log outputs.
LOGGED _TEXT = LOGGED _TEXT . replace ( /\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[mGK]/g , '' ) ;
fs . writeFileSync ( logFilePath , LOGGED _TEXT ) ;
// For failure codes greater than 1, the new logged lines should be written to a specific log
// file for the command run failure.
if ( code > 1 ) {
var logFileName = ".ng-dev.err-" + now . getTime ( ) + ".log" ;
console . error ( "Exit code: " + code + ". Writing full log to " + logFileName ) ;
fs . writeFileSync ( path . join ( git . baseDir , logFileName ) , LOGGED _TEXT ) ;
}
} ) ;
// Mark file logging as enabled to prevent the function from executing multiple times.
FILE _LOGGING _ENABLED = true ;
}
/** Write the provided text to the log file, prepending each line with the log level. */
function printToLogFile ( logLevel ) {
var text = [ ] ;
for ( var _i = 1 ; _i < arguments . length ; _i ++ ) {
text [ _i - 1 ] = arguments [ _i ] ;
}
var logLevelText = ( LOG _LEVELS [ logLevel ] + ":" ) . padEnd ( LOG _LEVEL _COLUMNS ) ;
LOGGED _TEXT += text . join ( ' ' ) . split ( '\n' ) . map ( function ( l ) { return logLevelText + " " + l + "\n" ; } ) . join ( '' ) ;
}
2020-10-01 19:06:56 -04:00
2021-04-08 15:34:55 -04:00
/ * *
* @ 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 ) ;
}
2021-04-13 14:22:07 -04:00
try {
GitClient . getAuthenticatedInstance ( ) ;
}
catch ( _a ) {
GitClient . authenticateWithToken ( githubToken ) ;
}
2021-04-08 15:34:55 -04:00
return githubToken ;
} ,
} )
. default ( 'github-token' , '' , '<LOCAL TOKEN>' ) ;
}
2020-10-01 19:06:56 -04:00
/ * *
* @ 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
* /
/** Retrieve and validate the config as `CaretakerConfig`. */
function getCaretakerConfig ( ) {
// List of errors encountered validating the config.
const errors = [ ] ;
// The non-validated config object.
const config = getConfig ( ) ;
assertNoErrors ( errors ) ;
return config ;
}
/ * *
* @ 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
* /
/** Class describing a release-train. */
class ReleaseTrain {
constructor (
/** Name of the branch for this release-train. */
2020-10-28 13:47:22 -04:00
branchName ,
2020-10-01 19:06:56 -04:00
/** Most recent version for this release train. */
version ) {
this . branchName = branchName ;
this . version = version ;
/** Whether the release train is currently targeting a major. */
this . isMajor = this . version . minor === 0 && this . version . patch === 0 ;
}
}
/ * *
* @ 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
* /
/** Regular expression that matches version-branches. */
const versionBranchNameRegex = /^(\d+)\.(\d+)\.x$/ ;
/** Gets the version of a given branch by reading the `package.json` upstream. */
function getVersionOfBranch ( repo , branchName ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const { data } = yield repo . api . repos . getContents ( { owner : repo . owner , repo : repo . name , path : '/package.json' , ref : branchName } ) ;
const { version } = JSON . parse ( Buffer . from ( data . content , 'base64' ) . toString ( ) ) ;
const parsedVersion = semver . parse ( version ) ;
if ( parsedVersion === null ) {
throw Error ( ` Invalid version detected in following branch: ${ branchName } . ` ) ;
}
return parsedVersion ;
} ) ;
}
/** Whether the given branch corresponds to a version branch. */
function isVersionBranch ( branchName ) {
return versionBranchNameRegex . test ( branchName ) ;
}
/ * *
* Converts a given version - branch into a SemVer version that can be used with SemVer
* utilities . e . g . to determine semantic order , extract major digit , compare .
*
* For example ` 10.0.x ` will become ` 10.0.0 ` in SemVer . The patch digit is not
* relevant but needed for parsing . SemVer does not allow ` x ` as patch digit .
* /
function getVersionForVersionBranch ( branchName ) {
return semver . parse ( branchName . replace ( versionBranchNameRegex , '$1.$2.0' ) ) ;
}
/ * *
* Gets the version branches for the specified major versions in descending
* order . i . e . latest version branches first .
* /
function getBranchesForMajorVersions ( repo , majorVersions ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const { data : branchData } = yield repo . api . repos . listBranches ( { owner : repo . owner , repo : repo . name , protected : true } ) ;
const branches = [ ] ;
for ( const { name } of branchData ) {
if ( ! isVersionBranch ( name ) ) {
continue ;
}
// Convert the version-branch into a SemVer version that can be used with the
// SemVer utilities. e.g. to determine semantic order, compare versions.
const parsed = getVersionForVersionBranch ( name ) ;
// Collect all version-branches that match the specified major versions.
if ( parsed !== null && majorVersions . includes ( parsed . major ) ) {
branches . push ( { name , parsed } ) ;
}
}
// Sort captured version-branches in descending order.
return branches . sort ( ( a , b ) => semver . rcompare ( a . parsed , b . parsed ) ) ;
} ) ;
}
/ * *
* @ 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
* /
/** Branch name for the `next` branch. */
const nextBranchName = 'master' ;
/** Fetches the active release trains for the configured project. */
function fetchActiveReleaseTrains ( repo ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const nextVersion = yield getVersionOfBranch ( repo , nextBranchName ) ;
const next = new ReleaseTrain ( nextBranchName , nextVersion ) ;
const majorVersionsToConsider = [ ] ;
let expectedReleaseCandidateMajor ;
// If the `next` branch (i.e. `master` branch) is for an upcoming major version, we know
// that there is no patch branch or feature-freeze/release-candidate branch for this major
// digit. If the current `next` version is the first minor of a major version, we know that
// the feature-freeze/release-candidate branch can only be the actual major branch. The
// patch branch is based on that, either the actual major branch or the last minor from the
// preceding major version. In all other cases, the patch branch and feature-freeze or
// release-candidate branch are part of the same major version. Consider the following:
//
// CASE 1. next: 11.0.0-next.0: patch and feature-freeze/release-candidate can only be
// most recent `10.<>.x` branches. The FF/RC branch can only be the last-minor of v10.
// CASE 2. next: 11.1.0-next.0: patch can be either `11.0.x` or last-minor in v10 based
// on whether there is a feature-freeze/release-candidate branch (=> `11.0.x`).
// CASE 3. next: 10.6.0-next.0: patch can be either `10.5.x` or `10.4.x` based on whether
// there is a feature-freeze/release-candidate branch (=> `10.5.x`)
if ( nextVersion . minor === 0 ) {
expectedReleaseCandidateMajor = nextVersion . major - 1 ;
majorVersionsToConsider . push ( nextVersion . major - 1 ) ;
}
else if ( nextVersion . minor === 1 ) {
expectedReleaseCandidateMajor = nextVersion . major ;
majorVersionsToConsider . push ( nextVersion . major , nextVersion . major - 1 ) ;
}
else {
expectedReleaseCandidateMajor = nextVersion . major ;
majorVersionsToConsider . push ( nextVersion . major ) ;
}
// Collect all version-branches that should be considered for the latest version-branch,
// or the feature-freeze/release-candidate.
const branches = ( yield getBranchesForMajorVersions ( repo , majorVersionsToConsider ) ) ;
const { latest , releaseCandidate } = yield findActiveReleaseTrainsFromVersionBranches ( repo , nextVersion , branches , expectedReleaseCandidateMajor ) ;
if ( latest === null ) {
throw Error ( ` Unable to determine the latest release-train. The following branches ` +
` have been considered: [ ${ branches . map ( b => b . name ) . join ( ', ' ) } ] ` ) ;
}
return { releaseCandidate , latest , next } ;
} ) ;
}
/** Finds the currently active release trains from the specified version branches. */
function findActiveReleaseTrainsFromVersionBranches ( repo , nextVersion , branches , expectedReleaseCandidateMajor ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
// Version representing the release-train currently in the next phase. Note that we ignore
// patch and pre-release segments in order to be able to compare the next release train to
// other release trains from version branches (which follow the `N.N.x` pattern).
const nextReleaseTrainVersion = semver . parse ( ` ${ nextVersion . major } . ${ nextVersion . minor } .0 ` ) ;
let latest = null ;
let releaseCandidate = null ;
// Iterate through the captured branches and find the latest non-prerelease branch and a
// potential release candidate branch. From the collected branches we iterate descending
// order (most recent semantic version-branch first). The first branch is either the latest
// active version branch (i.e. patch) or a feature-freeze/release-candidate branch. A FF/RC
// branch cannot be older than the latest active version-branch, so we stop iterating once
// we found such a branch. Otherwise, if we found a FF/RC branch, we continue looking for the
// next version-branch as that one is supposed to be the latest active version-branch. If it
// is not, then an error will be thrown due to two FF/RC branches existing at the same time.
for ( const { name , parsed } of branches ) {
// It can happen that version branches have been accidentally created which are more recent
// than the release-train in the next branch (i.e. `master`). We could ignore such branches
// silently, but it might be symptomatic for an outdated version in the `next` branch, or an
// accidentally created branch by the caretaker. In either way we want to raise awareness.
if ( semver . gt ( parsed , nextReleaseTrainVersion ) ) {
throw Error ( ` Discovered unexpected version-branch " ${ name } " for a release-train that is ` +
` more recent than the release-train currently in the " ${ nextBranchName } " branch. ` +
` Please either delete the branch if created by accident, or update the outdated ` +
` version in the next branch ( ${ nextBranchName } ). ` ) ;
}
else if ( semver . eq ( parsed , nextReleaseTrainVersion ) ) {
throw Error ( ` Discovered unexpected version-branch " ${ name } " for a release-train that is already ` +
` active in the " ${ nextBranchName } " branch. Please either delete the branch if ` +
` created by accident, or update the version in the next branch ( ${ nextBranchName } ). ` ) ;
}
const version = yield getVersionOfBranch ( repo , name ) ;
const releaseTrain = new ReleaseTrain ( name , version ) ;
const isPrerelease = version . prerelease [ 0 ] === 'rc' || version . prerelease [ 0 ] === 'next' ;
if ( isPrerelease ) {
if ( releaseCandidate !== null ) {
throw Error ( ` Unable to determine latest release-train. Found two consecutive ` +
` branches in feature-freeze/release-candidate phase. Did not expect both " ${ name } " ` +
` and " ${ releaseCandidate . branchName } " to be in feature-freeze/release-candidate mode. ` ) ;
}
else if ( version . major !== expectedReleaseCandidateMajor ) {
throw Error ( ` Discovered unexpected old feature-freeze/release-candidate branch. Expected no ` +
` version-branch in feature-freeze/release-candidate mode for v ${ version . major } . ` ) ;
}
releaseCandidate = releaseTrain ;
}
else {
latest = releaseTrain ;
break ;
}
}
return { releaseCandidate , latest } ;
} ) ;
}
/ * *
* @ 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
* /
/ * *
* Cache for requested NPM package information . A cache is desirable as the NPM
* registry requests are usually very large and slow .
* /
const _npmPackageInfoCache = { } ;
/ * *
* Fetches the NPM package representing the project . Angular repositories usually contain
* multiple packages in a monorepo scheme , but packages dealt with as part of the release
* tooling are released together with the same versioning and branching . This means that
* a single package can be used as source of truth for NPM package queries .
* /
function fetchProjectNpmPackageInfo ( config ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const pkgName = getRepresentativeNpmPackage ( config ) ;
return yield fetchPackageInfoFromNpmRegistry ( pkgName ) ;
} ) ;
}
/** Gets whether the given version is published to NPM or not */
function isVersionPublishedToNpm ( version , config ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const { versions } = yield fetchProjectNpmPackageInfo ( config ) ;
return versions [ version . format ( ) ] !== undefined ;
} ) ;
}
/ * *
* Gets the representative NPM package for the specified release configuration . Angular
* repositories usually contain multiple packages in a monorepo scheme , but packages dealt with
* as part of the release tooling are released together with the same versioning and branching .
* This means that a single package can be used as source of truth for NPM package queries .
* /
function getRepresentativeNpmPackage ( config ) {
return config . npmPackages [ 0 ] ;
}
/** Fetches the specified NPM package from the NPM registry. */
function fetchPackageInfoFromNpmRegistry ( pkgName ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
if ( _npmPackageInfoCache [ pkgName ] === undefined ) {
_npmPackageInfoCache [ pkgName ] =
fetch ( ` https://registry.npmjs.org/ ${ pkgName } ` ) . then ( r => r . json ( ) ) ;
}
return yield _npmPackageInfoCache [ pkgName ] ;
} ) ;
}
/ * *
* @ 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
* /
/ * *
* Number of months a major version in Angular is actively supported . See :
* https : //angular.io/guide/releases#support-policy-and-schedule.
* /
const majorActiveSupportDuration = 6 ;
/ * *
* Number of months a major version has active long - term support . See :
* https : //angular.io/guide/releases#support-policy-and-schedule.
* /
const majorLongTermSupportDuration = 12 ;
/** Regular expression that matches LTS NPM dist tags. */
const ltsNpmDistTagRegex = /^v(\d+)-lts$/ ;
/** Finds all long-term support release trains from the specified NPM package. */
function fetchLongTermSupportBranchesFromNpm ( config ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const { 'dist-tags' : distTags , time } = yield fetchProjectNpmPackageInfo ( config ) ;
const today = new Date ( ) ;
const active = [ ] ;
const inactive = [ ] ;
// Iterate through the NPM package information and determine active/inactive LTS versions with
// their corresponding branches. We assume that an LTS tagged version in NPM belongs to the
// last-minor branch of a given major (i.e. we assume there are no outdated LTS NPM dist tags).
for ( const npmDistTag in distTags ) {
if ( ltsNpmDistTagRegex . test ( npmDistTag ) ) {
const version = semver . parse ( distTags [ npmDistTag ] ) ;
const branchName = ` ${ version . major } . ${ version . minor } .x ` ;
const majorReleaseDate = new Date ( time [ ` ${ version . major } .0.0 ` ] ) ;
const ltsEndDate = computeLtsEndDateOfMajor ( majorReleaseDate ) ;
const ltsBranch = { name : branchName , version , npmDistTag } ;
// Depending on whether the LTS phase is still active, add the branch
// to the list of active or inactive LTS branches.
if ( today <= ltsEndDate ) {
active . push ( ltsBranch ) ;
}
else {
inactive . push ( ltsBranch ) ;
}
}
}
// Sort LTS branches in descending order. i.e. most recent ones first.
active . sort ( ( a , b ) => semver . rcompare ( a . version , b . version ) ) ;
inactive . sort ( ( a , b ) => semver . rcompare ( a . version , b . version ) ) ;
return { active , inactive } ;
} ) ;
}
/ * *
* Computes the date when long - term support ends for a major released at the
* specified date .
* /
function computeLtsEndDateOfMajor ( majorReleaseDate ) {
return new Date ( majorReleaseDate . getFullYear ( ) , majorReleaseDate . getMonth ( ) + majorActiveSupportDuration + majorLongTermSupportDuration , majorReleaseDate . getDate ( ) , majorReleaseDate . getHours ( ) , majorReleaseDate . getMinutes ( ) , majorReleaseDate . getSeconds ( ) , majorReleaseDate . getMilliseconds ( ) ) ;
}
/** Gets the long-term support NPM dist tag for a given major version. */
function getLtsNpmDistTagOfMajor ( major ) {
// LTS versions should be tagged in NPM in the following format: `v{major}-lts`.
return ` v ${ major } -lts ` ;
}
2020-09-14 14:24:15 -04:00
/** The BaseModule to extend modules for caretaker checks from. */
class BaseModule {
2021-04-08 15:34:55 -04:00
constructor ( config ) {
2020-09-14 14:24:15 -04:00
this . config = config ;
2021-04-08 15:34:55 -04:00
/** The singleton instance of the GitClient. */
this . git = GitClient . getAuthenticatedInstance ( ) ;
2020-09-14 14:24:15 -04:00
/** The data for the module. */
this . data = this . retrieveData ( ) ;
}
}
2020-10-01 19:06:56 -04:00
/ * *
* @ 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
* /
2020-09-14 14:24:15 -04:00
class CiModule extends BaseModule {
retrieveData ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const gitRepoWithApi = Object . assign ( { api : this . git . github } , this . git . remoteConfig ) ;
const releaseTrains = yield fetchActiveReleaseTrains ( gitRepoWithApi ) ;
const ciResultPromises = Object . entries ( releaseTrains ) . map ( ( [ trainName , train ] ) => tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
if ( train === null ) {
return {
active : false ,
name : trainName ,
label : '' ,
status : 'not found' ,
} ;
}
return {
active : true ,
name : train . branchName ,
label : ` ${ trainName } ( ${ train . branchName } ) ` ,
status : yield this . getBranchStatusFromCi ( train . branchName ) ,
} ;
} ) ) ;
return yield Promise . all ( ciResultPromises ) ;
} ) ;
}
printToTerminal ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const data = yield this . data ;
const minLabelLength = Math . max ( ... data . map ( result => result . label . length ) ) ;
info . group ( bold ( ` CI ` ) ) ;
data . forEach ( result => {
if ( result . active === false ) {
debug ( ` No active release train for ${ result . name } ` ) ;
return ;
}
const label = result . label . padEnd ( minLabelLength ) ;
if ( result . status === 'not found' ) {
info ( ` ${ result . name } was not found on CircleCI ` ) ;
}
else if ( result . status === 'success' ) {
info ( ` ${ label } ✅ ` ) ;
}
else {
info ( ` ${ label } ❌ ` ) ;
}
} ) ;
info . groupEnd ( ) ;
info ( ) ;
} ) ;
}
/** Get the CI status of a given branch from CircleCI. */
getBranchStatusFromCi ( branch ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const { owner , name } = this . git . remoteConfig ;
const url = ` https://circleci.com/gh/ ${ owner } / ${ name } /tree/ ${ branch } .svg?style=shield ` ;
const result = yield fetch ( url ) . then ( result => result . text ( ) ) ;
if ( result && ! result . includes ( 'no builds' ) ) {
return result . includes ( 'passing' ) ? 'success' : 'failed' ;
2020-10-01 19:06:56 -04:00
}
2020-09-14 14:24:15 -04:00
return 'not found' ;
} ) ;
}
2020-10-01 19:06:56 -04:00
}
/ * *
* @ 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
* /
2020-09-14 14:24:15 -04:00
class G3Module extends BaseModule {
retrieveData ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const toCopyToG3 = this . getG3FileIncludeAndExcludeLists ( ) ;
const latestSha = this . getLatestShas ( ) ;
if ( toCopyToG3 === null || latestSha === null ) {
return ;
}
return this . getDiffStats ( latestSha . g3 , latestSha . master , toCopyToG3 . include , toCopyToG3 . exclude ) ;
} ) ;
}
printToTerminal ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const stats = yield this . data ;
if ( ! stats ) {
return ;
}
info . group ( bold ( 'g3 branch check' ) ) ;
if ( stats . files === 0 ) {
info ( ` ${ stats . commits } commits between g3 and master ` ) ;
info ( '✅ No sync is needed at this time' ) ;
}
else {
info ( ` ${ stats . files } files changed, ${ stats . insertions } insertions(+), ${ stats . deletions } ` +
` deletions(-) from ${ stats . commits } commits will be included in the next sync ` ) ;
}
info . groupEnd ( ) ;
info ( ) ;
} ) ;
}
/** Fetch and retrieve the latest sha for a specific branch. */
getShaForBranchLatest ( branch ) {
const { owner , name } = this . git . remoteConfig ;
/** The result fo the fetch command. */
const fetchResult = this . git . runGraceful ( [ 'fetch' , '-q' , ` https://github.com/ ${ owner } / ${ name } .git ` , branch ] ) ;
if ( fetchResult . status !== 0 &&
fetchResult . stderr . includes ( ` couldn't find remote ref ${ branch } ` ) ) {
debug ( ` No ' ${ branch } ' branch exists on upstream, skipping. ` ) ;
return null ;
}
return this . git . runGraceful ( [ 'rev-parse' , 'FETCH_HEAD' ] ) . stdout . trim ( ) ;
}
/ * *
* Get git diff stats between master and g3 , for all files and filtered to only g3 affecting
* files .
* /
getDiffStats ( g3Ref , masterRef , includeFiles , excludeFiles ) {
/** The diff stats to be returned. */
const stats = {
insertions : 0 ,
deletions : 0 ,
files : 0 ,
commits : 0 ,
} ;
// Determine the number of commits between master and g3 refs. */
stats . commits =
parseInt ( this . git . run ( [ 'rev-list' , '--count' , ` ${ g3Ref } .. ${ masterRef } ` ] ) . stdout , 10 ) ;
// Get the numstat information between master and g3
this . git . run ( [ 'diff' , ` ${ g3Ref } ... ${ masterRef } ` , '--numstat' ] )
. stdout
// Remove the extra space after git's output.
. trim ( )
// Split each line of git output into array
. split ( '\n' )
// Split each line from the git output into components parts: insertions,
// deletions and file name respectively
. map ( line => line . trim ( ) . split ( '\t' ) )
// Parse number value from the insertions and deletions values
// Example raw line input:
// 10\t5\tsrc/file/name.ts
. map ( line => [ Number ( line [ 0 ] ) , Number ( line [ 1 ] ) , line [ 2 ] ] )
// Add each line's value to the diff stats, and conditionally to the g3
// stats as well if the file name is included in the files synced to g3.
. forEach ( ( [ insertions , deletions , fileName ] ) => {
if ( this . checkMatchAgainstIncludeAndExclude ( fileName , includeFiles , excludeFiles ) ) {
stats . insertions += insertions ;
stats . deletions += deletions ;
stats . files += 1 ;
}
} ) ;
return stats ;
}
/** Determine whether the file name passes both include and exclude checks. */
checkMatchAgainstIncludeAndExclude ( file , includes , excludes ) {
return ( multimatch . call ( undefined , file , includes ) . length >= 1 &&
multimatch . call ( undefined , file , excludes ) . length === 0 ) ;
}
getG3FileIncludeAndExcludeLists ( ) {
var _a , _b , _c , _d ;
2021-04-12 17:50:50 -04:00
const angularRobotFilePath = path . join ( this . git . baseDir , '.github/angular-robot.yml' ) ;
2020-10-01 19:06:56 -04:00
if ( ! fs . existsSync ( angularRobotFilePath ) ) {
2020-09-14 14:24:15 -04:00
debug ( 'No angular robot configuration file exists, skipping.' ) ;
return null ;
2020-10-01 19:06:56 -04:00
}
/** The configuration defined for the angular robot. */
const robotConfig = yaml . parse ( fs . readFileSync ( angularRobotFilePath ) . toString ( ) ) ;
/** The files to be included in the g3 sync. */
2020-09-14 14:24:15 -04:00
const include = ( ( _b = ( _a = robotConfig === null || robotConfig === void 0 ? void 0 : robotConfig . merge ) === null || _a === void 0 ? void 0 : _a . g3Status ) === null || _b === void 0 ? void 0 : _b . include ) || [ ] ;
2020-10-01 19:06:56 -04:00
/** The files to be expected in the g3 sync. */
2020-09-14 14:24:15 -04:00
const exclude = ( ( _d = ( _c = robotConfig === null || robotConfig === void 0 ? void 0 : robotConfig . merge ) === null || _c === void 0 ? void 0 : _c . g3Status ) === null || _d === void 0 ? void 0 : _d . exclude ) || [ ] ;
if ( include . length === 0 && exclude . length === 0 ) {
debug ( 'No g3Status include or exclude lists are defined in the angular robot configuration' ) ;
return null ;
2020-10-01 19:06:56 -04:00
}
2020-09-14 14:24:15 -04:00
return { include , exclude } ;
}
getLatestShas ( ) {
2020-10-01 19:06:56 -04:00
/** The latest sha for the g3 branch. */
2020-09-14 14:24:15 -04:00
const g3 = this . getShaForBranchLatest ( 'g3' ) ;
2020-10-01 19:06:56 -04:00
/** The latest sha for the master branch. */
2020-09-14 14:24:15 -04:00
const master = this . getShaForBranchLatest ( 'master' ) ;
if ( g3 === null || master === null ) {
debug ( 'Either the g3 or master was unable to be retrieved' ) ;
return null ;
2020-10-01 19:06:56 -04:00
}
2020-09-14 14:24:15 -04:00
return { g3 , master } ;
}
2020-10-01 19:06:56 -04:00
}
/ * *
* @ 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
* /
2020-09-14 14:24:15 -04:00
/** The fragment for a result from Github's api for a Github query. */
const GithubQueryResultFragment = {
issueCount : typedGraphqlify . types . number ,
nodes : [ Object . assign ( { } , typedGraphqlify . onUnion ( {
PullRequest : {
url : typedGraphqlify . types . string ,
} ,
Issue : {
url : typedGraphqlify . types . string ,
} ,
} ) ) ] ,
} ;
2020-10-01 19:06:56 -04:00
/ * *
2020-09-14 14:24:15 -04:00
* Cap the returned issues in the queries to an arbitrary 20. At that point , caretaker has a lot
2020-10-01 19:06:56 -04:00
* of work to do and showing more than that isn ' t really useful .
* /
const MAX _RETURNED _ISSUES = 20 ;
2020-09-14 14:24:15 -04:00
class GithubQueriesModule extends BaseModule {
retrieveData ( ) {
var _a ;
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
// Non-null assertion is used here as the check for undefined immediately follows to confirm the
// assertion. Typescript's type filtering does not seem to work as needed to understand
// whether githubQueries is undefined or not.
let queries = ( _a = this . config . caretaker ) === null || _a === void 0 ? void 0 : _a . githubQueries ;
if ( queries === undefined || queries . length === 0 ) {
debug ( 'No github queries defined in the configuration, skipping' ) ;
return ;
}
/** The results of the generated github query. */
2021-04-08 15:34:55 -04:00
const queryResult = yield this . git . github . graphql ( this . buildGraphqlQuery ( queries ) ) ;
2020-09-14 14:24:15 -04:00
const results = Object . values ( queryResult ) ;
const { owner , name : repo } = this . git . remoteConfig ;
return results . map ( ( result , i ) => {
return {
queryName : queries [ i ] . name ,
count : result . issueCount ,
queryUrl : encodeURI ( ` https://github.com/ ${ owner } / ${ repo } /issues?q= ${ queries [ i ] . query } ` ) ,
matchedUrls : result . nodes . map ( node => node . url )
} ;
} ) ;
} ) ;
}
/** Build a Graphql query statement for the provided queries. */
buildGraphqlQuery ( queries ) {
2020-10-01 19:06:56 -04:00
/** The query object for graphql. */
2021-04-08 15:34:55 -04:00
const graphqlQuery = { } ;
2020-09-14 14:24:15 -04:00
const { owner , name : repo } = this . git . remoteConfig ;
2020-10-01 19:06:56 -04:00
/** The Github search filter for the configured repository. */
2020-09-14 14:24:15 -04:00
const repoFilter = ` repo: ${ owner } / ${ repo } ` ;
2020-10-01 19:06:56 -04:00
queries . forEach ( ( { name , query } ) => {
2021-04-08 15:34:55 -04:00
/** The name of the query, with spaces removed to match Graphql requirements. */
2020-10-01 19:06:56 -04:00
const queryKey = typedGraphqlify . alias ( name . replace ( / /g , '' ) , 'search' ) ;
2021-04-08 15:34:55 -04:00
graphqlQuery [ queryKey ] = typedGraphqlify . params ( {
2020-10-01 19:06:56 -04:00
type : 'ISSUE' ,
first : MAX _RETURNED _ISSUES ,
query : ` " ${ repoFilter } ${ query . replace ( /"/g , '\\"' ) } " ` ,
2020-09-14 14:24:15 -04:00
} , Object . assign ( { } , GithubQueryResultFragment ) ) ;
2020-10-01 19:06:56 -04:00
} ) ;
2021-04-08 15:34:55 -04:00
return graphqlQuery ;
2020-09-14 14:24:15 -04:00
}
printToTerminal ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const queryResults = yield this . data ;
if ( ! queryResults ) {
return ;
}
info . group ( bold ( 'Github Tasks' ) ) ;
const minQueryNameLength = Math . max ( ... queryResults . map ( result => result . queryName . length ) ) ;
for ( const queryResult of queryResults ) {
info ( ` ${ queryResult . queryName . padEnd ( minQueryNameLength ) } ${ queryResult . count } ` ) ;
if ( queryResult . count > 0 ) {
info . group ( queryResult . queryUrl ) ;
queryResult . matchedUrls . forEach ( url => info ( ` - ${ url } ` ) ) ;
if ( queryResult . count > MAX _RETURNED _ISSUES ) {
info ( ` ... ${ queryResult . count - MAX _RETURNED _ISSUES } additional matches ` ) ;
}
info . groupEnd ( ) ;
2020-10-01 19:06:56 -04:00
}
}
2020-09-14 14:24:15 -04:00
info . groupEnd ( ) ;
info ( ) ;
2020-10-01 19:06:56 -04:00
} ) ;
2020-09-14 14:24:15 -04:00
}
2020-10-01 19:06:56 -04:00
}
/ * *
* @ 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
* /
2020-09-14 14:24:15 -04:00
/** List of services Angular relies on. */
const services = [
{
url : 'https://status.us-west-1.saucelabs.com/api/v2/status.json' ,
name : 'Saucelabs' ,
} ,
{
url : 'https://status.npmjs.org/api/v2/status.json' ,
name : 'Npm' ,
} ,
{
url : 'https://status.circleci.com/api/v2/status.json' ,
name : 'CircleCi' ,
} ,
{
url : 'https://www.githubstatus.com/api/v2/status.json' ,
name : 'Github' ,
} ,
] ;
class ServicesModule extends BaseModule {
retrieveData ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
return Promise . all ( services . map ( service => this . getStatusFromStandardApi ( service ) ) ) ;
} ) ;
2020-10-01 19:06:56 -04:00
}
2020-09-14 14:24:15 -04:00
printToTerminal ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const statuses = yield this . data ;
const serviceNameMinLength = Math . max ( ... statuses . map ( service => service . name . length ) ) ;
info . group ( bold ( 'Service Statuses' ) ) ;
for ( const status of statuses ) {
const name = status . name . padEnd ( serviceNameMinLength ) ;
if ( status . status === 'passing' ) {
info ( ` ${ name } ✅ ` ) ;
}
else {
info . group ( ` ${ name } ❌ (Updated: ${ status . lastUpdated . toLocaleString ( ) } ) ` ) ;
info ( ` Details: ${ status . description } ` ) ;
info . groupEnd ( ) ;
}
}
info . groupEnd ( ) ;
info ( ) ;
} ) ;
}
/** Retrieve the status information for a service which uses a standard API response. */
getStatusFromStandardApi ( service ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const result = yield fetch ( service . url ) . then ( result => result . json ( ) ) ;
const status = result . status . indicator === 'none' ? 'passing' : 'failing' ;
return {
name : service . name ,
status ,
description : result . status . description ,
lastUpdated : new Date ( result . page . updated _at )
} ;
} ) ;
2020-10-01 19:06:56 -04:00
}
}
/ * *
* @ 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
* /
2020-09-14 14:24:15 -04:00
/** List of modules checked for the caretaker check command. */
const moduleList = [
GithubQueriesModule ,
ServicesModule ,
CiModule ,
G3Module ,
] ;
2020-10-01 19:06:56 -04:00
/** Check the status of services which Angular caretakers need to monitor. */
2021-04-08 15:34:55 -04:00
function checkServiceStatuses ( ) {
2020-10-01 19:06:56 -04:00
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
/** The configuration for the caretaker commands. */
const config = getCaretakerConfig ( ) ;
2020-09-14 14:24:15 -04:00
/** List of instances of Caretaker Check modules */
2021-04-08 15:34:55 -04:00
const caretakerCheckModules = moduleList . map ( module => new module ( config ) ) ;
2020-09-14 14:24:15 -04:00
// 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.
yield Promise . all ( caretakerCheckModules . map ( module => module . data ) ) ;
for ( const module of caretakerCheckModules ) {
yield module . printToTerminal ( ) ;
}
2020-10-01 19:06:56 -04:00
} ) ;
}
/ * *
* @ 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
* /
/** Builds the command. */
function builder ( yargs ) {
return addGithubTokenOption ( yargs ) ;
}
/** Handles the command. */
2021-04-08 15:34:55 -04:00
function handler ( ) {
2020-10-01 19:06:56 -04:00
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
2021-04-08 15:34:55 -04:00
yield checkServiceStatuses ( ) ;
2020-10-01 19:06:56 -04:00
} ) ;
}
/** yargs command module for checking status information for the repository */
const CheckModule = {
handler ,
builder ,
command : 'check' ,
describe : 'Check the status of information the caretaker manages for the repository' ,
} ;
/ * *
* @ 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
* /
/** Build the parser for the caretaker commands. */
function buildCaretakerParser ( yargs ) {
return yargs . command ( CheckModule ) ;
}
/ * *
* @ 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
* /
/** Load the commit message draft from the file system if it exists. */
function loadCommitMessageDraft ( basePath ) {
const commitMessageDraftPath = ` ${ basePath } .ngDevSave ` ;
if ( fs . existsSync ( commitMessageDraftPath ) ) {
return fs . readFileSync ( commitMessageDraftPath ) . toString ( ) ;
}
return '' ;
}
/** Remove the commit message draft from the file system. */
function deleteCommitMessageDraft ( basePath ) {
const commitMessageDraftPath = ` ${ basePath } .ngDevSave ` ;
if ( fs . existsSync ( commitMessageDraftPath ) ) {
fs . unlinkSync ( commitMessageDraftPath ) ;
}
}
/** Save the commit message draft to the file system for later retrieval. */
function saveCommitMessageDraft ( basePath , commitMessage ) {
fs . writeFileSync ( ` ${ basePath } .ngDevSave ` , commitMessage ) ;
}
/ * *
* @ 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
* /
/ * *
* Restore the commit message draft to the git to be used as the default commit message .
*
* The source provided may be one of the sources described in
* https : //git-scm.com/docs/githooks#_prepare_commit_msg
* /
function restoreCommitMessage ( filePath , source ) {
if ( ! ! source ) {
log ( 'Skipping commit message restoration attempt' ) ;
if ( source === 'message' ) {
debug ( 'A commit message was already provided via the command with a -m or -F flag' ) ;
}
if ( source === 'template' ) {
debug ( 'A commit message was already provided via the -t flag or config.template setting' ) ;
}
if ( source === 'squash' ) {
debug ( 'A commit message was already provided as a merge action or via .git/MERGE_MSG' ) ;
}
if ( source === 'commit' ) {
debug ( 'A commit message was already provided through a revision specified via --fixup, -c,' ) ;
debug ( '-C or --amend flag' ) ;
}
process . exit ( 0 ) ;
}
/** A draft of a commit message. */
const commitMessage = loadCommitMessageDraft ( filePath ) ;
// If the commit message draft has content, restore it into the provided filepath.
if ( commitMessage ) {
fs . writeFileSync ( filePath , commitMessage ) ;
}
// Exit the process
process . exit ( 0 ) ;
}
/ * *
* @ 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
* /
/** Builds the command. */
function builder$1 ( yargs ) {
2020-11-18 11:48:59 -05:00
return yargs
. option ( 'file-env-variable' , {
2020-10-01 19:06:56 -04:00
type : 'string' ,
description : 'The key for the environment variable which holds the arguments for the\n' +
'prepare-commit-msg hook as described here:\n' +
2020-11-18 11:48:59 -05:00
'https://git-scm.com/docs/githooks#_prepare_commit_msg'
} )
. positional ( 'file' , { type : 'string' } )
. positional ( 'source' , { type : 'string' } ) ;
2020-10-01 19:06:56 -04:00
}
/** Handles the command. */
2020-11-18 11:48:59 -05:00
function handler$1 ( { fileEnvVariable , file , source } ) {
2020-10-01 19:06:56 -04:00
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
2020-11-18 11:48:59 -05:00
// File and source are provided as command line parameters
if ( file !== undefined ) {
restoreCommitMessage ( file , source ) ;
return ;
}
// File and source are provided as values held in an environment variable.
if ( fileEnvVariable !== undefined ) {
const [ fileFromEnv , sourceFromEnv ] = ( process . env [ fileEnvVariable ] || '' ) . split ( ' ' ) ;
if ( ! fileFromEnv ) {
throw new Error ( ` Provided environment variable " ${ fileEnvVariable } " was not found. ` ) ;
}
restoreCommitMessage ( fileFromEnv , sourceFromEnv ) ;
return ;
}
2021-03-24 19:17:15 -04:00
throw new Error ( 'No file path and commit message source provide. Provide values via positional command ' +
2020-11-18 11:48:59 -05:00
'arguments, or via the --file-env-variable flag' ) ;
2020-10-01 19:06:56 -04:00
} ) ;
}
2021-03-24 19:17:15 -04:00
/** yargs command module describing the command. */
2020-10-01 19:06:56 -04:00
const RestoreCommitMessageModule = {
handler : handler$1 ,
builder : builder$1 ,
2020-11-18 11:48:59 -05:00
command : 'restore-commit-message-draft [file] [source]' ,
2020-10-01 19:06:56 -04:00
// Description: Restore a commit message draft if one has been saved from a failed commit attempt.
// No describe is defiend to hide the command from the --help.
describe : false ,
} ;
/ * *
* @ 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
* /
/** Retrieve and validate the config as `CommitMessageConfig`. */
function getCommitMessageConfig ( ) {
// List of errors encountered validating the config.
const errors = [ ] ;
// The non-validated config object.
const config = getConfig ( ) ;
if ( config . commitMessage === undefined ) {
errors . push ( ` No configuration defined for "commitMessage" ` ) ;
}
assertNoErrors ( errors ) ;
return config ;
}
2021-03-24 19:17:15 -04:00
/** Scope requirement level to be set for each commit type. */
2020-10-01 19:06:56 -04:00
var ScopeRequirement ;
( function ( ScopeRequirement ) {
ScopeRequirement [ ScopeRequirement [ "Required" ] = 0 ] = "Required" ;
ScopeRequirement [ ScopeRequirement [ "Optional" ] = 1 ] = "Optional" ;
ScopeRequirement [ ScopeRequirement [ "Forbidden" ] = 2 ] = "Forbidden" ;
} ) ( ScopeRequirement || ( ScopeRequirement = { } ) ) ;
2021-04-01 19:03:38 -04:00
var ReleaseNotesLevel ;
( function ( ReleaseNotesLevel ) {
ReleaseNotesLevel [ ReleaseNotesLevel [ "Hidden" ] = 0 ] = "Hidden" ;
ReleaseNotesLevel [ ReleaseNotesLevel [ "Visible" ] = 1 ] = "Visible" ;
} ) ( ReleaseNotesLevel || ( ReleaseNotesLevel = { } ) ) ;
2020-10-01 19:06:56 -04:00
/** The valid commit types for Angular commit messages. */
const COMMIT _TYPES = {
build : {
name : 'build' ,
description : 'Changes to local repository build system and tooling' ,
scope : ScopeRequirement . Optional ,
2021-04-01 19:03:38 -04:00
releaseNotesLevel : ReleaseNotesLevel . Hidden ,
2020-10-01 19:06:56 -04:00
} ,
ci : {
name : 'ci' ,
description : 'Changes to CI configuration and CI specific tooling' ,
scope : ScopeRequirement . Forbidden ,
2021-04-01 19:03:38 -04:00
releaseNotesLevel : ReleaseNotesLevel . Hidden ,
2020-10-01 19:06:56 -04:00
} ,
docs : {
name : 'docs' ,
description : 'Changes which exclusively affects documentation.' ,
scope : ScopeRequirement . Optional ,
2021-04-01 19:03:38 -04:00
releaseNotesLevel : ReleaseNotesLevel . Hidden ,
2020-10-01 19:06:56 -04:00
} ,
feat : {
name : 'feat' ,
description : 'Creates a new feature' ,
scope : ScopeRequirement . Required ,
2021-04-01 19:03:38 -04:00
releaseNotesLevel : ReleaseNotesLevel . Visible ,
2020-10-01 19:06:56 -04:00
} ,
fix : {
name : 'fix' ,
description : 'Fixes a previously discovered failure/bug' ,
scope : ScopeRequirement . Required ,
2021-04-01 19:03:38 -04:00
releaseNotesLevel : ReleaseNotesLevel . Visible ,
2020-10-01 19:06:56 -04:00
} ,
perf : {
name : 'perf' ,
description : 'Improves performance without any change in functionality or API' ,
scope : ScopeRequirement . Required ,
2021-04-01 19:03:38 -04:00
releaseNotesLevel : ReleaseNotesLevel . Visible ,
2020-10-01 19:06:56 -04:00
} ,
refactor : {
name : 'refactor' ,
description : 'Refactor without any change in functionality or API (includes style changes)' ,
2021-04-07 09:35:31 -04:00
scope : ScopeRequirement . Optional ,
2021-04-01 19:03:38 -04:00
releaseNotesLevel : ReleaseNotesLevel . Hidden ,
2020-10-01 19:06:56 -04:00
} ,
release : {
name : 'release' ,
description : 'A release point in the repository' ,
scope : ScopeRequirement . Forbidden ,
2021-04-01 19:03:38 -04:00
releaseNotesLevel : ReleaseNotesLevel . Hidden ,
2020-10-01 19:06:56 -04:00
} ,
test : {
name : 'test' ,
description : 'Improvements or corrections made to the project\'s test suite' ,
2021-04-07 09:35:31 -04:00
scope : ScopeRequirement . Optional ,
2021-04-01 19:03:38 -04:00
releaseNotesLevel : ReleaseNotesLevel . Hidden ,
2020-10-01 19:06:56 -04:00
} ,
} ;
/ * *
* @ 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
* /
2021-03-24 19:17:15 -04:00
/ * *
* A list of tuples expressing the fields to extract from each commit log entry . The tuple contains
* two values , the first is the key for the property and the second is the template shortcut for the
* git log command .
* /
const commitFields = {
hash : '%H' ,
shortHash : '%h' ,
author : '%aN' ,
} ;
/** The commit fields described as git log format entries for parsing. */
const commitFieldsAsFormat = ( fields ) => {
return Object . entries ( fields ) . map ( ( [ key , value ] ) => ` %n- ${ key } -%n ${ value } ` ) . join ( '' ) ;
} ;
/ * *
* The git log format template to create git log entries for parsing .
*
* The conventional commits parser expects to parse the standard git log raw body ( % B ) into its
* component parts . Additionally it will parse additional fields with keys defined by
* ` -{key name}- ` separated by new lines .
* * /
const gitLogFormatForParsing = ` %B ${ commitFieldsAsFormat ( commitFields ) } ` ;
2021-03-19 15:11:21 -04:00
/** Markers used to denote the start of a note section in a commit. */
var NoteSections ;
( function ( NoteSections ) {
NoteSections [ "BREAKING_CHANGE" ] = "BREAKING CHANGE" ;
NoteSections [ "DEPRECATED" ] = "DEPRECATED" ;
} ) ( NoteSections || ( NoteSections = { } ) ) ;
2020-10-01 19:06:56 -04:00
/** Regex determining if a commit is a fixup. */
const FIXUP _PREFIX _RE = /^fixup! /i ;
/** Regex determining if a commit is a squash. */
const SQUASH _PREFIX _RE = /^squash! /i ;
/** Regex determining if a commit is a revert. */
const REVERT _PREFIX _RE = /^revert:? /i ;
2021-03-19 15:11:21 -04:00
/ * *
* Regex pattern for parsing the header line of a commit .
*
* Several groups are being matched to be used in the parsed commit object , being mapped to the
* ` headerCorrespondence ` object .
*
* The pattern can be broken down into component parts :
* - ` ( \w +) ` - a capturing group discovering the type of the commit .
* - ` (?: \( (?:([^/]+) \/ )?([^)]+) \) )? ` - a pair of capturing groups to capture the scope and ,
* optionally the npmScope of the commit .
* - ` (.*) ` - a capturing group discovering the subject of the commit .
* /
const headerPattern = /^(\w+)(?:\((?:([^/]+)\/)?([^)]+)\))?: (.*)$/ ;
/ * *
* The property names used for the values extracted from the header via the ` headerPattern ` regex .
* /
const headerCorrespondence = [ 'type' , 'npmScope' , 'scope' , 'subject' ] ;
/ * *
* Configuration options for the commit parser .
*
* NOTE : An extended type from ` Options ` must be used because the current
* @ types / conventional - commits - parser version does not include the ` notesPattern ` field .
* /
const parseOptions = {
commentChar : '#' ,
headerPattern ,
headerCorrespondence ,
noteKeywords : [ NoteSections . BREAKING _CHANGE , NoteSections . DEPRECATED ] ,
notesPattern : ( keywords ) => new RegExp ( ` ( ${ keywords } )(?:: ?)(.*) ` ) ,
} ;
2021-04-01 19:03:38 -04:00
/** Parse a commit message into its composite parts. */
const parseCommitMessage = parseInternal ;
/** Parse a commit message from a git log entry into its composite parts. */
const parseCommitFromGitLog = parseInternal ;
function parseInternal ( fullText ) {
2021-03-24 19:17:15 -04:00
// Ensure the fullText symbol is a `string`, even if a Buffer was provided.
fullText = fullText . toString ( ) ;
2021-03-19 15:11:21 -04:00
/** The commit message text with the fixup and squash markers stripped out. */
const strippedCommitMsg = fullText . replace ( FIXUP _PREFIX _RE , '' )
. replace ( SQUASH _PREFIX _RE , '' )
. replace ( REVERT _PREFIX _RE , '' ) ;
/** The initially parsed commit. */
const commit = conventionalCommitsParser . sync ( strippedCommitMsg , parseOptions ) ;
/** A list of breaking change notes from the commit. */
const breakingChanges = [ ] ;
/** A list of deprecation notes from the commit. */
const deprecations = [ ] ;
// Extract the commit message notes by marked types into their respective lists.
commit . notes . forEach ( ( note ) => {
if ( note . title === NoteSections . BREAKING _CHANGE ) {
return breakingChanges . push ( note ) ;
}
if ( note . title === NoteSections . DEPRECATED ) {
return deprecations . push ( note ) ;
}
} ) ;
2020-10-01 19:06:56 -04:00
return {
2021-03-19 15:11:21 -04:00
fullText ,
breakingChanges ,
deprecations ,
body : commit . body || '' ,
footer : commit . footer || '' ,
header : commit . header || '' ,
references : commit . references ,
scope : commit . scope || '' ,
subject : commit . subject || '' ,
type : commit . type || '' ,
npmScope : commit . npmScope || '' ,
isFixup : FIXUP _PREFIX _RE . test ( fullText ) ,
isSquash : SQUASH _PREFIX _RE . test ( fullText ) ,
isRevert : REVERT _PREFIX _RE . test ( fullText ) ,
2021-04-01 19:03:38 -04:00
author : commit . author || undefined ,
hash : commit . hash || undefined ,
shortHash : commit . shortHash || undefined ,
2020-10-01 19:06:56 -04:00
} ;
}
/ * *
* @ 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
* /
/** Regex matching a URL for an entire commit body line. */
const COMMIT _BODY _URL _LINE _RE = /^https?:\/\/.*$/ ;
2021-03-19 05:48:35 -04:00
/ * *
* Regex matching a breaking change .
*
* - Starts with BREAKING CHANGE
* - Followed by a colon
* - Followed by a single space or two consecutive new lines
*
* NB : Anything after ` BREAKING CHANGE ` is optional to facilitate the validation .
* /
const COMMIT _BODY _BREAKING _CHANGE _RE = /^BREAKING CHANGE(:( |\n{2}))?/m ;
2020-10-01 19:06:56 -04:00
/** Validate a commit message against using the local repo's config. */
function validateCommitMessage ( commitMsg , options = { } ) {
const config = getCommitMessageConfig ( ) . commitMessage ;
2020-11-17 14:20:41 -05:00
const commit = typeof commitMsg === 'string' ? parseCommitMessage ( commitMsg ) : commitMsg ;
2020-10-01 19:06:56 -04:00
const errors = [ ] ;
/** Perform the validation checks against the parsed commit. */
function validateCommitAndCollectErrors ( ) {
////////////////////////////////////
// Checking revert, squash, fixup //
////////////////////////////////////
2021-03-19 15:11:21 -04:00
var _a ;
2020-10-01 19:06:56 -04:00
// All revert commits are considered valid.
if ( commit . isRevert ) {
return true ;
}
// All squashes are considered valid, as the commit will be squashed into another in
// the git history anyway, unless the options provided to not allow squash commits.
if ( commit . isSquash ) {
if ( options . disallowSquash ) {
errors . push ( 'The commit must be manually squashed into the target commit' ) ;
return false ;
}
return true ;
}
// Fixups commits are considered valid, unless nonFixupCommitHeaders are provided to check
// against. If `nonFixupCommitHeaders` is not empty, we check whether there is a corresponding
// non-fixup commit (i.e. a commit whose header is identical to this commit's header after
// stripping the `fixup! ` prefix), otherwise we assume this verification will happen in another
// check.
if ( commit . isFixup ) {
if ( options . nonFixupCommitHeaders && ! options . nonFixupCommitHeaders . includes ( commit . header ) ) {
errors . push ( 'Unable to find match for fixup commit among prior commits: ' +
( options . nonFixupCommitHeaders . map ( x => ` \n ${ x } ` ) . join ( '' ) || '-' ) ) ;
return false ;
}
return true ;
}
////////////////////////////
// Checking commit header //
////////////////////////////
if ( commit . header . length > config . maxLineLength ) {
errors . push ( ` The commit message header is longer than ${ config . maxLineLength } characters ` ) ;
return false ;
}
if ( ! commit . type ) {
errors . push ( ` The commit message header does not match the expected format. ` ) ;
return false ;
}
if ( COMMIT _TYPES [ commit . type ] === undefined ) {
errors . push ( ` ' ${ commit . type } ' is not an allowed type. \n => TYPES: ${ Object . keys ( COMMIT _TYPES ) . join ( ', ' ) } ` ) ;
return false ;
}
/** The scope requirement level for the provided type of the commit message. */
const scopeRequirementForType = COMMIT _TYPES [ commit . type ] . scope ;
if ( scopeRequirementForType === ScopeRequirement . Forbidden && commit . scope ) {
errors . push ( ` Scopes are forbidden for commits with type ' ${ commit . type } ', but a scope of ' ${ commit . scope } ' was provided. ` ) ;
return false ;
}
if ( scopeRequirementForType === ScopeRequirement . Required && ! commit . scope ) {
errors . push ( ` Scopes are required for commits with type ' ${ commit . type } ', but no scope was provided. ` ) ;
return false ;
}
2021-04-02 09:59:52 -04:00
const fullScope = commit . npmScope ? ` ${ commit . npmScope } / ${ commit . scope } ` : commit . scope ;
if ( fullScope && ! config . scopes . includes ( fullScope ) ) {
errors . push ( ` ' ${ fullScope } ' is not an allowed scope. \n => SCOPES: ${ config . scopes . join ( ', ' ) } ` ) ;
2020-10-01 19:06:56 -04:00
return false ;
}
// Commits with the type of `release` do not require a commit body.
if ( commit . type === 'release' ) {
return true ;
}
//////////////////////////
// Checking commit body //
//////////////////////////
2021-03-29 14:10:43 -04:00
// Due to an issue in which conventional-commits-parser considers all parts of a commit after
// a `#` reference to be the footer, we check the length of all of the commit content after the
// header. In the future, we expect to be able to check only the body once the parser properly
// handles this case.
const allNonHeaderContent = ` ${ commit . body . trim ( ) } \n ${ commit . footer . trim ( ) } ` ;
2020-10-01 19:06:56 -04:00
if ( ! ( ( _a = config . minBodyLengthTypeExcludes ) === null || _a === void 0 ? void 0 : _a . includes ( commit . type ) ) &&
2021-03-29 14:10:43 -04:00
allNonHeaderContent . length < config . minBodyLength ) {
2020-10-01 19:06:56 -04:00
errors . push ( ` The commit message body does not meet the minimum length of ${ config . minBodyLength } characters ` ) ;
return false ;
}
const bodyByLine = commit . body . split ( '\n' ) ;
2021-03-19 15:11:21 -04:00
const lineExceedsMaxLength = bodyByLine . some ( ( line ) => {
2020-10-01 19:06:56 -04:00
// Check if any line exceeds the max line length limit. The limit is ignored for
// lines that just contain an URL (as these usually cannot be wrapped or shortened).
return line . length > config . maxLineLength && ! COMMIT _BODY _URL _LINE _RE . test ( line ) ;
} ) ;
if ( lineExceedsMaxLength ) {
2021-03-19 05:48:35 -04:00
errors . push ( ` The commit message body contains lines greater than ${ config . maxLineLength } characters. ` ) ;
2020-10-01 19:06:56 -04:00
return false ;
}
2021-03-19 05:48:35 -04:00
// Breaking change
// Check if the commit message contains a valid break change description.
// https://github.com/angular/angular/blob/88fbc066775ab1a2f6a8c75f933375b46d8fa9a4/CONTRIBUTING.md#commit-message-footer
2021-03-19 15:11:21 -04:00
const hasBreakingChange = COMMIT _BODY _BREAKING _CHANGE _RE . exec ( commit . fullText ) ;
2021-03-19 05:48:35 -04:00
if ( hasBreakingChange !== null ) {
const [ , breakingChangeDescription ] = hasBreakingChange ;
if ( ! breakingChangeDescription ) {
// Not followed by :, space or two consecutive new lines,
errors . push ( ` The commit message body contains an invalid breaking change description. ` ) ;
return false ;
}
}
2020-10-01 19:06:56 -04:00
return true ;
}
return { valid : validateCommitAndCollectErrors ( ) , errors , commit } ;
}
/** Print the error messages from the commit message validation to the console. */
function printValidationErrors ( errors , print = error ) {
print . group ( ` Error ${ errors . length === 1 ? '' : 's' } : ` ) ;
errors . forEach ( line => print ( line ) ) ;
print . groupEnd ( ) ;
print ( ) ;
print ( 'The expected format for a commit is: ' ) ;
print ( '<type>(<scope>): <summary>' ) ;
print ( ) ;
print ( '<body>' ) ;
print ( ) ;
2021-03-19 05:48:35 -04:00
print ( ` BREAKING CHANGE: <breaking change summary> ` ) ;
print ( ) ;
print ( ` <breaking change description> ` ) ;
print ( ) ;
print ( ) ;
2020-10-01 19:06:56 -04:00
}
/ * *
* @ 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
* /
/** Validate commit message at the provided file path. */
function validateFile ( filePath , isErrorMode ) {
2021-04-12 17:50:50 -04:00
const git = GitClient . getInstance ( ) ;
const commitMessage = fs . readFileSync ( path . resolve ( git . baseDir , filePath ) , 'utf8' ) ;
2020-10-01 19:06:56 -04:00
const { valid , errors } = validateCommitMessage ( commitMessage ) ;
if ( valid ) {
info ( ` ${ green ( '√' ) } Valid commit message ` ) ;
deleteCommitMessageDraft ( filePath ) ;
process . exitCode = 0 ;
return ;
}
/** Function used to print to the console log. */
let printFn = isErrorMode ? error : log ;
printFn ( ` ${ isErrorMode ? red ( '✘' ) : yellow ( '!' ) } Invalid commit message ` ) ;
printValidationErrors ( errors , printFn ) ;
if ( isErrorMode ) {
printFn ( red ( 'Aborting commit attempt due to invalid commit message.' ) ) ;
printFn ( red ( 'Commit message aborted as failure rather than warning due to local configuration.' ) ) ;
}
else {
printFn ( yellow ( 'Before this commit can be merged into the upstream repository, it must be' ) ) ;
printFn ( yellow ( 'amended to follow commit message guidelines.' ) ) ;
}
// On all invalid commit messages, the commit message should be saved as a draft to be
// restored on the next commit attempt.
saveCommitMessageDraft ( filePath , commitMessage ) ;
// Set the correct exit code based on if invalid commit message is an error.
process . exitCode = isErrorMode ? 1 : 0 ;
}
/ * *
* @ 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
* /
/** Builds the command. */
function builder$2 ( yargs ) {
var _a ;
return yargs
. option ( 'file' , {
type : 'string' ,
conflicts : [ 'file-env-variable' ] ,
description : 'The path of the commit message file.' ,
} )
. option ( 'file-env-variable' , {
type : 'string' ,
conflicts : [ 'file' ] ,
description : 'The key of the environment variable for the path of the commit message file.' ,
coerce : ( arg ) => {
2021-05-04 19:25:39 -04:00
if ( arg === undefined ) {
return arg ;
}
2020-10-01 19:06:56 -04:00
const file = process . env [ arg ] ;
if ( ! file ) {
throw new Error ( ` Provided environment variable " ${ arg } " was not found. ` ) ;
}
return file ;
} ,
} )
. option ( 'error' , {
type : 'boolean' ,
description : 'Whether invalid commit messages should be treated as failures rather than a warning' ,
default : ! ! ( ( _a = getUserConfig ( ) . commitMessage ) === null || _a === void 0 ? void 0 : _a . errorOnInvalidMessage ) || ! ! process . env [ 'CI' ]
} ) ;
}
/** Handles the command. */
function handler$2 ( { error , file , fileEnvVariable } ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const filePath = file || fileEnvVariable || '.git/COMMIT_EDITMSG' ;
validateFile ( filePath , error ) ;
} ) ;
}
2021-03-24 19:17:15 -04:00
/** yargs command module describing the command. */
2020-10-01 19:06:56 -04:00
const ValidateFileModule = {
handler : handler$2 ,
builder : builder$2 ,
command : 'pre-commit-validate' ,
describe : 'Validate the most recent commit message' ,
} ;
2020-11-17 14:20:41 -05:00
/ * *
* @ 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
* /
2021-03-24 19:17:15 -04:00
// Set `gitCommits` as this imported value to address "Cannot call a namespace" error.
const gitCommits = gitCommits _ ;
/ * *
* Find all commits within the given range and return an object describing those .
* /
function getCommitsInRange ( from , to = 'HEAD' ) {
return new Promise ( ( resolve , reject ) => {
/** List of parsed commit objects. */
const commits = [ ] ;
/** Stream of raw git commit strings in the range provided. */
const commitStream = gitCommits ( { from , to , format : gitLogFormatForParsing } ) ;
// Accumulate the parsed commits for each commit from the Readable stream into an array, then
// resolve the promise with the array when the Readable stream ends.
2021-04-01 19:03:38 -04:00
commitStream . on ( 'data' , ( commit ) => commits . push ( parseCommitFromGitLog ( commit ) ) ) ;
2021-03-24 19:17:15 -04:00
commitStream . on ( 'error' , ( err ) => reject ( err ) ) ;
commitStream . on ( 'end' , ( ) => resolve ( commits ) ) ;
} ) ;
}
2020-11-17 14:20:41 -05:00
// Whether the provided commit is a fixup commit.
const isNonFixup = ( commit ) => ! commit . isFixup ;
// Extracts commit header (first line of commit message).
const extractCommitHeader = ( commit ) => commit . header ;
/** Validate all commits in a provided git commit range. */
2021-03-24 19:17:15 -04:00
function validateCommitRange ( from , to ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
/** A list of tuples of the commit header string and a list of error messages for the commit. */
const errors = [ ] ;
/** A list of parsed commit messages from the range. */
const commits = yield getCommitsInRange ( from , to ) ;
info ( ` Examining ${ commits . length } commit(s) in the provided range: ${ from } .. ${ to } ` ) ;
/ * *
* Whether all commits in the range are valid , commits are allowed to be fixup commits for other
* commits in the provided commit range .
* /
const allCommitsInRangeValid = commits . every ( ( commit , i ) => {
const options = {
disallowSquash : true ,
nonFixupCommitHeaders : isNonFixup ( commit ) ?
undefined :
commits . slice ( i + 1 ) . filter ( isNonFixup ) . map ( extractCommitHeader )
} ;
const { valid , errors : localErrors } = validateCommitMessage ( commit , options ) ;
if ( localErrors . length ) {
errors . push ( [ commit . header , localErrors ] ) ;
}
return valid ;
} ) ;
if ( allCommitsInRangeValid ) {
info ( green ( '√ All commit messages in range valid.' ) ) ;
}
else {
error ( red ( '✘ Invalid commit message' ) ) ;
errors . forEach ( ( [ header , validationErrors ] ) => {
error . group ( header ) ;
printValidationErrors ( validationErrors ) ;
error . groupEnd ( ) ;
} ) ;
// Exit with a non-zero exit code if invalid commit messages have
// been discovered.
process . exit ( 1 ) ;
2020-10-01 19:06:56 -04:00
}
} ) ;
}
/ * *
* @ 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
* /
/** Builds the command. */
function builder$3 ( yargs ) {
2021-03-24 19:17:15 -04:00
return yargs
. positional ( 'startingRef' , {
description : 'The first ref in the range to select' ,
2020-10-01 19:06:56 -04:00
type : 'string' ,
2021-03-24 19:17:15 -04:00
demandOption : true ,
} )
. positional ( 'endingRef' , {
description : 'The last ref in the range to select' ,
type : 'string' ,
default : 'HEAD' ,
2020-10-01 19:06:56 -04:00
} ) ;
}
/** Handles the command. */
2021-03-24 19:17:15 -04:00
function handler$3 ( { startingRef , endingRef } ) {
2020-10-01 19:06:56 -04:00
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
// If on CI, and no pull request number is provided, assume the branch
// being run on is an upstream branch.
if ( process . env [ 'CI' ] && process . env [ 'CI_PULL_REQUEST' ] === 'false' ) {
info ( ` Since valid commit messages are enforced by PR linting on CI, we do not ` ) ;
info ( ` need to validate commit messages on CI runs on upstream branches. ` ) ;
info ( ) ;
info ( ` Skipping check of provided commit range ` ) ;
return ;
}
2021-03-24 19:17:15 -04:00
yield validateCommitRange ( startingRef , endingRef ) ;
2020-10-01 19:06:56 -04:00
} ) ;
}
2021-03-24 19:17:15 -04:00
/** yargs command module describing the command. */
2020-10-01 19:06:56 -04:00
const ValidateRangeModule = {
handler : handler$3 ,
builder : builder$3 ,
2021-03-24 19:17:15 -04:00
command : 'validate-range <starting-ref> [ending-ref]' ,
2020-10-01 19:06:56 -04:00
describe : 'Validate a range of commit messages' ,
} ;
/** Build the parser for the commit-message commands. */
function buildCommitMessageParser ( localYargs ) {
return localYargs . help ( )
. strict ( )
. command ( RestoreCommitMessageModule )
. command ( ValidateFileModule )
. command ( ValidateRangeModule ) ;
}
/ * *
* @ 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
* /
/** Retrieve and validate the config as `FormatConfig`. */
function getFormatConfig ( ) {
// List of errors encountered validating the config.
const errors = [ ] ;
// The unvalidated config object.
const config = getConfig ( ) ;
if ( config . format === undefined ) {
errors . push ( ` No configuration defined for "format" ` ) ;
}
for ( const [ key , value ] of Object . entries ( config . format ) ) {
switch ( typeof value ) {
case 'boolean' :
break ;
case 'object' :
checkFormatterConfig ( key , value , errors ) ;
break ;
default :
errors . push ( ` "format. ${ key } " is not a boolean or Formatter object ` ) ;
}
}
assertNoErrors ( errors ) ;
return config ;
}
/** Validate an individual Formatter config. */
function checkFormatterConfig ( key , config , errors ) {
if ( config . matchers === undefined ) {
errors . push ( ` Missing "format. ${ key } .matchers" value ` ) ;
}
}
/ * *
* @ 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
* /
/ * *
* The base class for formatters to run against provided files .
* /
class Formatter {
constructor ( config ) {
this . config = config ;
2021-04-12 17:50:50 -04:00
this . git = GitClient . getInstance ( ) ;
2020-10-01 19:06:56 -04:00
}
/ * *
* Retrieve the command to execute the provided action , including both the binary
* and command line flags .
* /
commandFor ( action ) {
switch ( action ) {
case 'check' :
return ` ${ this . binaryFilePath } ${ this . actions . check . commandFlags } ` ;
case 'format' :
return ` ${ this . binaryFilePath } ${ this . actions . format . commandFlags } ` ;
default :
throw Error ( 'Unknown action type' ) ;
}
}
/ * *
* Retrieve the callback for the provided action to determine if an action
* failed in formatting .
* /
callbackFor ( action ) {
switch ( action ) {
case 'check' :
return this . actions . check . callback ;
case 'format' :
return this . actions . format . callback ;
default :
throw Error ( 'Unknown action type' ) ;
}
}
/** Whether the formatter is enabled in the provided config. */
isEnabled ( ) {
return ! ! this . config [ this . name ] ;
}
/** Retrieve the active file matcher for the formatter. */
getFileMatcher ( ) {
return this . getFileMatcherFromConfig ( ) || this . defaultFileMatcher ;
}
/ * *
* Retrieves the file matcher from the config provided to the constructor if provided .
* /
getFileMatcherFromConfig ( ) {
const formatterConfig = this . config [ this . name ] ;
if ( typeof formatterConfig === 'boolean' ) {
return undefined ;
}
return formatterConfig . matchers ;
}
}
/ * *
* @ 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
* /
/ * *
* Formatter for running buildifier against bazel related files .
* /
class Buildifier extends Formatter {
constructor ( ) {
super ( ... arguments ) ;
this . name = 'buildifier' ;
2021-04-12 17:50:50 -04:00
this . binaryFilePath = path . join ( this . git . baseDir , 'node_modules/.bin/buildifier' ) ;
2020-10-01 19:06:56 -04:00
this . defaultFileMatcher = [ '**/*.bzl' , '**/BUILD.bazel' , '**/WORKSPACE' , '**/BUILD' ] ;
this . actions = {
check : {
commandFlags : ` ${ BAZEL _WARNING _FLAG } --lint=warn --mode=check --format=json ` ,
callback : ( _ , code , stdout ) => {
2021-02-04 17:42:42 -05:00
return code !== 0 || ! JSON . parse ( stdout ) . success ;
2020-10-01 19:06:56 -04:00
} ,
} ,
format : {
commandFlags : ` ${ BAZEL _WARNING _FLAG } --lint=fix --mode=fix ` ,
callback : ( file , code , _ , stderr ) => {
if ( code !== 0 ) {
error ( ` Error running buildifier on: ${ file } ` ) ;
error ( stderr ) ;
error ( ) ;
return true ;
}
return false ;
}
}
} ;
}
}
// The warning flag for buildifier copied from angular/angular's usage.
const BAZEL _WARNING _FLAG = ` --warnings=attr-cfg,attr-license,attr-non-empty,attr-output-default, ` +
` attr-single-file,constant-glob,ctx-args,depset-iteration,depset-union,dict-concatenation, ` +
` duplicated-name,filetype,git-repository,http-archive,integer-division,load,load-on-top, ` +
` native-build,native-package,output-group,package-name,package-on-top,positional-args, ` +
` redefined-variable,repository-name,same-origin-load,string-iteration,unused-variable ` ;
/ * *
* @ 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
* /
/ * *
* Formatter for running clang - format against Typescript and Javascript files
* /
class ClangFormat extends Formatter {
constructor ( ) {
super ( ... arguments ) ;
this . name = 'clang-format' ;
2021-04-12 17:50:50 -04:00
this . binaryFilePath = path . join ( this . git . baseDir , 'node_modules/.bin/clang-format' ) ;
2020-10-01 19:06:56 -04:00
this . defaultFileMatcher = [ '**/*.{t,j}s' ] ;
this . actions = {
check : {
commandFlags : ` --Werror -n -style=file ` ,
callback : ( _ , code ) => {
return code !== 0 ;
} ,
} ,
format : {
commandFlags : ` -i -style=file ` ,
callback : ( file , code , _ , stderr ) => {
if ( code !== 0 ) {
error ( ` Error running clang-format on: ${ file } ` ) ;
error ( stderr ) ;
error ( ) ;
return true ;
}
return false ;
}
}
} ;
}
}
2021-04-26 15:45:02 -04:00
/ * *
* @ 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
* /
/ * *
* Formatter for running prettier against Typescript and Javascript files .
* /
class Prettier extends Formatter {
constructor ( ) {
super ( ... arguments ) ;
this . name = 'prettier' ;
this . binaryFilePath = path . join ( this . git . baseDir , 'node_modules/.bin/prettier' ) ;
this . defaultFileMatcher = [ '**/*.{t,j}s' ] ;
/ * *
2021-04-28 10:10:24 -04:00
* The configuration path of the prettier config , obtained during construction to prevent needing
2021-04-26 15:45:02 -04:00
* to discover it repeatedly for each execution .
* /
2021-04-28 10:10:24 -04:00
this . configPath = this . config [ 'prettier' ] ? shelljs . exec ( ` ${ this . binaryFilePath } --find-config-path . ` ) . trim ( ) : '' ;
2021-04-26 15:45:02 -04:00
this . actions = {
check : {
commandFlags : ` --config ${ this . configPath } --check ` ,
callback : ( _ , code , stdout ) => {
return code !== 0 ;
} ,
} ,
format : {
commandFlags : ` --config ${ this . configPath } --write ` ,
callback : ( file , code , _ , stderr ) => {
if ( code !== 0 ) {
error ( ` Error running prettier on: ${ file } ` ) ;
error ( stderr ) ;
error ( ) ;
return true ;
}
return false ;
} ,
} ,
} ;
}
}
2020-10-01 19:06:56 -04:00
/ * *
* @ 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
* /
/ * *
* Get all defined formatters which are active based on the current loaded config .
* /
function getActiveFormatters ( ) {
const config = getFormatConfig ( ) . format ;
2021-04-26 15:45:02 -04:00
return [
new Prettier ( config ) ,
new Buildifier ( config ) ,
new ClangFormat ( config ) ,
] . filter ( ( formatter ) => formatter . isEnabled ( ) ) ;
2020-10-01 19:06:56 -04:00
}
/ * *
* @ 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
* /
const AVAILABLE _THREADS = Math . max ( os . cpus ( ) . length - 1 , 1 ) ;
/ * *
* Run the provided commands in parallel for each provided file .
*
* Running the formatter is split across ( number of available cpu threads - 1 ) processess .
* The task is done in multiple processess to speed up the overall time of the task , as running
* across entire repositories takes a large amount of time .
* As a data point for illustration , using 8 process rather than 1 cut the execution
* time from 276 seconds to 39 seconds for the same 2700 files .
*
* A promise is returned , completed when the command has completed running for each file .
* The promise resolves with a list of failures , or ` false ` if no formatters have matched .
* /
function runFormatterInParallel ( allFiles , action ) {
return new Promise ( ( resolve ) => {
const formatters = getActiveFormatters ( ) ;
const failures = [ ] ;
const pendingCommands = [ ] ;
for ( const formatter of formatters ) {
pendingCommands . push ( ... multimatch . call ( undefined , allFiles , formatter . getFileMatcher ( ) , { dot : true } )
. map ( file => ( { formatter , file } ) ) ) ;
}
// If no commands are generated, resolve the promise as `false` as no files
// were run against the any formatters.
if ( pendingCommands . length === 0 ) {
return resolve ( false ) ;
}
switch ( action ) {
case 'format' :
info ( ` Formatting ${ pendingCommands . length } file(s) ` ) ;
break ;
case 'check' :
info ( ` Checking format of ${ pendingCommands . length } file(s) ` ) ;
break ;
default :
throw Error ( ` Invalid format action " ${ action } ": allowed actions are "format" and "check" ` ) ;
}
// The progress bar instance to use for progress tracking.
const progressBar = new cliProgress . Bar ( { format : ` [{bar}] ETA: {eta}s | {value}/{total} files ` , clearOnComplete : true } ) ;
// A local copy of the files to run the command on.
// An array to represent the current usage state of each of the threads for parallelization.
const threads = new Array ( AVAILABLE _THREADS ) . fill ( false ) ;
// Recursively run the command on the next available file from the list using the provided
// thread.
function runCommandInThread ( thread ) {
const nextCommand = pendingCommands . pop ( ) ;
// If no file was pulled from the array, return as there are no more files to run against.
if ( nextCommand === undefined ) {
threads [ thread ] = false ;
return ;
}
// Get the file and formatter for the next command.
const { file , formatter } = nextCommand ;
shelljs . exec ( ` ${ formatter . commandFor ( action ) } ${ file } ` , { async : true , silent : true } , ( code , stdout , stderr ) => {
// Run the provided callback function.
const failed = formatter . callbackFor ( action ) ( file , code , stdout , stderr ) ;
if ( failed ) {
failures . push ( file ) ;
}
// Note in the progress bar another file being completed.
progressBar . increment ( 1 ) ;
// If more files exist in the list, run again to work on the next file,
// using the same slot.
if ( pendingCommands . length ) {
return runCommandInThread ( thread ) ;
}
// If not more files are available, mark the thread as unused.
threads [ thread ] = false ;
// If all of the threads are false, as they are unused, mark the progress bar
// completed and resolve the promise.
if ( threads . every ( active => ! active ) ) {
progressBar . stop ( ) ;
resolve ( failures ) ;
}
} ) ;
// Mark the thread as in use as the command execution has been started.
threads [ thread ] = true ;
}
// Start the progress bar
progressBar . start ( pendingCommands . length , 0 ) ;
// Start running the command on files from the least in each available thread.
threads . forEach ( ( _ , idx ) => runCommandInThread ( idx ) ) ;
} ) ;
}
/ * *
* @ 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
* /
/ * *
* Format provided files in place .
* /
function formatFiles ( files ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
// Whether any files failed to format.
let failures = yield runFormatterInParallel ( files , 'format' ) ;
if ( failures === false ) {
info ( 'No files matched for formatting.' ) ;
process . exit ( 0 ) ;
}
// The process should exit as a failure if any of the files failed to format.
if ( failures . length !== 0 ) {
error ( ` Formatting failed, see errors above for more information. ` ) ;
process . exit ( 1 ) ;
}
info ( ` √ Formatting complete. ` ) ;
process . exit ( 0 ) ;
} ) ;
}
/ * *
* Check provided files for formatting correctness .
* /
function checkFiles ( files ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
// Files which are currently not formatted correctly.
const failures = yield runFormatterInParallel ( files , 'check' ) ;
if ( failures === false ) {
info ( 'No files matched for formatting check.' ) ;
process . exit ( 0 ) ;
}
if ( failures . length ) {
// Provide output expressing which files are failing formatting.
info . group ( '\nThe following files are out of format:' ) ;
for ( const file of failures ) {
info ( ` - ${ file } ` ) ;
}
info . groupEnd ( ) ;
info ( ) ;
// If the command is run in a non-CI environment, prompt to format the files immediately.
let runFormatter = false ;
if ( ! process . env [ 'CI' ] ) {
runFormatter = yield promptConfirm ( 'Format the files now?' , true ) ;
}
if ( runFormatter ) {
// Format the failing files as requested.
yield formatFiles ( failures ) ;
process . exit ( 0 ) ;
}
else {
// Inform user how to format files in the future.
info ( ) ;
info ( ` To format the failing file run the following command: ` ) ;
info ( ` yarn ng-dev format files ${ failures . join ( ' ' ) } ` ) ;
process . exit ( 1 ) ;
}
}
else {
info ( '√ All files correctly formatted.' ) ;
process . exit ( 0 ) ;
}
} ) ;
}
/** Build the parser for the format commands. */
function buildFormatParser ( localYargs ) {
return localYargs . help ( )
. strict ( )
. demandCommand ( )
. option ( 'check' , {
type : 'boolean' ,
default : process . env [ 'CI' ] ? true : false ,
description : 'Run the formatter to check formatting rather than updating code format'
} )
. command ( 'all' , 'Run the formatter on all files in the repository' , args => args , ( { check } ) => {
const executionCmd = check ? checkFiles : formatFiles ;
2021-04-12 17:50:50 -04:00
const allFiles = GitClient . getInstance ( ) . allFiles ( ) ;
executionCmd ( allFiles ) ;
2020-10-01 19:06:56 -04:00
} )
. command ( 'changed [shaOrRef]' , 'Run the formatter on files changed since the provided sha/ref' , args => args . positional ( 'shaOrRef' , { type : 'string' } ) , ( { shaOrRef , check } ) => {
const sha = shaOrRef || 'master' ;
const executionCmd = check ? checkFiles : formatFiles ;
2021-04-12 17:50:50 -04:00
const allChangedFilesSince = GitClient . getInstance ( ) . allChangesFilesSince ( sha ) ;
executionCmd ( allChangedFilesSince ) ;
2020-10-01 19:06:56 -04:00
} )
. command ( 'staged' , 'Run the formatter on all staged files' , args => args , ( { check } ) => {
const executionCmd = check ? checkFiles : formatFiles ;
2021-04-12 17:50:50 -04:00
const allStagedFiles = GitClient . getInstance ( ) . allStagedFiles ( ) ;
executionCmd ( allStagedFiles ) ;
2020-10-01 19:06:56 -04:00
} )
. command ( 'files <files..>' , 'Run the formatter on provided files' , args => args . positional ( 'files' , { array : true , type : 'string' } ) , ( { check , files } ) => {
const executionCmd = check ? checkFiles : formatFiles ;
executionCmd ( files ) ;
} ) ;
}
/ * *
* @ 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
* /
function verify ( ) {
2021-04-12 17:50:50 -04:00
const git = GitClient . getInstance ( ) ;
2020-10-01 19:06:56 -04:00
/** Full path to NgBot config file */
2021-04-12 17:50:50 -04:00
const NGBOT _CONFIG _YAML _PATH = path . resolve ( git . baseDir , '.github/angular-robot.yml' ) ;
2020-10-01 19:06:56 -04:00
/** The NgBot config file */
const ngBotYaml = fs . readFileSync ( NGBOT _CONFIG _YAML _PATH , 'utf8' ) ;
try {
// Try parsing the config file to verify that the syntax is correct.
yaml . parse ( ngBotYaml ) ;
info ( ` ${ green ( '√' ) } Valid NgBot YAML config ` ) ;
}
catch ( e ) {
error ( ` ${ red ( '!' ) } Invalid NgBot YAML config ` ) ;
error ( e ) ;
process . exitCode = 1 ;
}
}
/** Build the parser for the NgBot commands. */
function buildNgbotParser ( localYargs ) {
return localYargs . help ( ) . strict ( ) . demandCommand ( ) . command ( 'verify' , 'Verify the NgBot config' , { } , ( ) => verify ( ) ) ;
}
2020-10-28 20:14:40 -04:00
/ * *
* @ 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
* /
/** Loads and validates the merge configuration. */
function loadAndValidateConfig ( config , api ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
var mergeConfig , errors ;
return tslib . _ _generator ( this , function ( _a ) {
switch ( _a . label ) {
case 0 :
if ( config . merge === undefined ) {
return [ 2 /*return*/ , { errors : [ 'No merge configuration found. Set the `merge` configuration.' ] } ] ;
}
if ( typeof config . merge !== 'function' ) {
return [ 2 /*return*/ , { errors : [ 'Expected merge configuration to be defined lazily through a function.' ] } ] ;
}
return [ 4 /*yield*/ , config . merge ( api ) ] ;
case 1 :
mergeConfig = _a . sent ( ) ;
errors = validateMergeConfig ( mergeConfig ) ;
if ( errors . length ) {
return [ 2 /*return*/ , { errors : errors } ] ;
}
return [ 2 /*return*/ , { config : mergeConfig } ] ;
}
} ) ;
} ) ;
}
/** Validates the specified configuration. Returns a list of failure messages. */
function validateMergeConfig ( config ) {
var errors = [ ] ;
if ( ! config . labels ) {
errors . push ( 'No label configuration.' ) ;
}
else if ( ! Array . isArray ( config . labels ) ) {
errors . push ( 'Label configuration needs to be an array.' ) ;
}
if ( ! config . claSignedLabel ) {
errors . push ( 'No CLA signed label configured.' ) ;
}
if ( ! config . mergeReadyLabel ) {
errors . push ( 'No merge ready label configured.' ) ;
}
if ( config . githubApiMerge === undefined ) {
errors . push ( 'No explicit choice of merge strategy. Please set `githubApiMerge`.' ) ;
}
return errors ;
}
/ * *
* @ 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
* /
/** Checks whether the specified value matches the given pattern. */
function matchesPattern ( value , pattern ) {
return typeof pattern === 'string' ? value === pattern : pattern . test ( value ) ;
}
/ * *
* @ 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
* /
/ * *
* Unique error that can be thrown in the merge configuration if an
* invalid branch is targeted .
* /
var InvalidTargetBranchError = /** @class */ ( function ( ) {
function InvalidTargetBranchError ( failureMessage ) {
this . failureMessage = failureMessage ;
}
return InvalidTargetBranchError ;
} ( ) ) ;
/ * *
* Unique error that can be thrown in the merge configuration if an
* invalid label has been applied to a pull request .
* /
var InvalidTargetLabelError = /** @class */ ( function ( ) {
function InvalidTargetLabelError ( failureMessage ) {
this . failureMessage = failureMessage ;
}
return InvalidTargetLabelError ;
} ( ) ) ;
/** Gets the target label from the specified pull request labels. */
function getTargetLabelFromPullRequest ( config , labels ) {
var e _1 , _a ;
2020-12-16 13:02:48 -05:00
/** List of discovered target labels for the PR. */
var matches = [ ] ;
2020-10-28 20:14:40 -04:00
var _loop _1 = function ( label ) {
var match = config . labels . find ( function ( _a ) {
var pattern = _a . pattern ;
return matchesPattern ( label , pattern ) ;
} ) ;
if ( match !== undefined ) {
2020-12-16 13:02:48 -05:00
matches . push ( match ) ;
2020-10-28 20:14:40 -04:00
}
} ;
try {
for ( var labels _1 = tslib . _ _values ( labels ) , labels _1 _1 = labels _1 . next ( ) ; ! labels _1 _1 . done ; labels _1 _1 = labels _1 . next ( ) ) {
var label = labels _1 _1 . value ;
2020-12-16 13:02:48 -05:00
_loop _1 ( label ) ;
2020-10-28 20:14:40 -04:00
}
}
catch ( e _1 _1 ) { e _1 = { error : e _1 _1 } ; }
finally {
try {
if ( labels _1 _1 && ! labels _1 _1 . done && ( _a = labels _1 . return ) ) _a . call ( labels _1 ) ;
}
finally { if ( e _1 ) throw e _1 . error ; }
}
2020-12-16 13:02:48 -05:00
if ( matches . length === 1 ) {
return matches [ 0 ] ;
}
if ( matches . length === 0 ) {
throw new InvalidTargetLabelError ( 'Unable to determine target for the PR as it has no target label.' ) ;
}
throw new InvalidTargetLabelError ( 'Unable to determine target for the PR as it has multiple target labels.' ) ;
2020-10-28 20:14:40 -04:00
}
/ * *
* Gets the branches from the specified target label .
*
* @ throws { InvalidTargetLabelError } Invalid label has been applied to pull request .
* @ throws { InvalidTargetBranchError } Invalid Github target branch has been selected .
* /
function getBranchesFromTargetLabel ( label , githubTargetBranch ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
var _a ;
return tslib . _ _generator ( this , function ( _b ) {
switch ( _b . label ) {
case 0 :
if ( ! ( typeof label . branches === 'function' ) ) return [ 3 /*break*/ , 2 ] ;
return [ 4 /*yield*/ , label . branches ( githubTargetBranch ) ] ;
case 1 :
_a = _b . sent ( ) ;
return [ 3 /*break*/ , 4 ] ;
case 2 : return [ 4 /*yield*/ , label . branches ] ;
case 3 :
_a = _b . sent ( ) ;
_b . label = 4 ;
case 4 : return [ 2 /*return*/ , _a ] ;
}
} ) ;
} ) ;
}
/ * *
* @ 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
* /
2020-11-30 17:53:05 -05:00
function getTargetBranchesForPr ( prNumber ) {
2020-10-28 20:14:40 -04:00
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
/** The ng-dev configuration. */
const config = getConfig ( ) ;
/** Repo owner and name for the github repository. */
const { owner , name : repo } = config . github ;
2021-04-08 15:34:55 -04:00
/** The singleton instance of the GitClient. */
const git = GitClient . getInstance ( ) ;
2020-10-28 20:14:40 -04:00
/** The validated merge config. */
const { config : mergeConfig , errors } = yield loadAndValidateConfig ( config , git . github ) ;
if ( errors !== undefined ) {
throw Error ( ` Invalid configuration found: ${ errors } ` ) ;
}
/** The current state of the pull request from Github. */
const prData = ( yield git . github . pulls . get ( { owner , repo , pull _number : prNumber } ) ) . data ;
/** The list of labels on the PR as strings. */
const labels = prData . labels . map ( l => l . name ) ;
/** The branch targetted via the Github UI. */
const githubTargetBranch = prData . base . ref ;
/** The active label which is being used for targetting the PR. */
2020-12-16 13:02:48 -05:00
let targetLabel ;
try {
targetLabel = getTargetLabelFromPullRequest ( mergeConfig , labels ) ;
}
catch ( e ) {
if ( e instanceof InvalidTargetLabelError ) {
error ( red ( e . failureMessage ) ) ;
process . exitCode = 1 ;
return ;
}
throw e ;
2020-10-28 20:14:40 -04:00
}
/** The target branches based on the target label and branch targetted in the Github UI. */
2020-11-30 17:53:05 -05:00
return yield getBranchesFromTargetLabel ( targetLabel , githubTargetBranch ) ;
} ) ;
}
function printTargetBranchesForPr ( prNumber ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const targets = yield getTargetBranchesForPr ( prNumber ) ;
if ( targets === undefined ) {
2020-10-28 20:14:40 -04:00
return ;
}
info . group ( ` PR # ${ prNumber } will merge into: ` ) ;
targets . forEach ( target => info ( ` - ${ target } ` ) ) ;
info . groupEnd ( ) ;
} ) ;
}
/ * *
* @ 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
* /
/** Builds the command. */
2021-03-19 13:01:11 -04:00
function builder$4 ( yargs ) {
2020-11-30 17:53:05 -05:00
return yargs . positional ( 'pr' , {
2020-10-28 20:14:40 -04:00
description : 'The pull request number' ,
type : 'number' ,
demandOption : true ,
} ) ;
}
/** Handles the command. */
2021-03-19 13:01:11 -04:00
function handler$4 ( { pr } ) {
2020-10-28 20:14:40 -04:00
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
2020-11-30 17:53:05 -05:00
yield printTargetBranchesForPr ( pr ) ;
2020-10-28 20:14:40 -04:00
} ) ;
}
/** yargs command module describing the command. */
const CheckTargetBranchesModule = {
2021-03-19 13:01:11 -04:00
handler : handler$4 ,
builder : builder$4 ,
2020-10-28 20:14:40 -04:00
command : 'check-target-branches <pr>' ,
describe : 'Check a PR to determine what branches it is currently targeting' ,
} ;
2020-10-01 19:06:56 -04:00
/ * *
* @ 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
* /
/** Get a PR from github */
function getPr ( prSchema , prNumber , git ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
var _a , owner , name , PR _QUERY , result ;
return tslib . _ _generator ( this , function ( _b ) {
switch ( _b . label ) {
case 0 :
_a = git . remoteConfig , owner = _a . owner , name = _a . name ;
PR _QUERY = typedGraphqlify . params ( {
$number : 'Int!' ,
$owner : 'String!' ,
2021-03-16 15:10:32 -04:00
$name : 'String!' , // The organization to query for
2020-10-01 19:06:56 -04:00
} , {
repository : typedGraphqlify . params ( { owner : '$owner' , name : '$name' } , {
pullRequest : typedGraphqlify . params ( { number : '$number' } , prSchema ) ,
} )
} ) ;
2021-04-08 15:34:55 -04:00
return [ 4 /*yield*/ , git . github . graphql ( PR _QUERY , { number : prNumber , owner : owner , name : name } ) ] ;
2020-10-01 19:06:56 -04:00
case 1 :
result = ( _b . sent ( ) ) ;
return [ 2 /*return*/ , result . repository . pullRequest ] ;
}
} ) ;
} ) ;
}
/** Get all pending PRs from github */
function getPendingPrs ( prSchema , git ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
var _a , owner , name , PRS _QUERY , cursor , hasNextPage , prs , params _1 , results ;
return tslib . _ _generator ( this , function ( _b ) {
switch ( _b . label ) {
case 0 :
_a = git . remoteConfig , owner = _a . owner , name = _a . name ;
PRS _QUERY = typedGraphqlify . params ( {
$first : 'Int' ,
$after : 'String' ,
$owner : 'String!' ,
2021-03-16 15:10:32 -04:00
$name : 'String!' , // The repository to query for
2020-10-01 19:06:56 -04:00
} , {
repository : typedGraphqlify . params ( { owner : '$owner' , name : '$name' } , {
pullRequests : typedGraphqlify . params ( {
first : '$first' ,
after : '$after' ,
states : "OPEN" ,
} , {
nodes : [ prSchema ] ,
pageInfo : {
hasNextPage : typedGraphqlify . types . boolean ,
endCursor : typedGraphqlify . types . string ,
} ,
} ) ,
} )
} ) ;
hasNextPage = true ;
prs = [ ] ;
_b . label = 1 ;
case 1 :
if ( ! hasNextPage ) return [ 3 /*break*/ , 3 ] ;
params _1 = {
after : cursor || null ,
first : 100 ,
owner : owner ,
name : name ,
} ;
2021-04-08 15:34:55 -04:00
return [ 4 /*yield*/ , git . github . graphql ( PRS _QUERY , params _1 ) ] ;
2020-10-01 19:06:56 -04:00
case 2 :
results = _b . sent ( ) ;
2021-03-16 15:10:32 -04:00
prs . push . apply ( prs , tslib . _ _spreadArray ( [ ] , tslib . _ _read ( results . repository . pullRequests . nodes ) ) ) ;
2020-10-01 19:06:56 -04:00
hasNextPage = results . repository . pullRequests . pageInfo . hasNextPage ;
cursor = results . repository . pullRequests . pageInfo . endCursor ;
return [ 3 /*break*/ , 1 ] ;
case 3 : return [ 2 /*return*/ , prs ] ;
}
} ) ;
} ) ;
}
/ * *
* @ 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
* /
2021-04-08 15:34:55 -04:00
/* Graphql schema for the response body for a pending PR. */
2020-10-01 19:06:56 -04:00
const PR _SCHEMA = {
state : typedGraphqlify . types . string ,
maintainerCanModify : typedGraphqlify . types . boolean ,
viewerDidAuthor : typedGraphqlify . types . boolean ,
headRefOid : typedGraphqlify . types . string ,
headRef : {
name : typedGraphqlify . types . string ,
repository : {
url : typedGraphqlify . types . string ,
nameWithOwner : typedGraphqlify . types . string ,
} ,
} ,
baseRef : {
name : typedGraphqlify . types . string ,
repository : {
url : typedGraphqlify . types . string ,
nameWithOwner : typedGraphqlify . types . string ,
} ,
} ,
} ;
class UnexpectedLocalChangesError extends Error {
constructor ( m ) {
super ( m ) ;
Object . setPrototypeOf ( this , UnexpectedLocalChangesError . prototype ) ;
}
}
class MaintainerModifyAccessError extends Error {
constructor ( m ) {
super ( m ) ;
Object . setPrototypeOf ( this , MaintainerModifyAccessError . prototype ) ;
}
}
/ * *
* Rebase the provided PR onto its merge target branch , and push up the resulting
* commit to the PRs repository .
* /
function checkOutPullRequestLocally ( prNumber , githubToken , opts = { } ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
2021-04-08 15:34:55 -04:00
/** The singleton instance of the GitClient. */
const git = GitClient . getAuthenticatedInstance ( ) ;
2020-10-01 19:06:56 -04:00
// 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 ( ) ) {
throw new UnexpectedLocalChangesError ( 'Unable to checkout PR due to uncommitted changes.' ) ;
}
/ * *
* The branch or revision originally checked out before this method performed
* any Git operations that may change the working branch .
* /
const previousBranchOrRevision = git . getCurrentBranchOrRevision ( ) ;
/* The PR information from Github. */
const pr = yield getPr ( PR _SCHEMA , prNumber , git ) ;
/** The branch name of the PR from the repository the PR came from. */
const headRefName = pr . headRef . name ;
/** 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 = 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
// never accidentally override upstream changes that have been pushed in the meanwhile.
// See:
// https://git-scm.com/docs/git-push#Documentation/git-push.txt---force-with-leaseltrefnamegtltexpectgt
/** Flag for a force push with leage back to upstream. */
const forceWithLeaseFlag = ` --force-with-lease= ${ headRefName } : ${ pr . headRefOid } ` ;
// If the PR does not allow maintainers to modify it, exit as the rebased PR cannot
// be pushed up.
if ( ! pr . maintainerCanModify && ! pr . viewerDidAuthor && ! opts . allowIfMaintainerCannotModify ) {
throw new MaintainerModifyAccessError ( 'PR is not set to allow maintainers to modify the PR' ) ;
}
try {
// Fetch the branch at the commit of the PR, and check it out in a detached state.
info ( ` Checking out PR # ${ prNumber } from ${ fullHeadRef } ` ) ;
2020-10-29 18:43:44 -04:00
git . run ( [ 'fetch' , '-q' , headRefUrl , headRefName ] ) ;
2020-10-01 19:06:56 -04:00
git . run ( [ 'checkout' , '--detach' , 'FETCH_HEAD' ] ) ;
}
catch ( e ) {
git . checkout ( previousBranchOrRevision , true ) ;
throw e ;
}
return {
/ * *
* Pushes the current local branch to the PR on the upstream repository .
*
* @ returns true If the command did not fail causing a GitCommandError to be thrown .
* @ throws GitCommandError Thrown when the push back to upstream fails .
* /
pushToUpstream : ( ) => {
git . run ( [ 'push' , headRefUrl , ` HEAD: ${ headRefName } ` , forceWithLeaseFlag ] ) ;
return true ;
} ,
/** Restores the state of the local repository to before the PR checkout occured. */
resetGitState : ( ) => {
return git . checkout ( previousBranchOrRevision , true ) ;
}
} ;
} ) ;
}
/ * *
* @ 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
* /
/** Builds the checkout pull request command. */
2021-03-19 13:01:11 -04:00
function builder$5 ( yargs ) {
2020-10-01 19:06:56 -04:00
return addGithubTokenOption ( yargs ) . positional ( 'prNumber' , { type : 'number' , demandOption : true } ) ;
}
/** Handles the checkout pull request command. */
2021-03-19 13:01:11 -04:00
function handler$5 ( { prNumber , githubToken } ) {
2020-10-01 19:06:56 -04:00
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const prCheckoutOptions = { allowIfMaintainerCannotModify : true , branchName : ` pr- ${ prNumber } ` } ;
yield checkOutPullRequestLocally ( prNumber , githubToken , prCheckoutOptions ) ;
} ) ;
}
/** yargs command module for checking out a PR */
const CheckoutCommandModule = {
2021-03-19 13:01:11 -04:00
handler : handler$5 ,
builder : builder$5 ,
2020-10-01 19:06:56 -04:00
command : 'checkout <pr-number>' ,
describe : 'Checkout a PR from the upstream repo' ,
} ;
2021-04-12 17:50:50 -04:00
/ * *
* @ 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
* /
/ * *
* Runs an given command as child process . By default , child process
* output will not be printed .
* /
function exec ( cmd , opts ) {
return shelljs . exec ( cmd , tslib . _ _assign ( tslib . _ _assign ( { silent : true } , opts ) , { async : false } ) ) ;
}
2020-10-01 19:06:56 -04:00
/ * *
* @ 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
* /
2021-04-08 15:34:55 -04:00
/* Graphql schema for the response body for each pending PR. */
2020-10-01 19:06:56 -04:00
const PR _SCHEMA$1 = {
headRef : {
name : typedGraphqlify . types . string ,
repository : {
url : typedGraphqlify . types . string ,
nameWithOwner : typedGraphqlify . types . string ,
} ,
} ,
baseRef : {
name : typedGraphqlify . types . string ,
repository : {
url : typedGraphqlify . types . string ,
nameWithOwner : typedGraphqlify . types . string ,
} ,
} ,
updatedAt : typedGraphqlify . types . string ,
number : typedGraphqlify . types . number ,
mergeable : typedGraphqlify . types . string ,
title : typedGraphqlify . types . string ,
} ;
/** Convert raw Pull Request response from Github to usable Pull Request object. */
function processPr ( pr ) {
return Object . assign ( Object . assign ( { } , pr ) , { updatedAt : ( new Date ( pr . updatedAt ) ) . getTime ( ) } ) ;
}
/** 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. */
2021-04-08 15:34:55 -04:00
function discoverNewConflictsForPr ( newPrNumber , updatedAfter ) {
2020-10-01 19:06:56 -04:00
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
2021-04-08 15:34:55 -04:00
/** The singleton instance of the GitClient. */
const git = GitClient . getAuthenticatedInstance ( ) ;
2020-10-01 19:06:56 -04:00
// 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 ( ) ) {
error ( 'Cannot run with local changes. Please make sure there are no local changes.' ) ;
process . exit ( 1 ) ;
}
/** The active github branch or revision before we performed any Git commands. */
const previousBranchOrRevision = git . getCurrentBranchOrRevision ( ) ;
/* Progress bar to indicate progress. */
const progressBar = new cliProgress . Bar ( { format : ` [{bar}] ETA: {eta}s | {value}/{total} ` } ) ;
/* PRs which were found to be conflicting. */
const conflicts = [ ] ;
info ( ` Requesting pending PRs from Github ` ) ;
/** List of PRs from github currently known as mergable. */
const allPendingPRs = ( yield getPendingPrs ( PR _SCHEMA$1 , git ) ) . map ( processPr ) ;
/** The PR which is being checked against. */
const requestedPr = allPendingPRs . find ( pr => pr . number === newPrNumber ) ;
if ( requestedPr === undefined ) {
error ( ` The request PR, # ${ newPrNumber } was not found as a pending PR on github, please confirm ` ) ;
error ( ` the PR number is correct and is an open PR ` ) ;
process . exit ( 1 ) ;
}
const pendingPrs = allPendingPRs . filter ( pr => {
return (
// PRs being merged into the same target branch as the requested PR
pr . baseRef . name === requestedPr . baseRef . name &&
// PRs which either have not been processed or are determined as mergable by Github
pr . mergeable !== 'CONFLICTING' &&
// PRs updated after the provided date
pr . updatedAt >= updatedAfter ) ;
} ) ;
info ( ` Retrieved ${ allPendingPRs . length } total pending PRs ` ) ;
info ( ` Checking ${ pendingPrs . length } PRs for conflicts after a merge of # ${ newPrNumber } ` ) ;
// Fetch and checkout the PR being checked.
exec ( ` git fetch ${ requestedPr . headRef . repository . url } ${ requestedPr . headRef . name } ` ) ;
exec ( ` git checkout -B ${ tempWorkingBranch } FETCH_HEAD ` ) ;
// Rebase the PR against the PRs target branch.
exec ( ` git fetch ${ requestedPr . baseRef . repository . url } ${ requestedPr . baseRef . name } ` ) ;
const result = exec ( ` git rebase FETCH_HEAD ` ) ;
if ( result . code ) {
error ( 'The requested PR currently has conflicts' ) ;
cleanUpGitState ( previousBranchOrRevision ) ;
process . exit ( 1 ) ;
}
// Start the progress bar
progressBar . start ( pendingPrs . length , 0 ) ;
// Check each PR to determine if it can merge cleanly into the repo after the target PR.
for ( const pr of pendingPrs ) {
// Fetch and checkout the next PR
exec ( ` git fetch ${ pr . headRef . repository . url } ${ pr . headRef . name } ` ) ;
exec ( ` git checkout --detach FETCH_HEAD ` ) ;
// Check if the PR cleanly rebases into the repo after the target PR.
const result = exec ( ` git rebase ${ tempWorkingBranch } ` ) ;
if ( result . code !== 0 ) {
conflicts . push ( pr ) ;
}
// Abort any outstanding rebase attempt.
exec ( ` git rebase --abort ` ) ;
progressBar . increment ( 1 ) ;
}
// End the progress bar as all PRs have been processed.
progressBar . stop ( ) ;
info ( ) ;
info ( ` Result: ` ) ;
cleanUpGitState ( previousBranchOrRevision ) ;
// If no conflicts are found, exit successfully.
if ( conflicts . length === 0 ) {
info ( ` No new conflicting PRs found after # ${ newPrNumber } merging ` ) ;
process . exit ( 0 ) ;
}
// Inform about discovered conflicts, exit with failure.
error . group ( ` ${ conflicts . length } PR(s) which conflict(s) after # ${ newPrNumber } merges: ` ) ;
for ( const pr of conflicts ) {
error ( ` - # ${ pr . number } : ${ pr . title } ` ) ;
}
error . groupEnd ( ) ;
process . exit ( 1 ) ;
} ) ;
}
/** Reset git back to the provided branch or revision. */
function cleanUpGitState ( previousBranchOrRevision ) {
// Ensure that any outstanding rebases are aborted.
exec ( ` git rebase --abort ` ) ;
// Ensure that any changes in the current repo state are cleared.
exec ( ` git reset --hard ` ) ;
// Checkout the original branch from before the run began.
exec ( ` git checkout ${ previousBranchOrRevision } ` ) ;
// Delete the generated branch.
exec ( ` git branch -D ${ tempWorkingBranch } ` ) ;
}
/ * *
* @ 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
* /
/** Builds the discover-new-conflicts pull request command. */
function buildDiscoverNewConflictsCommand ( yargs ) {
2021-04-26 18:42:10 -04:00
return addGithubTokenOption ( yargs )
2020-10-01 19:06:56 -04:00
. option ( 'date' , {
description : 'Only consider PRs updated since provided date' ,
defaultDescription : '30 days ago' ,
coerce : ( date ) => typeof date === 'number' ? date : Date . parse ( date ) ,
default : getThirtyDaysAgoDate ( ) ,
} )
. positional ( 'pr-number' , { demandOption : true , type : 'number' } ) ;
}
/** Handles the discover-new-conflicts pull request command. */
function handleDiscoverNewConflictsCommand ( { 'pr-number' : prNumber , date } ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
// If a provided date is not able to be parsed, yargs provides it as NaN.
if ( isNaN ( date ) ) {
error ( 'Unable to parse the value provided via --date flag' ) ;
process . exit ( 1 ) ;
}
yield discoverNewConflictsForPr ( prNumber , date ) ;
} ) ;
}
/** Gets a date object 30 days ago from today. */
function getThirtyDaysAgoDate ( ) {
const date = new Date ( ) ;
// Set the hours, minutes and seconds to 0 to only consider date.
date . setHours ( 0 , 0 , 0 , 0 ) ;
// Set the date to 30 days in the past.
date . setDate ( date . getDate ( ) - 30 ) ;
return date . getTime ( ) ;
}
/ * *
* @ 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
* /
/ * *
* Class that can be used to describe pull request failures . A failure
* is described through a human - readable message and a flag indicating
* whether it is non - fatal or not .
* /
var PullRequestFailure = /** @class */ ( function ( ) {
function PullRequestFailure (
/** Human-readable message for the failure */
2020-10-28 13:47:22 -04:00
message ,
2020-10-01 19:06:56 -04:00
/** Whether the failure is non-fatal and can be forcibly ignored. */
nonFatal ) {
if ( nonFatal === void 0 ) { nonFatal = false ; }
this . message = message ;
this . nonFatal = nonFatal ;
}
PullRequestFailure . claUnsigned = function ( ) {
return new this ( "CLA has not been signed. Please make sure the PR author has signed the CLA." ) ;
} ;
PullRequestFailure . failingCiJobs = function ( ) {
return new this ( "Failing CI jobs." , true ) ;
} ;
PullRequestFailure . pendingCiJobs = function ( ) {
return new this ( "Pending CI jobs." , true ) ;
} ;
PullRequestFailure . notMergeReady = function ( ) {
return new this ( "Not marked as merge ready." ) ;
} ;
2021-04-13 17:12:32 -04:00
PullRequestFailure . isDraft = function ( ) {
return new this ( 'Pull request is still in draft.' ) ;
} ;
PullRequestFailure . isClosed = function ( ) {
return new this ( 'Pull request is already closed.' ) ;
} ;
PullRequestFailure . isMerged = function ( ) {
return new this ( 'Pull request is already merged.' ) ;
} ;
2020-10-01 19:06:56 -04:00
PullRequestFailure . mismatchingTargetBranch = function ( allowedBranches ) {
return new this ( "Pull request is set to wrong base branch. Please update the PR in the Github UI " +
( "to one of the following branches: " + allowedBranches . join ( ', ' ) + "." ) ) ;
} ;
PullRequestFailure . unsatisfiedBaseSha = function ( ) {
return new this ( "Pull request has not been rebased recently and could be bypassing CI checks. " +
"Please rebase the PR." ) ;
} ;
PullRequestFailure . mergeConflicts = function ( failedBranches ) {
return new this ( "Could not merge pull request into the following branches due to merge " +
( "conflicts: " + failedBranches . join ( ', ' ) + ". Please rebase the PR or update the target label." ) ) ;
} ;
PullRequestFailure . unknownMergeError = function ( ) {
return new this ( "Unknown merge error occurred. Please see console output above for debugging." ) ;
} ;
PullRequestFailure . unableToFixupCommitMessageSquashOnly = function ( ) {
return new this ( "Unable to fixup commit message of pull request. Commit message can only be " +
"modified if the PR is merged using squash." ) ;
} ;
PullRequestFailure . notFound = function ( ) {
return new this ( "Pull request could not be found upstream." ) ;
} ;
PullRequestFailure . insufficientPermissionsToMerge = function ( message ) {
if ( message === void 0 ) { message = "Insufficient Github API permissions to merge pull request. Please ensure that " +
"your auth token has write access." ; }
return new this ( message ) ;
} ;
2021-04-06 10:49:49 -04:00
PullRequestFailure . hasBreakingChanges = function ( label ) {
var message = "Cannot merge into branch for \"" + label . pattern + "\" as the pull request has " +
2021-04-06 11:18:44 -04:00
"breaking changes. Breaking changes can only be merged with the \"target: major\" label." ;
2021-04-06 10:49:49 -04:00
return new this ( message ) ;
} ;
PullRequestFailure . hasFeatureCommits = function ( label ) {
var message = "Cannot merge into branch for \"" + label . pattern + "\" as the pull request has " +
2021-04-06 11:18:44 -04:00
'commits with the "feat" type. New features can only be merged with the "target: minor" ' +
'or "target: major" label.' ;
2021-04-06 10:49:49 -04:00
return new this ( message ) ;
} ;
2021-04-06 11:57:32 -04:00
PullRequestFailure . missingBreakingChangeLabel = function ( ) {
var message = 'Pull Request has at least one commit containing a breaking change note, but ' +
'does not have a breaking change label.' ;
return new this ( message ) ;
} ;
PullRequestFailure . missingBreakingChangeCommit = function ( ) {
var message = 'Pull Request has a breaking change label, but does not contain any commits ' +
'with breaking change notes.' ;
return new this ( message ) ;
} ;
2020-10-01 19:06:56 -04:00
return PullRequestFailure ;
} ( ) ) ;
/ * *
* @ 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
* /
function getCaretakerNotePromptMessage ( pullRequest ) {
return red ( 'Pull request has a caretaker note applied. Please make sure you read it.' ) +
( "\nQuick link to PR: " + pullRequest . url + "\nDo you want to proceed merging?" ) ;
}
function getTargettedBranchesConfirmationPromptMessage ( pullRequest ) {
var targetBranchListAsString = pullRequest . targetBranches . map ( function ( b ) { return " - " + b + "\n" ; } ) . join ( '' ) ;
return "Pull request #" + pullRequest . prNumber + " will merge into:\n" + targetBranchListAsString + "\nDo you want to proceed merging?" ;
}
/ * *
* @ 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
* /
2021-04-06 11:57:32 -04:00
/** The default label for labeling pull requests containing a breaking change. */
var BreakingChangeLabel = 'breaking changes' ;
2020-10-01 19:06:56 -04:00
/ * *
* Loads and validates the specified pull request against the given configuration .
* If the pull requests fails , a pull request failure is returned .
* /
function loadAndValidatePullRequest ( _a , prNumber , ignoreNonFatalFailures ) {
var git = _a . git , config = _a . config ;
if ( ignoreNonFatalFailures === void 0 ) { ignoreNonFatalFailures = false ; }
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
2021-04-06 11:57:32 -04:00
var prData , labels , targetLabel , commitsInPr , state , githubTargetBranch , requiredBaseSha , needsCommitMessageFixup , hasCaretakerNote , targetBranches , error _1 ;
2020-10-01 19:06:56 -04:00
return tslib . _ _generator ( this , function ( _b ) {
switch ( _b . label ) {
case 0 : return [ 4 /*yield*/ , fetchPullRequestFromGithub ( git , prNumber ) ] ;
case 1 :
prData = _b . sent ( ) ;
if ( prData === null ) {
return [ 2 /*return*/ , PullRequestFailure . notFound ( ) ] ;
}
2021-04-06 10:43:21 -04:00
labels = prData . labels . nodes . map ( function ( l ) { return l . name ; } ) ;
2020-10-01 19:06:56 -04:00
if ( ! labels . some ( function ( name ) { return matchesPattern ( name , config . mergeReadyLabel ) ; } ) ) {
return [ 2 /*return*/ , PullRequestFailure . notMergeReady ( ) ] ;
}
if ( ! labels . some ( function ( name ) { return matchesPattern ( name , config . claSignedLabel ) ; } ) ) {
return [ 2 /*return*/ , PullRequestFailure . claUnsigned ( ) ] ;
}
2020-12-16 13:02:48 -05:00
try {
targetLabel = getTargetLabelFromPullRequest ( config , labels ) ;
}
catch ( error ) {
if ( error instanceof InvalidTargetLabelError ) {
return [ 2 /*return*/ , new PullRequestFailure ( error . failureMessage ) ] ;
}
throw error ;
2020-10-01 19:06:56 -04:00
}
2021-04-06 11:57:32 -04:00
commitsInPr = prData . commits . nodes . map ( function ( n ) { return parseCommitMessage ( n . commit . message ) ; } ) ;
2021-04-06 10:49:49 -04:00
try {
2021-04-13 17:12:32 -04:00
assertPendingState ( prData ) ;
2021-04-06 11:57:32 -04:00
assertChangesAllowForTargetLabel ( commitsInPr , targetLabel , config ) ;
assertCorrectBreakingChangeLabeling ( commitsInPr , labels , config ) ;
2021-04-06 10:49:49 -04:00
}
catch ( error ) {
return [ 2 /*return*/ , error ] ;
}
2021-04-06 10:43:21 -04:00
state = prData . commits . nodes . slice ( - 1 ) [ 0 ] . commit . status . state ;
if ( state === 'FAILURE' && ! ignoreNonFatalFailures ) {
2020-10-01 19:06:56 -04:00
return [ 2 /*return*/ , PullRequestFailure . failingCiJobs ( ) ] ;
}
2021-04-06 10:43:21 -04:00
if ( state === 'PENDING' && ! ignoreNonFatalFailures ) {
2020-10-01 19:06:56 -04:00
return [ 2 /*return*/ , PullRequestFailure . pendingCiJobs ( ) ] ;
}
2021-04-08 17:22:37 -04:00
githubTargetBranch = prData . baseRefName ;
2020-10-01 19:06:56 -04:00
requiredBaseSha = config . requiredBaseCommits && config . requiredBaseCommits [ githubTargetBranch ] ;
needsCommitMessageFixup = ! ! config . commitMessageFixupLabel &&
labels . some ( function ( name ) { return matchesPattern ( name , config . commitMessageFixupLabel ) ; } ) ;
hasCaretakerNote = ! ! config . caretakerNoteLabel &&
labels . some ( function ( name ) { return matchesPattern ( name , config . caretakerNoteLabel ) ; } ) ;
2021-04-06 10:43:21 -04:00
_b . label = 2 ;
case 2 :
_b . trys . push ( [ 2 , 4 , , 5 ] ) ;
2020-10-01 19:06:56 -04:00
return [ 4 /*yield*/ , getBranchesFromTargetLabel ( targetLabel , githubTargetBranch ) ] ;
2021-04-06 10:43:21 -04:00
case 3 :
2020-10-01 19:06:56 -04:00
targetBranches = _b . sent ( ) ;
2021-04-06 10:43:21 -04:00
return [ 3 /*break*/ , 5 ] ;
case 4 :
2020-10-01 19:06:56 -04:00
error _1 = _b . sent ( ) ;
if ( error _1 instanceof InvalidTargetBranchError || error _1 instanceof InvalidTargetLabelError ) {
return [ 2 /*return*/ , new PullRequestFailure ( error _1 . failureMessage ) ] ;
}
throw error _1 ;
2021-04-06 10:43:21 -04:00
case 5 : return [ 2 /*return*/ , {
url : prData . url ,
2020-10-01 19:06:56 -04:00
prNumber : prNumber ,
labels : labels ,
requiredBaseSha : requiredBaseSha ,
githubTargetBranch : githubTargetBranch ,
needsCommitMessageFixup : needsCommitMessageFixup ,
hasCaretakerNote : hasCaretakerNote ,
targetBranches : targetBranches ,
title : prData . title ,
2021-04-06 11:18:44 -04:00
commitCount : prData . commits . totalCount ,
2020-10-01 19:06:56 -04:00
} ] ;
}
} ) ;
} ) ;
}
2021-04-08 15:34:55 -04:00
/* Graphql schema for the response body the requested pull request. */
2021-04-06 10:43:21 -04:00
var PR _SCHEMA$2 = {
url : typedGraphqlify . types . string ,
2021-04-13 17:12:32 -04:00
isDraft : typedGraphqlify . types . boolean ,
state : typedGraphqlify . types . oneOf ( [ 'OPEN' , 'MERGED' , 'CLOSED' ] ) ,
2021-04-06 10:43:21 -04:00
number : typedGraphqlify . types . number ,
2021-04-06 11:18:44 -04:00
// 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 : typedGraphqlify . params ( { last : 100 } , {
totalCount : typedGraphqlify . types . number ,
2021-04-06 10:43:21 -04:00
nodes : [ {
commit : {
status : {
state : typedGraphqlify . types . oneOf ( [ 'FAILURE' , 'PENDING' , 'SUCCESS' ] ) ,
} ,
message : typedGraphqlify . types . string ,
} ,
} ] ,
} ) ,
2021-04-08 17:22:37 -04:00
baseRefName : typedGraphqlify . types . string ,
2021-04-06 10:43:21 -04:00
title : typedGraphqlify . types . string ,
labels : typedGraphqlify . params ( { first : 100 } , {
nodes : [ {
name : typedGraphqlify . types . string ,
} ]
} ) ,
} ;
2020-10-01 19:06:56 -04:00
/** Fetches a pull request from Github. Returns null if an error occurred. */
function fetchPullRequestFromGithub ( git , prNumber ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
2021-04-06 11:18:44 -04:00
var x , e _1 ;
2020-10-01 19:06:56 -04:00
return tslib . _ _generator ( this , function ( _a ) {
switch ( _a . label ) {
case 0 :
_a . trys . push ( [ 0 , 2 , , 3 ] ) ;
2021-04-06 10:43:21 -04:00
return [ 4 /*yield*/ , getPr ( PR _SCHEMA$2 , prNumber , git ) ] ;
2021-04-06 11:18:44 -04:00
case 1 :
x = _a . sent ( ) ;
return [ 2 /*return*/ , x ] ;
2020-10-01 19:06:56 -04:00
case 2 :
e _1 = _a . sent ( ) ;
// If the pull request could not be found, we want to return `null` so
// that the error can be handled gracefully.
if ( e _1 . status === 404 ) {
return [ 2 /*return*/ , null ] ;
}
throw e _1 ;
case 3 : return [ 2 /*return*/ ] ;
}
} ) ;
} ) ;
}
/** Whether the specified value resolves to a pull request. */
function isPullRequest ( v ) {
return v . targetBranches !== undefined ;
}
2021-04-06 10:49:49 -04:00
/ * *
* Assert the commits provided are allowed to merge to the provided target label , throwing a
* PullRequestFailure otherwise .
* /
2021-04-06 11:57:32 -04:00
function assertChangesAllowForTargetLabel ( commits , label , config ) {
2021-04-06 11:18:44 -04:00
/ * *
* List of commit scopes which are exempted from target label content requirements . i . e . no ` feat `
* scopes in patch branches , no breaking changes in minor or patch changes .
* /
var exemptedScopes = config . targetLabelExemptScopes || [ ] ;
2021-04-06 11:57:32 -04:00
/** List of commits which are subject to content requirements for the target label. */
commits = commits . filter ( function ( commit ) { return ! exemptedScopes . includes ( commit . scope ) ; } ) ;
2021-04-16 11:40:54 -04:00
var hasBreakingChanges = commits . some ( function ( commit ) { return commit . breakingChanges . length !== 0 ; } ) ;
var hasFeatureCommits = commits . some ( function ( commit ) { return commit . type === 'feat' ; } ) ;
2021-04-06 10:49:49 -04:00
switch ( label . pattern ) {
case 'target: major' :
break ;
case 'target: minor' :
2021-04-16 11:40:54 -04:00
if ( hasBreakingChanges ) {
2021-04-06 10:49:49 -04:00
throw PullRequestFailure . hasBreakingChanges ( label ) ;
}
break ;
2021-04-16 11:40:54 -04:00
case 'target: rc' :
2021-04-06 10:49:49 -04:00
case 'target: patch' :
case 'target: lts' :
2021-04-16 11:40:54 -04:00
if ( hasBreakingChanges ) {
2021-04-06 10:49:49 -04:00
throw PullRequestFailure . hasBreakingChanges ( label ) ;
}
2021-04-16 11:40:54 -04:00
if ( hasFeatureCommits ) {
2021-04-06 10:49:49 -04:00
throw PullRequestFailure . hasFeatureCommits ( label ) ;
}
break ;
default :
2021-04-06 11:18:44 -04:00
warn ( red ( 'WARNING: Unable to confirm all commits in the pull request are eligible to be' ) ) ;
warn ( red ( "merged into the target branch: " + label . pattern ) ) ;
2021-04-06 10:49:49 -04:00
break ;
}
}
2021-04-06 11:57:32 -04:00
/ * *
* Assert the pull request has the proper label for breaking changes if there are breaking change
* commits , and only has the label if there are breaking change commits .
* /
function assertCorrectBreakingChangeLabeling ( commits , labels , config ) {
/** Whether the PR has a label noting a breaking change. */
var hasLabel = labels . includes ( config . breakingChangeLabel || BreakingChangeLabel ) ;
//** Whether the PR has at least one commit which notes a breaking change. */
var hasCommit = commits . some ( function ( commit ) { return commit . breakingChanges . length !== 0 ; } ) ;
if ( ! hasLabel && hasCommit ) {
throw PullRequestFailure . missingBreakingChangeLabel ( ) ;
}
if ( hasLabel && ! hasCommit ) {
throw PullRequestFailure . missingBreakingChangeCommit ( ) ;
}
}
2021-04-13 17:12:32 -04:00
/** Assert the pull request is pending, not closed, merged or in draft. */
function assertPendingState ( pr ) {
if ( pr . isDraft ) {
throw PullRequestFailure . isDraft ( ) ;
}
switch ( pr . state ) {
case 'CLOSED' :
throw PullRequestFailure . isClosed ( ) ;
case 'MERGED' :
throw PullRequestFailure . isMerged ( ) ;
}
}
2020-10-01 19:06:56 -04:00
/ * *
* @ 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
* /
/ * *
* Name of a temporary branch that contains the head of a currently - processed PR . Note
* that a branch name should be used that most likely does not conflict with other local
* development branches .
* /
var TEMP _PR _HEAD _BRANCH = 'merge_pr_head' ;
/ * *
* Base class for merge strategies . A merge strategy accepts a pull request and
* merges it into the determined target branches .
* /
var MergeStrategy = /** @class */ ( function ( ) {
function MergeStrategy ( git ) {
this . git = git ;
}
/ * *
* Prepares a merge of the given pull request . The strategy by default will
* fetch all target branches and the pull request into local temporary branches .
* /
MergeStrategy . prototype . prepare = function ( pullRequest ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
return tslib . _ _generator ( this , function ( _a ) {
this . fetchTargetBranches ( pullRequest . targetBranches , "pull/" + pullRequest . prNumber + "/head:" + TEMP _PR _HEAD _BRANCH ) ;
return [ 2 /*return*/ ] ;
} ) ;
} ) ;
} ;
/** Cleans up the pull request merge. e.g. deleting temporary local branches. */
MergeStrategy . prototype . cleanup = function ( pullRequest ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
var _this = this ;
return tslib . _ _generator ( this , function ( _a ) {
// Delete all temporary target branches.
pullRequest . targetBranches . forEach ( function ( branchName ) { return _this . git . run ( [ 'branch' , '-D' , _this . getLocalTargetBranchName ( branchName ) ] ) ; } ) ;
// Delete temporary branch for the pull request head.
this . git . run ( [ 'branch' , '-D' , TEMP _PR _HEAD _BRANCH ] ) ;
return [ 2 /*return*/ ] ;
} ) ;
} ) ;
} ;
/** Gets the revision range for all commits in the given pull request. */
MergeStrategy . prototype . getPullRequestRevisionRange = function ( pullRequest ) {
return this . getPullRequestBaseRevision ( pullRequest ) + ".." + TEMP _PR _HEAD _BRANCH ;
} ;
/** Gets the base revision of a pull request. i.e. the commit the PR is based on. */
MergeStrategy . prototype . getPullRequestBaseRevision = function ( pullRequest ) {
return TEMP _PR _HEAD _BRANCH + "~" + pullRequest . commitCount ;
} ;
/** Gets a deterministic local branch name for a given branch. */
MergeStrategy . prototype . getLocalTargetBranchName = function ( targetBranch ) {
return "merge_pr_target_" + targetBranch . replace ( /\//g , '_' ) ;
} ;
/ * *
* Cherry - picks the given revision range into the specified target branches .
* @ returns A list of branches for which the revisions could not be cherry - picked into .
* /
MergeStrategy . prototype . cherryPickIntoTargetBranches = function ( revisionRange , targetBranches , options ) {
var e _1 , _a ;
if ( options === void 0 ) { options = { } ; }
var cherryPickArgs = [ revisionRange ] ;
var failedBranches = [ ] ;
if ( options . dryRun ) {
// https://git-scm.com/docs/git-cherry-pick#Documentation/git-cherry-pick.txt---no-commit
// This causes `git cherry-pick` to not generate any commits. Instead, the changes are
// applied directly in the working tree. This allow us to easily discard the changes
// for dry-run purposes.
cherryPickArgs . push ( '--no-commit' ) ;
}
if ( options . linkToOriginalCommits ) {
// We add `-x` when cherry-picking as that will allow us to easily jump to original
// commits for cherry-picked commits. With that flag set, Git will automatically append
// the original SHA/revision to the commit message. e.g. `(cherry picked from commit <..>)`.
// https://git-scm.com/docs/git-cherry-pick#Documentation/git-cherry-pick.txt--x.
cherryPickArgs . push ( '-x' ) ;
}
try {
// Cherry-pick the refspec into all determined target branches.
for ( var targetBranches _1 = tslib . _ _values ( targetBranches ) , targetBranches _1 _1 = targetBranches _1 . next ( ) ; ! targetBranches _1 _1 . done ; targetBranches _1 _1 = targetBranches _1 . next ( ) ) {
var branchName = targetBranches _1 _1 . value ;
var localTargetBranch = this . getLocalTargetBranchName ( branchName ) ;
// Checkout the local target branch.
this . git . run ( [ 'checkout' , localTargetBranch ] ) ;
// Cherry-pick the refspec into the target branch.
2021-03-16 15:10:32 -04:00
if ( this . git . runGraceful ( tslib . _ _spreadArray ( [ 'cherry-pick' ] , tslib . _ _read ( cherryPickArgs ) ) ) . status !== 0 ) {
2020-10-01 19:06:56 -04:00
// Abort the failed cherry-pick. We do this because Git persists the failed
// cherry-pick state globally in the repository. This could prevent future
// pull request merges as a Git thinks a cherry-pick is still in progress.
this . git . runGraceful ( [ 'cherry-pick' , '--abort' ] ) ;
failedBranches . push ( branchName ) ;
}
// If we run with dry run mode, we reset the local target branch so that all dry-run
// cherry-pick changes are discard. Changes are applied to the working tree and index.
if ( options . dryRun ) {
this . git . run ( [ 'reset' , '--hard' , 'HEAD' ] ) ;
}
}
}
catch ( e _1 _1 ) { e _1 = { error : e _1 _1 } ; }
finally {
try {
if ( targetBranches _1 _1 && ! targetBranches _1 _1 . done && ( _a = targetBranches _1 . return ) ) _a . call ( targetBranches _1 ) ;
}
finally { if ( e _1 ) throw e _1 . error ; }
}
return failedBranches ;
} ;
/ * *
* Fetches the given target branches . Also accepts a list of additional refspecs that
* should be fetched . This is helpful as multiple slow fetches could be avoided .
* /
MergeStrategy . prototype . fetchTargetBranches = function ( names ) {
var _this = this ;
var extraRefspecs = [ ] ;
for ( var _i = 1 ; _i < arguments . length ; _i ++ ) {
extraRefspecs [ _i - 1 ] = arguments [ _i ] ;
}
var fetchRefspecs = names . map ( function ( targetBranch ) {
var localTargetBranch = _this . getLocalTargetBranchName ( targetBranch ) ;
return "refs/heads/" + targetBranch + ":" + localTargetBranch ;
} ) ;
// Fetch all target branches with a single command. We don't want to fetch them
// individually as that could cause an unnecessary slow-down.
2021-04-08 15:34:55 -04:00
this . git . run ( tslib . _ _spreadArray ( tslib . _ _spreadArray ( [ 'fetch' , '-q' , '-f' , this . git . getRepoGitUrl ( ) ] , tslib . _ _read ( fetchRefspecs ) ) , tslib . _ _read ( extraRefspecs ) ) ) ;
2020-10-01 19:06:56 -04:00
} ;
/** Pushes the given target branches upstream. */
MergeStrategy . prototype . pushTargetBranchesUpstream = function ( names ) {
var _this = this ;
var pushRefspecs = names . map ( function ( targetBranch ) {
var localTargetBranch = _this . getLocalTargetBranchName ( targetBranch ) ;
return localTargetBranch + ":refs/heads/" + targetBranch ;
} ) ;
// 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.
2021-04-08 15:34:55 -04:00
this . git . run ( tslib . _ _spreadArray ( [ 'push' , this . git . getRepoGitUrl ( ) ] , tslib . _ _read ( pushRefspecs ) ) ) ;
2020-10-01 19:06:56 -04:00
} ;
return MergeStrategy ;
} ( ) ) ;
/ * *
* @ 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
* /
/** Separator between commit message header and body. */
var COMMIT _HEADER _SEPARATOR = '\n\n' ;
/ * *
* Merge strategy that primarily leverages the Github API . The strategy merges a given
* pull request into a target branch using the API . This ensures that Github displays
* the pull request as merged . The merged commits are then cherry - picked into the remaining
* target branches using the local Git instance . The benefit is that the Github merged state
* is properly set , but a notable downside is that PRs cannot use fixup or squash commits .
* /
var GithubApiMergeStrategy = /** @class */ ( function ( _super ) {
tslib . _ _extends ( GithubApiMergeStrategy , _super ) ;
function GithubApiMergeStrategy ( git , _config ) {
var _this = _super . call ( this , git ) || this ;
_this . _config = _config ;
return _this ;
}
GithubApiMergeStrategy . prototype . merge = function ( pullRequest ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
var githubTargetBranch , prNumber , targetBranches , requiredBaseSha , needsCommitMessageFixup , method , cherryPickTargetBranches , failure , mergeOptions , mergeStatusCode , targetSha , result , e _1 , targetCommitsCount , failedBranches ;
return tslib . _ _generator ( this , function ( _a ) {
switch ( _a . label ) {
case 0 :
githubTargetBranch = pullRequest . githubTargetBranch , prNumber = pullRequest . prNumber , targetBranches = pullRequest . targetBranches , requiredBaseSha = pullRequest . requiredBaseSha , needsCommitMessageFixup = pullRequest . needsCommitMessageFixup ;
// If the pull request does not have its base branch set to any determined target
// branch, we cannot merge using the API.
if ( targetBranches . every ( function ( t ) { return t !== githubTargetBranch ; } ) ) {
return [ 2 /*return*/ , PullRequestFailure . mismatchingTargetBranch ( targetBranches ) ] ;
}
// In cases where a required base commit is specified for this pull request, check if
// the pull request contains the given commit. If not, return a pull request failure.
// This check is useful for enforcing that PRs are rebased on top of a given commit.
// e.g. a commit that changes the code ownership validation. PRs which are not rebased
// could bypass new codeowner ship rules.
if ( requiredBaseSha && ! this . git . hasCommit ( TEMP _PR _HEAD _BRANCH , requiredBaseSha ) ) {
return [ 2 /*return*/ , PullRequestFailure . unsatisfiedBaseSha ( ) ] ;
}
method = this . _getMergeActionFromPullRequest ( pullRequest ) ;
cherryPickTargetBranches = targetBranches . filter ( function ( b ) { return b !== githubTargetBranch ; } ) ;
return [ 4 /*yield*/ , this . _checkMergability ( pullRequest , cherryPickTargetBranches ) ] ;
case 1 :
failure = _a . sent ( ) ;
// If the PR could not be cherry-picked into all target branches locally, we know it can't
// be done through the Github API either. We abort merging and pass-through the failure.
if ( failure !== null ) {
return [ 2 /*return*/ , failure ] ;
}
mergeOptions = tslib . _ _assign ( { pull _number : prNumber , merge _method : method } , this . git . remoteParams ) ;
if ( ! needsCommitMessageFixup ) return [ 3 /*break*/ , 3 ] ;
// Commit message fixup does not work with other merge methods as the Github API only
// allows commit message modifications for squash merging.
if ( method !== 'squash' ) {
return [ 2 /*return*/ , PullRequestFailure . unableToFixupCommitMessageSquashOnly ( ) ] ;
}
return [ 4 /*yield*/ , this . _promptCommitMessageEdit ( pullRequest , mergeOptions ) ] ;
case 2 :
_a . sent ( ) ;
_a . label = 3 ;
case 3 :
_a . trys . push ( [ 3 , 5 , , 6 ] ) ;
return [ 4 /*yield*/ , this . git . github . pulls . merge ( mergeOptions ) ] ;
case 4 :
result = _a . sent ( ) ;
mergeStatusCode = result . status ;
targetSha = result . data . sha ;
return [ 3 /*break*/ , 6 ] ;
case 5 :
e _1 = _a . sent ( ) ;
// Note: Github usually returns `404` as status code if the API request uses a
// token with insufficient permissions. Github does this because it doesn't want
// to leak whether a repository exists or not. In our case we expect a certain
// repository to exist, so we always treat this as a permission failure.
if ( e _1 . status === 403 || e _1 . status === 404 ) {
return [ 2 /*return*/ , PullRequestFailure . insufficientPermissionsToMerge ( ) ] ;
}
throw e _1 ;
case 6 :
// https://developer.github.com/v3/pulls/#response-if-merge-cannot-be-performed
// Pull request cannot be merged due to merge conflicts.
if ( mergeStatusCode === 405 ) {
return [ 2 /*return*/ , PullRequestFailure . mergeConflicts ( [ githubTargetBranch ] ) ] ;
}
if ( mergeStatusCode !== 200 ) {
return [ 2 /*return*/ , PullRequestFailure . unknownMergeError ( ) ] ;
}
// If the PR does not need to be merged into any other target branches,
// we exit here as we already completed the merge.
if ( ! cherryPickTargetBranches . length ) {
return [ 2 /*return*/ , null ] ;
}
// Refresh the target branch the PR has been merged into through the API. We need
// to re-fetch as otherwise we cannot cherry-pick the new commits into the remaining
// target branches.
this . fetchTargetBranches ( [ githubTargetBranch ] ) ;
targetCommitsCount = method === 'squash' ? 1 : pullRequest . commitCount ;
return [ 4 /*yield*/ , this . cherryPickIntoTargetBranches ( targetSha + "~" + targetCommitsCount + ".." + targetSha , cherryPickTargetBranches , {
// Commits that have been created by the Github API do not necessarily contain
// a reference to the source pull request (unless the squash strategy is used).
// To ensure that original commits can be found when a commit is viewed in a
// target branch, we add a link to the original commits when cherry-picking.
linkToOriginalCommits : true ,
} ) ] ;
case 7 :
failedBranches = _a . sent ( ) ;
// We already checked whether the PR can be cherry-picked into the target branches,
// but in case the cherry-pick somehow fails, we still handle the conflicts here. The
// commits created through the Github API could be different (i.e. through squash).
if ( failedBranches . length ) {
return [ 2 /*return*/ , PullRequestFailure . mergeConflicts ( failedBranches ) ] ;
}
this . pushTargetBranchesUpstream ( cherryPickTargetBranches ) ;
return [ 2 /*return*/ , null ] ;
}
} ) ;
} ) ;
} ;
/ * *
* Prompts the user for the commit message changes . Unlike as in the autosquash merge
* strategy , we cannot start an interactive rebase because we merge using the Github API .
* The Github API only allows modifications to PR title and body for squash merges .
* /
GithubApiMergeStrategy . prototype . _promptCommitMessageEdit = function ( pullRequest , mergeOptions ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
var commitMessage , result , _a , newTitle , newMessage ;
return tslib . _ _generator ( this , function ( _b ) {
switch ( _b . label ) {
case 0 : return [ 4 /*yield*/ , this . _getDefaultSquashCommitMessage ( pullRequest ) ] ;
case 1 :
commitMessage = _b . sent ( ) ;
return [ 4 /*yield*/ , inquirer . prompt ( {
type : 'editor' ,
name : 'result' ,
message : 'Please update the commit message' ,
default : commitMessage ,
} ) ] ;
case 2 :
result = ( _b . sent ( ) ) . result ;
_a = tslib . _ _read ( result . split ( COMMIT _HEADER _SEPARATOR ) ) , newTitle = _a [ 0 ] , newMessage = _a . slice ( 1 ) ;
// Update the merge options so that the changes are reflected in there.
mergeOptions . commit _title = newTitle + " (#" + pullRequest . prNumber + ")" ;
mergeOptions . commit _message = newMessage . join ( COMMIT _HEADER _SEPARATOR ) ;
return [ 2 /*return*/ ] ;
}
} ) ;
} ) ;
} ;
/ * *
* Gets a commit message for the given pull request . Github by default concatenates
* multiple commit messages if a PR is merged in squash mode . We try to replicate this
* behavior here so that we have a default commit message that can be fixed up .
* /
GithubApiMergeStrategy . prototype . _getDefaultSquashCommitMessage = function ( pullRequest ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
var commits , messageBase , joinedMessages ;
return tslib . _ _generator ( this , function ( _a ) {
switch ( _a . label ) {
case 0 : return [ 4 /*yield*/ , this . _getPullRequestCommitMessages ( pullRequest ) ] ;
case 1 :
commits = ( _a . sent ( ) )
. map ( function ( message ) { return ( { message : message , parsed : parseCommitMessage ( message ) } ) ; } ) ;
messageBase = "" + pullRequest . title + COMMIT _HEADER _SEPARATOR ;
if ( commits . length <= 1 ) {
return [ 2 /*return*/ , "" + messageBase + commits [ 0 ] . parsed . body ] ;
}
joinedMessages = commits . map ( function ( c ) { return "* " + c . message ; } ) . join ( COMMIT _HEADER _SEPARATOR ) ;
return [ 2 /*return*/ , "" + messageBase + joinedMessages ] ;
}
} ) ;
} ) ;
} ;
/** Gets all commit messages of commits in the pull request. */
GithubApiMergeStrategy . prototype . _getPullRequestCommitMessages = function ( _a ) {
var prNumber = _a . prNumber ;
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
var request , allCommits ;
return tslib . _ _generator ( this , function ( _b ) {
switch ( _b . label ) {
case 0 :
request = this . git . github . pulls . listCommits . endpoint . merge ( tslib . _ _assign ( tslib . _ _assign ( { } , this . git . remoteParams ) , { pull _number : prNumber } ) ) ;
return [ 4 /*yield*/ , this . git . github . paginate ( request ) ] ;
case 1 :
allCommits = _b . sent ( ) ;
return [ 2 /*return*/ , allCommits . map ( function ( _a ) {
var commit = _a . commit ;
return commit . message ;
} ) ] ;
}
} ) ;
} ) ;
} ;
/ * *
* Checks if given pull request could be merged into its target branches .
* @ returns A pull request failure if it the PR could not be merged .
* /
GithubApiMergeStrategy . prototype . _checkMergability = function ( pullRequest , targetBranches ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
var revisionRange , failedBranches ;
return tslib . _ _generator ( this , function ( _a ) {
revisionRange = this . getPullRequestRevisionRange ( pullRequest ) ;
failedBranches = this . cherryPickIntoTargetBranches ( revisionRange , targetBranches , { dryRun : true } ) ;
if ( failedBranches . length ) {
return [ 2 /*return*/ , PullRequestFailure . mergeConflicts ( failedBranches ) ] ;
}
return [ 2 /*return*/ , null ] ;
} ) ;
} ) ;
} ;
/** Determines the merge action from the given pull request. */
GithubApiMergeStrategy . prototype . _getMergeActionFromPullRequest = function ( _a ) {
var labels = _a . labels ;
if ( this . _config . labels ) {
var matchingLabel = this . _config . labels . find ( function ( _a ) {
var pattern = _a . pattern ;
return labels . some ( function ( l ) { return matchesPattern ( l , pattern ) ; } ) ;
} ) ;
if ( matchingLabel !== undefined ) {
return matchingLabel . method ;
}
}
return this . _config . default ;
} ;
return GithubApiMergeStrategy ;
} ( MergeStrategy ) ) ;
/ * *
* @ 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
* /
/** Path to the commit message filter script. Git expects this paths to use forward slashes. */
var MSG _FILTER _SCRIPT = path . join ( _ _dirname , './commit-message-filter.js' ) . replace ( /\\/g , '/' ) ;
/ * *
* Merge strategy that does not use the Github API for merging . Instead , it fetches
* all target branches and the PR locally . The PR is then cherry - picked with autosquash
* enabled into the target branches . The benefit is the support for fixup and squash commits .
* A notable downside though is that Github does not show the PR as ` Merged ` due to non
* fast - forward merges
* /
var AutosquashMergeStrategy = /** @class */ ( function ( _super ) {
tslib . _ _extends ( AutosquashMergeStrategy , _super ) ;
function AutosquashMergeStrategy ( ) {
return _super !== null && _super . apply ( this , arguments ) || this ;
}
/ * *
* Merges the specified pull request into the target branches and pushes the target
* branches upstream . This method requires the temporary target branches to be fetched
* already as we don ' t want to fetch the target branches per pull request merge . This
* would causes unnecessary multiple fetch requests when multiple PRs are merged .
* @ throws { GitCommandError } An unknown Git command error occurred that is not
* specific to the pull request merge .
* @ returns A pull request failure or null in case of success .
* /
AutosquashMergeStrategy . prototype . merge = function ( pullRequest ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
2020-12-04 18:16:14 -05:00
var prNumber , targetBranches , requiredBaseSha , needsCommitMessageFixup , githubTargetBranch , baseSha , revisionRange , branchOrRevisionBeforeRebase , rebaseEnv , failedBranches , localBranch , sha ;
2020-10-01 19:06:56 -04:00
return tslib . _ _generator ( this , function ( _a ) {
2020-12-04 18:16:14 -05:00
switch ( _a . label ) {
case 0 :
prNumber = pullRequest . prNumber , targetBranches = pullRequest . targetBranches , requiredBaseSha = pullRequest . requiredBaseSha , needsCommitMessageFixup = pullRequest . needsCommitMessageFixup , githubTargetBranch = pullRequest . githubTargetBranch ;
// In case a required base is specified for this pull request, check if the pull
// request contains the given commit. If not, return a pull request failure. This
// check is useful for enforcing that PRs are rebased on top of a given commit. e.g.
// a commit that changes the codeowner ship validation. PRs which are not rebased
// could bypass new codeowner ship rules.
if ( requiredBaseSha && ! this . git . hasCommit ( TEMP _PR _HEAD _BRANCH , requiredBaseSha ) ) {
return [ 2 /*return*/ , PullRequestFailure . unsatisfiedBaseSha ( ) ] ;
}
baseSha = this . git . run ( [ 'rev-parse' , this . getPullRequestBaseRevision ( pullRequest ) ] ) . stdout . trim ( ) ;
revisionRange = baseSha + ".." + TEMP _PR _HEAD _BRANCH ;
branchOrRevisionBeforeRebase = this . git . getCurrentBranchOrRevision ( ) ;
rebaseEnv = needsCommitMessageFixup ? undefined : tslib . _ _assign ( tslib . _ _assign ( { } , process . env ) , { GIT _SEQUENCE _EDITOR : 'true' } ) ;
this . git . run ( [ 'rebase' , '--interactive' , '--autosquash' , baseSha , TEMP _PR _HEAD _BRANCH ] , { stdio : 'inherit' , env : rebaseEnv } ) ;
// Update pull requests commits to reference the pull request. This matches what
// Github does when pull requests are merged through the Web UI. The motivation is
// that it should be easy to determine which pull request contained a given commit.
// Note: The filter-branch command relies on the working tree, so we want to make sure
// that we are on the initial branch or revision where the merge script has been invoked.
this . git . run ( [ 'checkout' , '-f' , branchOrRevisionBeforeRebase ] ) ;
this . git . run ( [ 'filter-branch' , '-f' , '--msg-filter' , MSG _FILTER _SCRIPT + " " + prNumber , revisionRange ] ) ;
failedBranches = this . cherryPickIntoTargetBranches ( revisionRange , targetBranches ) ;
if ( failedBranches . length ) {
return [ 2 /*return*/ , PullRequestFailure . mergeConflicts ( failedBranches ) ] ;
}
this . pushTargetBranchesUpstream ( targetBranches ) ;
if ( ! ( githubTargetBranch !== 'master' ) ) return [ 3 /*break*/ , 3 ] ;
localBranch = this . getLocalTargetBranchName ( githubTargetBranch ) ;
sha = this . git . run ( [ 'rev-parse' , localBranch ] ) . stdout . trim ( ) ;
// Create a comment saying the PR was closed by the SHA.
return [ 4 /*yield*/ , this . git . github . issues . createComment ( tslib . _ _assign ( tslib . _ _assign ( { } , this . git . remoteParams ) , { issue _number : pullRequest . prNumber , body : "Closed by commit " + sha } ) ) ] ;
case 1 :
// Create a comment saying the PR was closed by the SHA.
_a . sent ( ) ;
// Actually close the PR.
return [ 4 /*yield*/ , this . git . github . pulls . update ( tslib . _ _assign ( tslib . _ _assign ( { } , this . git . remoteParams ) , { pull _number : pullRequest . prNumber , state : 'closed' } ) ) ] ;
case 2 :
// Actually close the PR.
_a . sent ( ) ;
_a . label = 3 ;
case 3 : return [ 2 /*return*/ , null ] ;
2020-10-01 19:06:56 -04:00
}
} ) ;
} ) ;
} ;
return AutosquashMergeStrategy ;
} ( MergeStrategy ) ) ;
/ * *
* @ 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
* /
2021-01-22 17:05:28 -05:00
var defaultPullRequestMergeTaskFlags = {
branchPrompt : true ,
} ;
2020-10-01 19:06:56 -04:00
/ * *
* Class that accepts a merge script configuration and Github token . It provides
* a programmatic interface for merging multiple pull requests based on their
* labels that have been resolved through the merge script configuration .
* /
var PullRequestMergeTask = /** @class */ ( function ( ) {
2021-01-22 17:05:28 -05:00
function PullRequestMergeTask ( config , git , flags ) {
2020-10-01 19:06:56 -04:00
this . config = config ;
this . git = git ;
2021-01-22 17:05:28 -05:00
// Update flags property with the provided flags values as patches to the default flag values.
this . flags = tslib . _ _assign ( tslib . _ _assign ( { } , defaultPullRequestMergeTaskFlags ) , flags ) ;
2020-10-01 19:06:56 -04:00
}
/ * *
* Merges the given pull request and pushes it upstream .
* @ param prNumber Pull request that should be merged .
* @ param force Whether non - critical pull request failures should be ignored .
* /
PullRequestMergeTask . prototype . merge = function ( prNumber , force ) {
if ( force === void 0 ) { force = false ; }
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
2021-01-22 17:05:28 -05:00
var hasOauthScopes , pullRequest , _a , _b , strategy , previousBranchOrRevision , failure , e _1 ;
2020-10-01 19:06:56 -04:00
var _this = this ;
2021-01-22 17:05:28 -05:00
return tslib . _ _generator ( this , function ( _c ) {
switch ( _c . label ) {
2020-10-01 19:06:56 -04:00
case 0 : return [ 4 /*yield*/ , this . git . hasOauthScopes ( function ( scopes , missing ) {
if ( ! scopes . includes ( 'repo' ) ) {
if ( _this . config . remote . private ) {
missing . push ( 'repo' ) ;
}
else if ( ! scopes . includes ( 'public_repo' ) ) {
missing . push ( 'public_repo' ) ;
}
}
2021-05-07 10:15:25 -04:00
// Pull requests can modify Github action workflow files. In such cases Github requires us to
// push with a token that has the `workflow` oauth scope set. To avoid errors when the
// caretaker intends to merge such PRs, we ensure the scope is always set on the token before
// the merge process starts.
// https://docs.github.com/en/developers/apps/scopes-for-oauth-apps#available-scopes
if ( ! scopes . includes ( 'workflow' ) ) {
missing . push ( 'workflow' ) ;
}
2020-10-01 19:06:56 -04:00
} ) ] ;
case 1 :
2021-01-22 17:05:28 -05:00
hasOauthScopes = _c . sent ( ) ;
2020-10-01 19:06:56 -04:00
if ( hasOauthScopes !== true ) {
return [ 2 /*return*/ , {
status : 5 /* GITHUB_ERROR */ ,
failure : PullRequestFailure . insufficientPermissionsToMerge ( hasOauthScopes . error )
} ] ;
}
if ( this . git . hasUncommittedChanges ( ) ) {
return [ 2 /*return*/ , { status : 1 /* DIRTY_WORKING_DIR */ } ] ;
}
return [ 4 /*yield*/ , loadAndValidatePullRequest ( this , prNumber , force ) ] ;
case 2 :
2021-01-22 17:05:28 -05:00
pullRequest = _c . sent ( ) ;
2020-10-01 19:06:56 -04:00
if ( ! isPullRequest ( pullRequest ) ) {
return [ 2 /*return*/ , { status : 3 /* FAILED */ , failure : pullRequest } ] ;
}
2021-01-22 17:05:28 -05:00
_a = this . flags . branchPrompt ;
if ( ! _a ) return [ 3 /*break*/ , 4 ] ;
2020-10-01 19:06:56 -04:00
return [ 4 /*yield*/ , promptConfirm ( getTargettedBranchesConfirmationPromptMessage ( pullRequest ) ) ] ;
case 3 :
2021-01-22 17:05:28 -05:00
_a = ! ( _c . sent ( ) ) ;
_c . label = 4 ;
case 4 :
if ( _a ) {
2020-10-01 19:06:56 -04:00
return [ 2 /*return*/ , { status : 4 /* USER_ABORTED */ } ] ;
}
2021-01-22 17:05:28 -05:00
_b = pullRequest . hasCaretakerNote ;
if ( ! _b ) return [ 3 /*break*/ , 6 ] ;
2020-10-01 19:06:56 -04:00
return [ 4 /*yield*/ , promptConfirm ( getCaretakerNotePromptMessage ( pullRequest ) ) ] ;
case 5 :
2021-01-22 17:05:28 -05:00
_b = ! ( _c . sent ( ) ) ;
_c . label = 6 ;
case 6 :
2020-10-01 19:06:56 -04:00
// If the pull request has a caretaker note applied, raise awareness by prompting
// the caretaker. The caretaker can then decide to proceed or abort the merge.
2021-01-22 17:05:28 -05:00
if ( _b ) {
2020-10-01 19:06:56 -04:00
return [ 2 /*return*/ , { status : 4 /* USER_ABORTED */ } ] ;
}
strategy = this . config . githubApiMerge ?
new GithubApiMergeStrategy ( this . git , this . config . githubApiMerge ) :
new AutosquashMergeStrategy ( this . git ) ;
previousBranchOrRevision = null ;
2021-01-22 17:05:28 -05:00
_c . label = 7 ;
case 7 :
_c . trys . push ( [ 7 , 11 , 12 , 13 ] ) ;
2020-10-01 19:06:56 -04:00
previousBranchOrRevision = this . git . getCurrentBranchOrRevision ( ) ;
// Run preparations for the merge (e.g. fetching branches).
return [ 4 /*yield*/ , strategy . prepare ( pullRequest ) ] ;
2021-01-22 17:05:28 -05:00
case 8 :
2020-10-01 19:06:56 -04:00
// Run preparations for the merge (e.g. fetching branches).
2021-01-22 17:05:28 -05:00
_c . sent ( ) ;
2020-10-01 19:06:56 -04:00
return [ 4 /*yield*/ , strategy . merge ( pullRequest ) ] ;
2021-01-22 17:05:28 -05:00
case 9 :
failure = _c . sent ( ) ;
2020-10-01 19:06:56 -04:00
if ( failure !== null ) {
return [ 2 /*return*/ , { status : 3 /* FAILED */ , failure : failure } ] ;
}
// Switch back to the previous branch. We need to do this before deleting the temporary
// branches because we cannot delete branches which are currently checked out.
this . git . run ( [ 'checkout' , '-f' , previousBranchOrRevision ] ) ;
return [ 4 /*yield*/ , strategy . cleanup ( pullRequest ) ] ;
2021-01-22 17:05:28 -05:00
case 10 :
_c . sent ( ) ;
2020-10-01 19:06:56 -04:00
// Return a successful merge status.
return [ 2 /*return*/ , { status : 2 /* SUCCESS */ } ] ;
2021-01-22 17:05:28 -05:00
case 11 :
e _1 = _c . sent ( ) ;
2020-10-01 19:06:56 -04:00
// Catch all git command errors and return a merge result w/ git error status code.
// Other unknown errors which aren't caused by a git command are re-thrown.
if ( e _1 instanceof GitCommandError ) {
return [ 2 /*return*/ , { status : 0 /* UNKNOWN_GIT_ERROR */ } ] ;
}
throw e _1 ;
2021-01-22 17:05:28 -05:00
case 12 :
2020-10-01 19:06:56 -04:00
// Always try to restore the branch if possible. We don't want to leave
// the repository in a different state than before.
if ( previousBranchOrRevision !== null ) {
this . git . runGraceful ( [ 'checkout' , '-f' , previousBranchOrRevision ] ) ;
}
return [ 7 /*endfinally*/ ] ;
2021-01-22 17:05:28 -05:00
case 13 : return [ 2 /*return*/ ] ;
2020-10-01 19:06:56 -04:00
}
} ) ;
} ) ;
} ;
return PullRequestMergeTask ;
} ( ) ) ;
/ * *
* @ 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
* /
/ * *
* 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
* strategy , or the local autosquash strategy . Either strategy has benefits and downsides .
* More information on these strategies can be found in their dedicated strategy classes .
*
* See { @ link GithubApiMergeStrategy } and { @ link AutosquashMergeStrategy }
*
* @ param prNumber Number of the pull request that should be merged .
2021-04-18 05:45:45 -04:00
* @ param flags Configuration options for merging pull requests .
2020-10-01 19:06:56 -04:00
* /
2021-04-08 15:34:55 -04:00
function mergePullRequest ( prNumber , flags ) {
2020-10-01 19:06:56 -04:00
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
/** Performs the merge and returns whether it was successful or not. */
function performMerge ( ignoreFatalErrors ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
var result , e _1 ;
return tslib . _ _generator ( this , function ( _a ) {
switch ( _a . label ) {
case 0 :
_a . trys . push ( [ 0 , 3 , , 4 ] ) ;
return [ 4 /*yield*/ , api . merge ( prNumber , ignoreFatalErrors ) ] ;
case 1 :
result = _a . sent ( ) ;
return [ 4 /*yield*/ , handleMergeResult ( result , ignoreFatalErrors ) ] ;
case 2 : return [ 2 /*return*/ , _a . sent ( ) ] ;
case 3 :
e _1 = _a . sent ( ) ;
// Catch errors to the Github API for invalid requests. We want to
// exit the script with a better explanation of the error.
if ( e _1 instanceof GithubApiRequestError && e _1 . status === 401 ) {
error ( red ( 'Github API request failed. ' + e _1 . message ) ) ;
error ( yellow ( 'Please ensure that your provided token is valid.' ) ) ;
error ( yellow ( "You can generate a token here: " + GITHUB _TOKEN _GENERATE _URL ) ) ;
process . exit ( 1 ) ;
}
throw e _1 ;
case 4 : return [ 2 /*return*/ ] ;
}
} ) ;
} ) ;
}
/ * *
* Prompts whether the specified pull request should be forcibly merged . If so , merges
* the specified pull request forcibly ( ignoring non - critical failures ) .
* @ returns Whether the specified pull request has been forcibly merged .
* /
function promptAndPerformForceMerge ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
return tslib . _ _generator ( this , function ( _a ) {
switch ( _a . label ) {
case 0 : return [ 4 /*yield*/ , promptConfirm ( 'Do you want to forcibly proceed with merging?' ) ] ;
case 1 :
if ( _a . sent ( ) ) {
// Perform the merge in force mode. This means that non-fatal failures
// are ignored and the merge continues.
return [ 2 /*return*/ , performMerge ( true ) ] ;
}
return [ 2 /*return*/ , false ] ;
}
} ) ;
} ) ;
}
/ * *
* Handles the merge result by printing console messages , exiting the process
* based on the result , or by restarting the merge if force mode has been enabled .
* @ returns Whether the merge completed without errors or not .
* /
function handleMergeResult ( result , disableForceMergePrompt ) {
if ( disableForceMergePrompt === void 0 ) { disableForceMergePrompt = false ; }
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
var failure , status , canForciblyMerge , _a ;
return tslib . _ _generator ( this , function ( _b ) {
switch ( _b . label ) {
case 0 :
failure = result . failure , status = result . status ;
canForciblyMerge = failure && failure . nonFatal ;
_a = status ;
switch ( _a ) {
case 2 /* SUCCESS */ : return [ 3 /*break*/ , 1 ] ;
case 1 /* DIRTY_WORKING_DIR */ : return [ 3 /*break*/ , 2 ] ;
case 0 /* UNKNOWN_GIT_ERROR */ : return [ 3 /*break*/ , 3 ] ;
case 5 /* GITHUB_ERROR */ : return [ 3 /*break*/ , 4 ] ;
case 4 /* USER_ABORTED */ : return [ 3 /*break*/ , 5 ] ;
case 3 /* FAILED */ : return [ 3 /*break*/ , 6 ] ;
}
return [ 3 /*break*/ , 9 ] ;
case 1 :
info ( green ( "Successfully merged the pull request: #" + prNumber ) ) ;
return [ 2 /*return*/ , true ] ;
case 2 :
error ( red ( "Local working repository not clean. Please make sure there are " +
"no uncommitted changes." ) ) ;
return [ 2 /*return*/ , false ] ;
case 3 :
error ( red ( 'An unknown Git error has been thrown. Please check the output ' +
'above for details.' ) ) ;
return [ 2 /*return*/ , false ] ;
case 4 :
error ( red ( 'An error related to interacting with Github has been discovered.' ) ) ;
error ( failure . message ) ;
return [ 2 /*return*/ , false ] ;
case 5 :
info ( "Merge of pull request has been aborted manually: #" + prNumber ) ;
return [ 2 /*return*/ , true ] ;
case 6 :
error ( yellow ( "Could not merge the specified pull request." ) ) ;
error ( red ( failure . message ) ) ;
if ( ! ( canForciblyMerge && ! disableForceMergePrompt ) ) return [ 3 /*break*/ , 8 ] ;
info ( ) ;
info ( yellow ( 'The pull request above failed due to non-critical errors.' ) ) ;
info ( yellow ( "This error can be forcibly ignored if desired." ) ) ;
return [ 4 /*yield*/ , promptAndPerformForceMerge ( ) ] ;
case 7 : return [ 2 /*return*/ , _b . sent ( ) ] ;
case 8 : return [ 2 /*return*/ , false ] ;
case 9 : throw Error ( "Unexpected merge result: " + status ) ;
}
} ) ;
} ) ;
}
var api ;
return tslib . _ _generator ( this , function ( _a ) {
switch ( _a . label ) {
case 0 :
// 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.
2020-12-08 12:12:11 -05:00
process . env [ 'HUSKY' ] = '0' ;
2021-04-08 15:34:55 -04:00
return [ 4 /*yield*/ , createPullRequestMergeTask ( flags ) ] ;
2020-10-01 19:06:56 -04:00
case 1 :
api = _a . sent ( ) ;
return [ 4 /*yield*/ , performMerge ( false ) ] ;
case 2 :
// Perform the merge. Force mode can be activated through a command line flag.
// Alternatively, if the merge fails with non-fatal failures, the script
// will prompt whether it should rerun in force mode.
if ( ! ( _a . sent ( ) ) ) {
process . exit ( 1 ) ;
}
return [ 2 /*return*/ ] ;
}
} ) ;
} ) ;
}
/ * *
2021-04-18 05:45:45 -04:00
* Creates the pull request merge task using the given configuration options . Explicit configuration
* options can be specified when the merge script is used outside of an ` ng-dev ` configured
* repository .
2020-10-01 19:06:56 -04:00
* /
2021-04-08 15:34:55 -04:00
function createPullRequestMergeTask ( flags ) {
2020-10-01 19:06:56 -04:00
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
2021-04-08 15:34:55 -04:00
var devInfraConfig , git , _a , config , errors ;
2020-10-01 19:06:56 -04:00
return tslib . _ _generator ( this , function ( _b ) {
switch ( _b . label ) {
case 0 :
devInfraConfig = getConfig ( ) ;
2021-04-08 15:34:55 -04:00
git = GitClient . getAuthenticatedInstance ( ) ;
2020-10-01 19:06:56 -04:00
return [ 4 /*yield*/ , loadAndValidateConfig ( devInfraConfig , git . github ) ] ;
case 1 :
_a = _b . sent ( ) , config = _a . config , errors = _a . errors ;
if ( errors ) {
error ( red ( 'Invalid merge configuration:' ) ) ;
errors . forEach ( function ( desc ) { return error ( yellow ( " - " + desc ) ) ; } ) ;
process . exit ( 1 ) ;
}
// Set the remote so that the merge tool has access to information about
// the remote it intends to merge to.
config . remote = devInfraConfig . github ;
// We can cast this to a merge config with remote because we always set the
// remote above.
2021-01-22 17:05:28 -05:00
return [ 2 /*return*/ , new PullRequestMergeTask ( config , git , flags ) ] ;
2020-10-01 19:06:56 -04:00
}
} ) ;
} ) ;
}
/ * *
* @ 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
* /
2021-01-22 17:05:28 -05:00
/** Builds the command. */
2021-03-19 13:01:11 -04:00
function builder$6 ( yargs ) {
2021-01-22 17:05:28 -05:00
return addGithubTokenOption ( yargs )
. help ( )
. strict ( )
. positional ( 'pr' , {
demandOption : true ,
type : 'number' ,
description : 'The PR to be merged.' ,
} )
. option ( 'branch-prompt' , {
type : 'boolean' ,
default : true ,
description : 'Whether to prompt to confirm the branches a PR will merge into.' ,
} ) ;
2020-10-01 19:06:56 -04:00
}
2021-01-22 17:05:28 -05:00
/** Handles the command. */
2021-03-19 13:01:11 -04:00
function handler$6 ( _a ) {
2021-04-08 15:34:55 -04:00
var pr = _a . pr , branchPrompt = _a . branchPrompt ;
2020-10-01 19:06:56 -04:00
return tslib . _ _awaiter ( this , void 0 , void 0 , function ( ) {
return tslib . _ _generator ( this , function ( _b ) {
switch ( _b . label ) {
2021-04-08 15:34:55 -04:00
case 0 : return [ 4 /*yield*/ , mergePullRequest ( pr , { branchPrompt : branchPrompt } ) ] ;
2020-10-01 19:06:56 -04:00
case 1 :
_b . sent ( ) ;
return [ 2 /*return*/ ] ;
}
} ) ;
} ) ;
}
2021-03-24 19:17:15 -04:00
/** yargs command module describing the command. */
2021-01-22 17:05:28 -05:00
var MergeCommandModule = {
2021-03-19 13:01:11 -04:00
handler : handler$6 ,
builder : builder$6 ,
2021-01-22 17:05:28 -05:00
command : 'merge <pr>' ,
describe : 'Merge a PR into its targeted branches.' ,
} ;
2020-10-01 19:06:56 -04:00
/ * *
* @ 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
* /
2021-04-08 15:34:55 -04:00
/* Graphql schema for the response body for each pending PR. */
2021-04-06 10:43:21 -04:00
const PR _SCHEMA$3 = {
2020-10-01 19:06:56 -04:00
state : typedGraphqlify . types . string ,
maintainerCanModify : typedGraphqlify . types . boolean ,
viewerDidAuthor : typedGraphqlify . types . boolean ,
headRefOid : typedGraphqlify . types . string ,
headRef : {
name : typedGraphqlify . types . string ,
repository : {
url : typedGraphqlify . types . string ,
nameWithOwner : typedGraphqlify . types . string ,
} ,
} ,
baseRef : {
name : typedGraphqlify . types . string ,
repository : {
url : typedGraphqlify . types . string ,
nameWithOwner : typedGraphqlify . types . string ,
} ,
} ,
} ;
/ * *
* Rebase the provided PR onto its merge target branch , and push up the resulting
* commit to the PRs repository .
* /
function rebasePr ( prNumber , githubToken , config = getConfig ( ) ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
2021-04-08 15:34:55 -04:00
/** The singleton instance of the GitClient. */
const git = GitClient . getAuthenticatedInstance ( ) ;
2020-10-01 19:06:56 -04:00
// TODO: Rely on a common assertNoLocalChanges function.
if ( git . hasLocalChanges ( ) ) {
error ( 'Cannot perform rebase of PR with local changes.' ) ;
process . exit ( 1 ) ;
}
/ * *
* The branch or revision originally checked out before this method performed
* any Git operations that may change the working branch .
* /
const previousBranchOrRevision = git . getCurrentBranchOrRevision ( ) ;
/* Get the PR information from Github. */
2021-04-06 10:43:21 -04:00
const pr = yield getPr ( PR _SCHEMA$3 , prNumber , git ) ;
2020-10-01 19:06:56 -04:00
const headRefName = pr . headRef . name ;
const baseRefName = pr . baseRef . name ;
const fullHeadRef = ` ${ pr . headRef . repository . nameWithOwner } : ${ headRefName } ` ;
const fullBaseRef = ` ${ pr . baseRef . repository . nameWithOwner } : ${ baseRefName } ` ;
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
// allows us to use `--force-with-lease` for the detached head while ensuring that we
// never accidentally override upstream changes that have been pushed in the meanwhile.
// See:
// https://git-scm.com/docs/git-push#Documentation/git-push.txt---force-with-leaseltrefnamegtltexpectgt
const forceWithLeaseFlag = ` --force-with-lease= ${ headRefName } : ${ pr . headRefOid } ` ;
// If the PR does not allow maintainers to modify it, exit as the rebased PR cannot
// be pushed up.
if ( ! pr . maintainerCanModify && ! pr . viewerDidAuthor ) {
error ( ` Cannot rebase as you did not author the PR and the PR does not allow maintainers ` +
` to modify the PR ` ) ;
process . exit ( 1 ) ;
}
try {
// Fetch the branch at the commit of the PR, and check it out in a detached state.
info ( ` Checking out PR # ${ prNumber } from ${ fullHeadRef } ` ) ;
2020-10-29 18:43:44 -04:00
git . run ( [ 'fetch' , '-q' , headRefUrl , headRefName ] ) ;
2020-11-17 13:08:40 -05:00
git . run ( [ 'checkout' , '-q' , '--detach' , 'FETCH_HEAD' ] ) ;
2020-10-01 19:06:56 -04:00
// Fetch the PRs target branch and rebase onto it.
info ( ` Fetching ${ fullBaseRef } to rebase # ${ prNumber } on ` ) ;
2020-10-29 18:43:44 -04:00
git . run ( [ 'fetch' , '-q' , baseRefUrl , baseRefName ] ) ;
2020-11-17 13:08:40 -05:00
const commonAncestorSha = git . run ( [ 'merge-base' , 'HEAD' , 'FETCH_HEAD' ] ) . stdout . trim ( ) ;
2021-03-24 19:17:15 -04:00
const commits = yield getCommitsInRange ( commonAncestorSha , 'HEAD' ) ;
2020-11-17 13:08:40 -05:00
let squashFixups = commits . filter ( ( commit ) => commit . isFixup ) . length === 0 ?
false :
yield promptConfirm ( ` PR # ${ prNumber } contains fixup commits, would you like to squash them during rebase? ` , true ) ;
2020-10-01 19:06:56 -04:00
info ( ` Attempting to rebase PR # ${ prNumber } on ${ fullBaseRef } ` ) ;
2020-11-17 13:08:40 -05:00
/ * *
* Tuple of flags to be added to the rebase command and env object to run the git command .
*
* Additional flags to perform the autosquashing are added when the user confirm squashing of
* fixup commits should occur .
* /
const [ flags , env ] = squashFixups ?
[ [ '--interactive' , '--autosquash' ] , Object . assign ( Object . assign ( { } , process . env ) , { GIT _SEQUENCE _EDITOR : 'true' } ) ] :
[ [ ] , undefined ] ;
const rebaseResult = git . runGraceful ( [ 'rebase' , ... flags , 'FETCH_HEAD' ] , { env : env } ) ;
2020-10-01 19:06:56 -04:00
// If the rebase was clean, push the rebased PR up to the authors fork.
if ( rebaseResult . status === 0 ) {
info ( ` Rebase was able to complete automatically without conflicts ` ) ;
info ( ` Pushing rebased PR # ${ prNumber } to ${ fullHeadRef } ` ) ;
git . run ( [ 'push' , headRefUrl , ` HEAD: ${ headRefName } ` , forceWithLeaseFlag ] ) ;
info ( ` Rebased and updated PR # ${ prNumber } ` ) ;
git . checkout ( previousBranchOrRevision , true ) ;
process . exit ( 0 ) ;
}
}
catch ( err ) {
error ( err . message ) ;
git . checkout ( previousBranchOrRevision , true ) ;
process . exit ( 1 ) ;
}
// On automatic rebase failures, prompt to choose if the rebase should be continued
// manually or aborted now.
info ( ` Rebase was unable to complete automatically without conflicts. ` ) ;
// If the command is run in a non-CI environment, prompt to format the files immediately.
const continueRebase = process . env [ 'CI' ] === undefined && ( yield promptConfirm ( 'Manually complete rebase?' ) ) ;
if ( continueRebase ) {
info ( ` After manually completing rebase, run the following command to update PR # ${ prNumber } : ` ) ;
info ( ` $ git push ${ pr . headRef . repository . url } HEAD: ${ headRefName } ${ forceWithLeaseFlag } ` ) ;
info ( ) ;
info ( ` To abort the rebase and return to the state of the repository before this command ` ) ;
info ( ` run the following command: ` ) ;
info ( ` $ git rebase --abort && git reset --hard && git checkout ${ previousBranchOrRevision } ` ) ;
process . exit ( 1 ) ;
}
else {
info ( ` Cleaning up git state, and restoring previous state. ` ) ;
}
git . checkout ( previousBranchOrRevision , true ) ;
process . exit ( 1 ) ;
} ) ;
}
/ * *
* @ 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
* /
/** Builds the rebase pull request command. */
function buildRebaseCommand ( yargs ) {
return addGithubTokenOption ( yargs ) . positional ( 'prNumber' , { type : 'number' , demandOption : true } ) ;
}
/** Handles the rebase pull request command. */
function handleRebaseCommand ( { prNumber , githubToken } ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
yield rebasePr ( prNumber , githubToken ) ;
} ) ;
}
/ * *
* @ 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
* /
/** Build the parser for pull request commands. */
function buildPrParser ( localYargs ) {
return localYargs . help ( )
. strict ( )
. demandCommand ( )
. command ( 'discover-new-conflicts <pr-number>' , 'Check if a pending PR causes new conflicts for other pending PRs' , buildDiscoverNewConflictsCommand , handleDiscoverNewConflictsCommand )
. command ( 'rebase <pr-number>' , 'Rebase a pending PR and push the rebased commits back to Github' , buildRebaseCommand , handleRebaseCommand )
2021-01-22 17:05:28 -05:00
. command ( MergeCommandModule )
2020-10-28 20:14:40 -04:00
. command ( CheckoutCommandModule )
. command ( CheckTargetBranchesModule ) ;
2020-10-01 19:06:56 -04:00
}
/ * *
* @ 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
* /
/** Create logs for each pullapprove group result. */
function logGroup ( group , conditionsToPrint , printMessageFn = info ) {
const conditions = group [ conditionsToPrint ] ;
printMessageFn . group ( ` [ ${ group . groupName } ] ` ) ;
if ( conditions . length ) {
conditions . forEach ( groupCondition => {
const count = groupCondition . matchedFiles . size ;
if ( conditionsToPrint === 'unverifiableConditions' ) {
printMessageFn ( ` ${ groupCondition . expression } ` ) ;
}
else {
printMessageFn ( ` ${ count } ${ count === 1 ? 'match' : 'matches' } - ${ groupCondition . expression } ` ) ;
}
} ) ;
printMessageFn . groupEnd ( ) ;
}
}
/** Logs a header within a text drawn box. */
function logHeader ( ... params ) {
const totalWidth = 80 ;
const fillWidth = totalWidth - 2 ;
const headerText = params . join ( ' ' ) . substr ( 0 , fillWidth ) ;
const leftSpace = Math . ceil ( ( fillWidth - headerText . length ) / 2 ) ;
const rightSpace = fillWidth - leftSpace - headerText . length ;
const fill = ( count , content ) => content . repeat ( count ) ;
info ( ` ┌ ${ fill ( fillWidth , '─' ) } ┐ ` ) ;
info ( ` │ ${ fill ( leftSpace , ' ' ) } ${ headerText } ${ fill ( rightSpace , ' ' ) } │ ` ) ;
info ( ` └ ${ fill ( fillWidth , '─' ) } ┘ ` ) ;
}
/ * *
* @ 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
* /
/** Map that holds patterns and their corresponding Minimatch globs. */
const patternCache = new Map ( ) ;
/ * *
* Gets a glob for the given pattern . The cached glob will be returned
* if available . Otherwise a new glob will be created and cached .
* /
function getOrCreateGlob ( pattern ) {
if ( patternCache . has ( pattern ) ) {
return patternCache . get ( pattern ) ;
}
const glob = new minimatch . Minimatch ( pattern , { dot : true } ) ;
patternCache . set ( pattern , glob ) ;
return glob ;
}
class PullApproveGroupStateDependencyError extends Error {
constructor ( message ) {
super ( message ) ;
// Set the prototype explicitly because in ES5, the prototype is accidentally
// lost due to a limitation in down-leveling.
// https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work.
Object . setPrototypeOf ( this , PullApproveGroupStateDependencyError . prototype ) ;
// Error names are displayed in their stack but can't be set in the constructor.
this . name = PullApproveGroupStateDependencyError . name ;
}
}
/ * *
* Superset of a native array . The superset provides methods which mimic the
* list data structure used in PullApprove for files in conditions .
* /
class PullApproveStringArray extends Array {
constructor ( ... elements ) {
super ( ... elements ) ;
// Set the prototype explicitly because in ES5, the prototype is accidentally
// lost due to a limitation in down-leveling.
// https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work.
Object . setPrototypeOf ( this , PullApproveStringArray . prototype ) ;
}
/** Returns a new array which only includes files that match the given pattern. */
include ( pattern ) {
return new PullApproveStringArray ( ... this . filter ( s => getOrCreateGlob ( pattern ) . match ( s ) ) ) ;
}
/** Returns a new array which only includes files that did not match the given pattern. */
exclude ( pattern ) {
return new PullApproveStringArray ( ... this . filter ( s => ! getOrCreateGlob ( pattern ) . match ( s ) ) ) ;
}
}
/ * *
* Superset of a native array . The superset provides methods which mimic the
* list data structure used in PullApprove for groups in conditions .
* /
class PullApproveGroupArray extends Array {
constructor ( ... elements ) {
super ( ... elements ) ;
// Set the prototype explicitly because in ES5, the prototype is accidentally
// lost due to a limitation in down-leveling.
// https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work.
Object . setPrototypeOf ( this , PullApproveGroupArray . prototype ) ;
}
include ( pattern ) {
return new PullApproveGroupArray ( ... this . filter ( s => s . groupName . match ( pattern ) ) ) ;
}
/** Returns a new array which only includes files that did not match the given pattern. */
exclude ( pattern ) {
return new PullApproveGroupArray ( ... this . filter ( s => s . groupName . match ( pattern ) ) ) ;
}
get pending ( ) {
throw new PullApproveGroupStateDependencyError ( ) ;
}
get active ( ) {
throw new PullApproveGroupStateDependencyError ( ) ;
}
get inactive ( ) {
throw new PullApproveGroupStateDependencyError ( ) ;
}
get rejected ( ) {
throw new PullApproveGroupStateDependencyError ( ) ;
}
get names ( ) {
return this . map ( g => g . groupName ) ;
}
}
/ * *
* @ 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
* /
/ * *
* Context that is provided to conditions . Conditions can use various helpers
* that PullApprove provides . We try to mock them here . Consult the official
* docs for more details : https : //docs.pullapprove.com/config/conditions.
* /
const conditionContext = {
'len' : ( value ) => value . length ,
'contains_any_globs' : ( files , patterns ) => {
// Note: Do not always create globs for the same pattern again. This method
// could be called for each source file. Creating glob's is expensive.
return files . some ( f => patterns . some ( pattern => getOrCreateGlob ( pattern ) . match ( f ) ) ) ;
} ,
} ;
/ * *
* Converts a given condition to a function that accepts a set of files . The returned
* function can be called to check if the set of files matches the condition .
* /
function convertConditionToFunction ( expr ) {
// Creates a dynamic function with the specified expression.
// The first parameter will be `files` as that corresponds to the supported `files` variable that
// can be accessed in PullApprove condition expressions. The second parameter is the list of
// PullApproveGroups that are accessible in the condition expressions. The followed parameters
// correspond to other context variables provided by PullApprove for conditions.
const evaluateFn = new Function ( 'files' , 'groups' , ... Object . keys ( conditionContext ) , `
return ( $ { transformExpressionToJs ( expr ) } ) ;
` );
// Create a function that calls the dynamically constructed function which mimics
// the condition expression that is usually evaluated with Python in PullApprove.
return ( files , groups ) => {
const result = evaluateFn ( new PullApproveStringArray ( ... files ) , new PullApproveGroupArray ( ... groups ) , ... Object . values ( conditionContext ) ) ;
// If an array is returned, we consider the condition as active if the array is not
// empty. This matches PullApprove's condition evaluation that is based on Python.
if ( Array . isArray ( result ) ) {
return result . length !== 0 ;
}
return ! ! result ;
} ;
}
/ * *
* Transforms a condition expression from PullApprove that is based on python
* so that it can be run inside JavaScript . Current transformations :
* 1. ` not <..> ` - > ` !<..> `
* /
function transformExpressionToJs ( expression ) {
return expression . replace ( /not\s+/g , '!' ) ;
}
/ * *
* @ 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
* /
// Regular expression that matches conditions for the global approval.
const GLOBAL _APPROVAL _CONDITION _REGEX = /^"global-(docs-)?approvers" not in groups.approved$/ ;
// Name of the PullApprove group that serves as fallback. This group should never capture
// any conditions as it would always match specified files. This is not desired as we want
// to figure out as part of this tool, whether there actually are unmatched files.
const FALLBACK _GROUP _NAME = 'fallback' ;
/** A PullApprove group to be able to test files against. */
class PullApproveGroup {
constructor ( groupName , config , precedingGroups = [ ] ) {
this . groupName = groupName ;
this . precedingGroups = precedingGroups ;
/** List of conditions for the group. */
this . conditions = [ ] ;
this . _captureConditions ( config ) ;
}
_captureConditions ( config ) {
if ( config . conditions && this . groupName !== FALLBACK _GROUP _NAME ) {
return config . conditions . forEach ( condition => {
const expression = condition . trim ( ) ;
if ( expression . match ( GLOBAL _APPROVAL _CONDITION _REGEX ) ) {
// Currently a noop as we don't take any action for global approval conditions.
return ;
}
try {
this . conditions . push ( {
expression ,
checkFn : convertConditionToFunction ( expression ) ,
matchedFiles : new Set ( ) ,
unverifiable : false ,
} ) ;
}
catch ( e ) {
error ( ` Could not parse condition in group: ${ this . groupName } ` ) ;
error ( ` - ${ expression } ` ) ;
error ( ` Error: ` ) ;
error ( e . message ) ;
error ( e . stack ) ;
process . exit ( 1 ) ;
}
} ) ;
}
}
/ * *
* Tests a provided file path to determine if it would be considered matched by
* the pull approve group ' s conditions .
* /
testFile ( filePath ) {
return this . conditions . every ( ( condition ) => {
const { matchedFiles , checkFn , expression } = condition ;
try {
const matchesFile = checkFn ( [ filePath ] , this . precedingGroups ) ;
if ( matchesFile ) {
matchedFiles . add ( filePath ) ;
}
return matchesFile ;
}
catch ( e ) {
// In the case of a condition that depends on the state of groups we want to
// ignore that the verification can't accurately evaluate the condition and then
// continue processing. Other types of errors fail the verification, as conditions
// should otherwise be able to execute without throwing.
if ( e instanceof PullApproveGroupStateDependencyError ) {
condition . unverifiable = true ;
// Return true so that `this.conditions.every` can continue evaluating.
return true ;
}
else {
const errMessage = ` Condition could not be evaluated: \n \n ` +
` From the [ ${ this . groupName } ] group: \n ` +
` - ${ expression } ` +
` \n \n ${ e . message } ${ e . stack } \n \n ` ;
error ( errMessage ) ;
process . exit ( 1 ) ;
}
}
} ) ;
}
/** Retrieve the results for the Group, all matched and unmatched conditions. */
getResults ( ) {
const matchedConditions = this . conditions . filter ( c => c . matchedFiles . size > 0 ) ;
const unmatchedConditions = this . conditions . filter ( c => c . matchedFiles . size === 0 && ! c . unverifiable ) ;
const unverifiableConditions = this . conditions . filter ( c => c . unverifiable ) ;
return {
matchedConditions ,
matchedCount : matchedConditions . length ,
unmatchedConditions ,
unmatchedCount : unmatchedConditions . length ,
unverifiableConditions ,
groupName : this . groupName ,
} ;
}
}
/ * *
* @ 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
* /
function parsePullApproveYaml ( rawYaml ) {
return yaml . parse ( rawYaml , { merge : true } ) ;
}
/** Parses all of the groups defined in the pullapprove yaml. */
function getGroupsFromYaml ( pullApproveYamlRaw ) {
/** JSON representation of the pullapprove yaml file. */
const pullApprove = parsePullApproveYaml ( pullApproveYamlRaw ) ;
return Object . entries ( pullApprove . groups ) . reduce ( ( groups , [ groupName , group ] ) => {
return groups . concat ( new PullApproveGroup ( groupName , group , groups ) ) ;
} , [ ] ) ;
}
/ * *
* @ 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
* /
function verify$1 ( ) {
2021-04-12 17:50:50 -04:00
const git = GitClient . getInstance ( ) ;
2020-10-01 19:06:56 -04:00
/** Full path to PullApprove config file */
2021-04-12 17:50:50 -04:00
const PULL _APPROVE _YAML _PATH = path . resolve ( git . baseDir , '.pullapprove.yml' ) ;
2020-10-01 19:06:56 -04:00
/** All tracked files in the repository. */
2021-04-12 17:50:50 -04:00
const REPO _FILES = git . allFiles ( ) ;
2020-10-01 19:06:56 -04:00
/** The pull approve config file. */
const pullApproveYamlRaw = fs . readFileSync ( PULL _APPROVE _YAML _PATH , 'utf8' ) ;
/** All of the groups defined in the pullapprove yaml. */
const groups = getGroupsFromYaml ( pullApproveYamlRaw ) ;
/ * *
* PullApprove groups without conditions . These are skipped in the verification
* as those would always be active and cause zero unmatched files .
* /
const groupsSkipped = groups . filter ( group => ! group . conditions . length ) ;
/** PullApprove groups with conditions. */
const groupsWithConditions = groups . filter ( group => ! ! group . conditions . length ) ;
/** Files which are matched by at least one group. */
const matchedFiles = [ ] ;
/** Files which are not matched by at least one group. */
const unmatchedFiles = [ ] ;
// Test each file in the repo against each group for being matched.
REPO _FILES . forEach ( ( file ) => {
if ( groupsWithConditions . filter ( group => group . testFile ( file ) ) . length ) {
matchedFiles . push ( file ) ;
}
else {
unmatchedFiles . push ( file ) ;
}
} ) ;
/** Results for each group */
const resultsByGroup = groupsWithConditions . map ( group => group . getResults ( ) ) ;
/ * *
* Whether all group condition lines match at least one file and all files
* are matched by at least one group .
* /
const verificationSucceeded = resultsByGroup . every ( r => ! r . unmatchedCount ) && ! unmatchedFiles . length ;
/ * *
* Overall result
* /
logHeader ( 'Overall Result' ) ;
if ( verificationSucceeded ) {
info ( 'PullApprove verification succeeded!' ) ;
}
else {
info ( ` PullApprove verification failed. ` ) ;
info ( ) ;
info ( ` Please update '.pullapprove.yml' to ensure that all necessary ` ) ;
info ( ` files/directories have owners and all patterns that appear in ` ) ;
info ( ` the file correspond to actual files/directories in the repo. ` ) ;
}
/ * *
* File by file Summary
* /
logHeader ( 'PullApprove results by file' ) ;
info . group ( ` Matched Files ( ${ matchedFiles . length } files) ` ) ;
matchedFiles . forEach ( file => debug ( file ) ) ;
info . groupEnd ( ) ;
info . group ( ` Unmatched Files ( ${ unmatchedFiles . length } files) ` ) ;
unmatchedFiles . forEach ( file => info ( file ) ) ;
info . groupEnd ( ) ;
/ * *
* Group by group Summary
* /
logHeader ( 'PullApprove results by group' ) ;
info . group ( ` Groups skipped ( ${ groupsSkipped . length } groups) ` ) ;
groupsSkipped . forEach ( group => debug ( ` ${ group . groupName } ` ) ) ;
info . groupEnd ( ) ;
const matchedGroups = resultsByGroup . filter ( group => ! group . unmatchedCount ) ;
info . group ( ` Matched conditions by Group ( ${ matchedGroups . length } groups) ` ) ;
matchedGroups . forEach ( group => logGroup ( group , 'matchedConditions' , debug ) ) ;
info . groupEnd ( ) ;
const unmatchedGroups = resultsByGroup . filter ( group => group . unmatchedCount ) ;
info . group ( ` Unmatched conditions by Group ( ${ unmatchedGroups . length } groups) ` ) ;
unmatchedGroups . forEach ( group => logGroup ( group , 'unmatchedConditions' ) ) ;
info . groupEnd ( ) ;
const unverifiableConditionsInGroups = resultsByGroup . filter ( group => group . unverifiableConditions . length > 0 ) ;
info . group ( ` Unverifiable conditions by Group ( ${ unverifiableConditionsInGroups . length } groups) ` ) ;
unverifiableConditionsInGroups . forEach ( group => logGroup ( group , 'unverifiableConditions' ) ) ;
info . groupEnd ( ) ;
// Provide correct exit code based on verification success.
process . exit ( verificationSucceeded ? 0 : 1 ) ;
}
/** Build the parser for the pullapprove commands. */
function buildPullapproveParser ( localYargs ) {
return localYargs . help ( ) . strict ( ) . demandCommand ( ) . command ( 'verify' , 'Verify the pullapprove config' , { } , ( ) => verify$1 ( ) ) ;
}
/ * *
* @ 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
* /
/** Retrieve and validate the config as `ReleaseConfig`. */
function getReleaseConfig ( config = getConfig ( ) ) {
var _a , _b , _c ;
// List of errors encountered validating the config.
const errors = [ ] ;
if ( config . release === undefined ) {
errors . push ( ` No configuration defined for "release" ` ) ;
}
if ( ( ( _a = config . release ) === null || _a === void 0 ? void 0 : _a . npmPackages ) === undefined ) {
errors . push ( ` No "npmPackages" configured for releasing. ` ) ;
}
if ( ( ( _b = config . release ) === null || _b === void 0 ? void 0 : _b . buildPackages ) === undefined ) {
errors . push ( ` No "buildPackages" function configured for releasing. ` ) ;
}
2021-04-19 14:58:32 -04:00
if ( ( ( _c = config . release ) === null || _c === void 0 ? void 0 : _c . releaseNotes ) === undefined ) {
errors . push ( ` No "releaseNotes" configured for releasing. ` ) ;
2020-10-01 19:06:56 -04:00
}
assertNoErrors ( errors ) ;
return config . release ;
}
/ * *
* @ 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
* /
/ * *
* Builds the release output without polluting the process stdout . Build scripts commonly
* print messages to stderr or stdout . This is fine in most cases , but sometimes other tooling
* reserves stdout for data transfer ( e . g . when ` ng release build --json ` is invoked ) . To not
* pollute the stdout in such cases , we launch a child process for building the release packages
* and redirect all stdout output to the stderr channel ( which can be read in the terminal ) .
* /
function buildReleaseOutput ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
return new Promise ( resolve => {
const buildProcess = child _process . fork ( require . resolve ( './build-worker' ) , [ ] , {
// The stdio option is set to redirect any "stdout" output directly to the "stderr" file
// descriptor. An additional "ipc" file descriptor is created to support communication with
// the build process. https://nodejs.org/api/child_process.html#child_process_options_stdio.
stdio : [ 'inherit' , 2 , 2 , 'ipc' ] ,
} ) ;
let builtPackages = null ;
// The child process will pass the `buildPackages()` output through the
// IPC channel. We keep track of it so that we can use it as resolve value.
buildProcess . on ( 'message' , buildResponse => builtPackages = buildResponse ) ;
// On child process exit, resolve the promise with the received output.
buildProcess . on ( 'exit' , ( ) => resolve ( builtPackages ) ) ;
} ) ;
} ) ;
}
/ * *
* @ 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
* /
/** Yargs command builder for configuring the `ng-dev release build` command. */
2021-03-19 13:01:11 -04:00
function builder$7 ( argv ) {
2020-10-01 19:06:56 -04:00
return argv . option ( 'json' , {
type : 'boolean' ,
description : 'Whether the built packages should be printed to stdout as JSON.' ,
default : false ,
} ) ;
}
/** Yargs command handler for building a release. */
2021-03-19 13:01:11 -04:00
function handler$7 ( args ) {
2020-10-01 19:06:56 -04:00
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const { npmPackages } = getReleaseConfig ( ) ;
let builtPackages = yield buildReleaseOutput ( ) ;
// If package building failed, print an error and exit with an error code.
if ( builtPackages === null ) {
error ( red ( ` ✘ Could not build release output. Please check output above. ` ) ) ;
process . exit ( 1 ) ;
}
// If no packages have been built, we assume that this is never correct
// and exit with an error code.
if ( builtPackages . length === 0 ) {
error ( red ( ` ✘ No release packages have been built. Please ensure that the ` ) ) ;
error ( red ( ` build script is configured correctly in ".ng-dev". ` ) ) ;
process . exit ( 1 ) ;
}
const missingPackages = npmPackages . filter ( pkgName => ! builtPackages . find ( b => b . name === pkgName ) ) ;
// Check for configured release packages which have not been built. We want to
// error and exit if any configured package has not been built.
if ( missingPackages . length > 0 ) {
error ( red ( ` ✘ Release output missing for the following packages: ` ) ) ;
missingPackages . forEach ( pkgName => error ( red ( ` - ${ pkgName } ` ) ) ) ;
process . exit ( 1 ) ;
}
if ( args . json ) {
process . stdout . write ( JSON . stringify ( builtPackages , null , 2 ) ) ;
}
else {
info ( green ( ' ✓ Built release packages.' ) ) ;
builtPackages . forEach ( ( { name } ) => info ( green ( ` - ${ name } ` ) ) ) ;
}
} ) ;
}
/** CLI command module for building release output. */
const ReleaseBuildCommandModule = {
2021-03-19 13:01:11 -04:00
builder : builder$7 ,
handler : handler$7 ,
2020-10-01 19:06:56 -04:00
command : 'build' ,
describe : 'Builds the release output for the current branch.' ,
} ;
2021-01-19 11:29:46 -05:00
/ * *
* @ 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
* /
2021-05-04 11:22:11 -04:00
/ * *
* Spawns a given command with the specified arguments inside an interactive shell . All process
* stdin , stdout and stderr output is printed to the current console .
*
* @ returns a Promise resolving on success , and rejecting on command failure with the status code .
* /
function spawnInteractiveCommand ( command , args , options ) {
if ( options === void 0 ) { options = { } ; }
return new Promise ( function ( resolve , reject ) {
var commandText = command + " " + args . join ( ' ' ) ;
debug ( "Executing command: " + commandText ) ;
var childProcess = child _process . spawn ( command , args , tslib . _ _assign ( tslib . _ _assign ( { } , options ) , { shell : true , stdio : 'inherit' } ) ) ;
childProcess . on ( 'exit' , function ( status ) { return status === 0 ? resolve ( ) : reject ( status ) ; } ) ;
} ) ;
}
2021-01-19 11:29:46 -05:00
/ * *
* Spawns a given command with the specified arguments inside a shell . All process stdout
* output is captured and returned as resolution on completion . Depending on the chosen
* output mode , stdout / stderr output is also printed to the console , or only on error .
*
2021-04-21 13:46:17 -04:00
* @ returns a Promise resolving with captured stdout and stderr on success . The promise
2021-01-19 11:29:46 -05:00
* rejects on command failure .
* /
function spawnWithDebugOutput ( command , args , options ) {
if ( options === void 0 ) { options = { } ; }
return new Promise ( function ( resolve , reject ) {
var commandText = command + " " + args . join ( ' ' ) ;
var outputMode = options . mode ;
debug ( "Executing command: " + commandText ) ;
2021-05-04 11:22:11 -04:00
var childProcess = child _process . spawn ( command , args , tslib . _ _assign ( tslib . _ _assign ( { } , options ) , { shell : true , stdio : 'pipe' } ) ) ;
2021-01-19 11:29:46 -05:00
var logOutput = '' ;
var stdout = '' ;
2021-04-21 13:46:17 -04:00
var stderr = '' ;
2021-01-19 11:29:46 -05:00
// Capture the stdout separately so that it can be passed as resolve value.
// This is useful if commands return parsable stdout.
childProcess . stderr . on ( 'data' , function ( message ) {
2021-04-21 13:46:17 -04:00
stderr += message ;
2021-01-19 11:29:46 -05:00
logOutput += message ;
// If console output is enabled, print the message directly to the stderr. Note that
// we intentionally print all output to stderr as stdout should not be polluted.
if ( outputMode === undefined || outputMode === 'enabled' ) {
process . stderr . write ( message ) ;
}
} ) ;
childProcess . stdout . on ( 'data' , function ( message ) {
stdout += message ;
logOutput += message ;
// If console output is enabled, print the message directly to the stderr. Note that
// we intentionally print all output to stderr as stdout should not be polluted.
if ( outputMode === undefined || outputMode === 'enabled' ) {
process . stderr . write ( message ) ;
}
} ) ;
childProcess . on ( 'exit' , function ( status , signal ) {
var exitDescription = status !== null ? "exit code \"" + status + "\"" : "signal \"" + signal + "\"" ;
var printFn = outputMode === 'on-error' ? error : debug ;
printFn ( "Command \"" + commandText + "\" completed with " + exitDescription + "." ) ;
printFn ( "Process output: \n" + logOutput ) ;
// On success, resolve the promise. Otherwise reject with the captured stderr
// and stdout log output if the output mode was set to `silent`.
if ( status === 0 ) {
2021-04-21 13:46:17 -04:00
resolve ( { stdout : stdout , stderr : stderr } ) ;
2021-01-19 11:29:46 -05:00
}
else {
reject ( outputMode === 'silent' ? logOutput : undefined ) ;
}
} ) ;
} ) ;
}
/ * *
* @ 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
* /
/ * *
* Runs NPM publish within a specified package directory .
* @ throws With the process log output if the publish failed .
* /
function runNpmPublish ( packagePath , distTag , registryUrl ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const args = [ 'publish' , '--access' , 'public' , '--tag' , distTag ] ;
// If a custom registry URL has been specified, add the `--registry` flag.
if ( registryUrl !== undefined ) {
args . push ( '--registry' , registryUrl ) ;
}
yield spawnWithDebugOutput ( 'npm' , args , { cwd : packagePath , mode : 'silent' } ) ;
} ) ;
}
/ * *
* Sets the NPM tag to the specified version for the given package .
* @ throws With the process log output if the tagging failed .
* /
function setNpmTagForPackage ( packageName , distTag , version , registryUrl ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const args = [ 'dist-tag' , 'add' , ` ${ packageName } @ ${ version } ` , distTag ] ;
// If a custom registry URL has been specified, add the `--registry` flag.
if ( registryUrl !== undefined ) {
args . push ( '--registry' , registryUrl ) ;
}
yield spawnWithDebugOutput ( 'npm' , args , { mode : 'silent' } ) ;
} ) ;
}
/ * *
* Checks whether the user is currently logged into NPM .
* @ returns Whether the user is currently logged into NPM .
* /
function npmIsLoggedIn ( registryUrl ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const args = [ 'whoami' ] ;
// If a custom registry URL has been specified, add the `--registry` flag.
if ( registryUrl !== undefined ) {
args . push ( '--registry' , registryUrl ) ;
}
try {
yield spawnWithDebugOutput ( 'npm' , args , { mode : 'silent' } ) ;
}
catch ( e ) {
return false ;
}
return true ;
} ) ;
}
/ * *
* Log into NPM at a provided registry .
2021-05-04 11:22:11 -04:00
* @ throws With the ` npm login ` status code if the login failed .
2021-01-19 11:29:46 -05:00
* /
function npmLogin ( registryUrl ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const args = [ 'login' , '--no-browser' ] ;
// If a custom registry URL has been specified, add the `--registry` flag. The `--registry` flag
// must be spliced into the correct place in the command as npm expects it to be the flag
// immediately following the login subcommand.
if ( registryUrl !== undefined ) {
args . splice ( 1 , 0 , '--registry' , registryUrl ) ;
}
2021-05-04 11:22:11 -04:00
// The login command prompts for username, password and other profile information. Hence
// the process needs to be interactive (i.e. respecting current TTYs stdin).
yield spawnInteractiveCommand ( 'npm' , args ) ;
2021-01-19 11:29:46 -05:00
} ) ;
}
/ * *
* Log out of NPM at a provided registry .
* @ returns Whether the user was logged out of NPM .
* /
function npmLogout ( registryUrl ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const args = [ 'logout' ] ;
// If a custom registry URL has been specified, add the `--registry` flag. The `--registry` flag
// must be spliced into the correct place in the command as npm expects it to be the flag
// immediately following the logout subcommand.
if ( registryUrl !== undefined ) {
args . splice ( 1 , 0 , '--registry' , registryUrl ) ;
}
try {
yield spawnWithDebugOutput ( 'npm' , args , { mode : 'silent' } ) ;
}
finally {
return npmIsLoggedIn ( registryUrl ) ;
}
} ) ;
}
2020-10-01 19:06:56 -04:00
/ * *
* @ 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
* /
/ * *
* Prints the active release trains to the console .
* @ params active Active release trains that should be printed .
* @ params config Release configuration used for querying NPM on published versions .
* /
function printActiveReleaseTrains ( active , config ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const { releaseCandidate , next , latest } = active ;
const isNextPublishedToNpm = yield isVersionPublishedToNpm ( next . version , config ) ;
const nextTrainType = next . isMajor ? 'major' : 'minor' ;
const ltsBranches = yield fetchLongTermSupportBranchesFromNpm ( config ) ;
info ( ) ;
info ( blue ( 'Current version branches in the project:' ) ) ;
// Print information for release trains in the feature-freeze/release-candidate phase.
if ( releaseCandidate !== null ) {
const rcVersion = releaseCandidate . version ;
const rcTrainType = releaseCandidate . isMajor ? 'major' : 'minor' ;
const rcTrainPhase = rcVersion . prerelease [ 0 ] === 'next' ? 'feature-freeze' : 'release-candidate' ;
info ( ` • ${ bold ( releaseCandidate . branchName ) } contains changes for an upcoming ` +
` ${ rcTrainType } that is currently in ${ bold ( rcTrainPhase ) } phase. ` ) ;
info ( ` Most recent pre-release for this branch is " ${ bold ( ` v ${ rcVersion } ` ) } ". ` ) ;
}
// Print information about the release-train in the latest phase. i.e. the patch branch.
info ( ` • ${ bold ( latest . branchName ) } contains changes for the most recent patch. ` ) ;
info ( ` Most recent patch version for this branch is " ${ bold ( ` v ${ latest . version } ` ) } ". ` ) ;
// Print information about the release-train in the next phase.
info ( ` • ${ bold ( next . branchName ) } contains changes for a ${ nextTrainType } ` +
` currently in active development. ` ) ;
// Note that there is a special case for versions in the next release-train. The version in
// the next branch is not always published to NPM. This can happen when we recently branched
// off for a feature-freeze release-train. More details are in the next pre-release action.
if ( isNextPublishedToNpm ) {
info ( ` Most recent pre-release version for this branch is " ${ bold ( ` v ${ next . version } ` ) } ". ` ) ;
}
else {
info ( ` Version is currently set to " ${ bold ( ` v ${ next . version } ` ) } ", but has not been ` +
` published yet. ` ) ;
}
// If no release-train in release-candidate or feature-freeze phase is active,
// we print a message as last bullet point to make this clear.
if ( releaseCandidate === null ) {
info ( ' • No release-candidate or feature-freeze branch currently active.' ) ;
}
info ( ) ;
info ( blue ( 'Current active LTS version branches:' ) ) ;
// Print all active LTS branches (each branch as own bullet point).
if ( ltsBranches . active . length !== 0 ) {
for ( const ltsBranch of ltsBranches . active ) {
info ( ` • ${ bold ( ltsBranch . name ) } is currently in active long-term support phase. ` ) ;
info ( ` Most recent patch version for this branch is " ${ bold ( ` v ${ ltsBranch . version } ` ) } ". ` ) ;
}
}
info ( ) ;
} ) ;
}
/ * *
* @ 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
* /
/** Error that will be thrown if the user manually aborted a release action. */
class UserAbortedReleaseActionError extends Error {
constructor ( ) {
super ( ) ;
// Set the prototype explicitly because in ES5, the prototype is accidentally lost due to
// a limitation in down-leveling.
// https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work.
Object . setPrototypeOf ( this , UserAbortedReleaseActionError . prototype ) ;
}
}
/** Error that will be thrown if the action has been aborted due to a fatal error. */
class FatalReleaseActionError extends Error {
constructor ( ) {
super ( ) ;
// Set the prototype explicitly because in ES5, the prototype is accidentally lost due to
// a limitation in down-leveling.
// https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work.
Object . setPrototypeOf ( this , FatalReleaseActionError . prototype ) ;
}
}
/ * *
* @ 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
* /
/ * *
* Increments a specified SemVer version . Compared to the original increment in SemVer ,
* the version is cloned to not modify the original version instance .
* /
function semverInc ( version , release , identifier ) {
const clone = new semver . SemVer ( version . version ) ;
return clone . inc ( release , identifier ) ;
}
/ * *
* @ 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
* /
/** Gets the commit message for a new release point in the project. */
function getCommitMessageForRelease ( newVersion ) {
return ` release: cut the v ${ newVersion } release ` ;
}
/ * *
* Gets the commit message for an exceptional version bump in the next branch . The next
* branch version will be bumped without the release being published in some situations .
* More details can be found in the ` MoveNextIntoFeatureFreeze ` release action and in :
* https : //hackmd.io/2Le8leq0S6G_R5VEVTNK9A.
* /
function getCommitMessageForExceptionalNextVersionBump ( newVersion ) {
return ` release: bump the next branch to v ${ newVersion } ` ;
}
/** Gets the commit message for a release notes cherry-pick commit */
function getReleaseNoteCherryPickCommitMessage ( newVersion ) {
return ` docs: release notes for the v ${ newVersion } release ` ;
}
/ * *
* @ 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
* /
/** Project-relative path for the changelog file. */
const changelogPath = 'CHANGELOG.md' ;
/** Project-relative path for the "package.json" file. */
const packageJsonPath = 'package.json' ;
/** Default interval in milliseconds to check whether a pull request has been merged. */
const waitForPullRequestInterval = 10000 ;
/ * *
* @ 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
* /
/ *
* # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
*
* This file contains helpers for invoking external ` ng-dev ` commands . A subset of actions ,
* like building release output or setting aν NPM dist tag for release packages , cannot be
* performed directly as part of the release tool and need to be delegated to external ` ng-dev `
* commands that exist across arbitrary version branches .
*
* In a concrete example : Consider a new patch version is released and that a new release
* package has been added to the ` next ` branch . The patch branch will not contain the new
* release package , so we could not build the release output for it . To work around this , we
* call the ng - dev build command for the patch version branch and expect it to return a list
* of built packages that need to be released as part of this release train .
*
* # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
* /
/ * *
* Invokes the ` ng-dev release set-dist-tag ` command in order to set the specified
* NPM dist tag for all packages in the checked out branch to the given version .
* /
function invokeSetNpmDistCommand ( npmDistTag , version ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
try {
// Note: No progress indicator needed as that is the responsibility of the command.
yield spawnWithDebugOutput ( 'yarn' , [ '--silent' , 'ng-dev' , 'release' , 'set-dist-tag' , npmDistTag , version . format ( ) ] ) ;
info ( green ( ` ✓ Set " ${ npmDistTag } " NPM dist tag for all packages to v ${ version } . ` ) ) ;
}
catch ( e ) {
error ( e ) ;
error ( red ( ` ✘ An error occurred while setting the NPM dist tag for " ${ npmDistTag } ". ` ) ) ;
throw new FatalReleaseActionError ( ) ;
}
} ) ;
}
/ * *
* Invokes the ` ng-dev release build ` command in order to build the release
* packages for the currently checked out branch .
* /
function invokeReleaseBuildCommand ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const spinner = ora . call ( undefined ) . start ( 'Building release output.' ) ;
try {
// Since we expect JSON to be printed from the `ng-dev release build` command,
// we spawn the process in silent mode. We have set up an Ora progress spinner.
const { stdout } = yield spawnWithDebugOutput ( 'yarn' , [ '--silent' , 'ng-dev' , 'release' , 'build' , '--json' ] , { mode : 'silent' } ) ;
spinner . stop ( ) ;
info ( green ( ' ✓ Built release output for all packages.' ) ) ;
// The `ng-dev release build` command prints a JSON array to stdout
// that represents the built release packages and their output paths.
return JSON . parse ( stdout . trim ( ) ) ;
}
catch ( e ) {
spinner . stop ( ) ;
error ( e ) ;
error ( red ( ' ✘ An error occurred while building the release packages.' ) ) ;
throw new FatalReleaseActionError ( ) ;
}
} ) ;
}
/ * *
* Invokes the ` yarn install ` command in order to install dependencies for
* the configured project with the currently checked out revision .
* /
function invokeYarnInstallCommand ( projectDir ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
try {
// Note: No progress indicator needed as that is the responsibility of the command.
// TODO: Consider using an Ora spinner instead to ensure minimal console output.
yield spawnWithDebugOutput ( 'yarn' , [ 'install' , '--frozen-lockfile' , '--non-interactive' ] , { cwd : projectDir } ) ;
info ( green ( ' ✓ Installed project dependencies.' ) ) ;
}
catch ( e ) {
error ( e ) ;
error ( red ( ' ✘ An error occurred while installing dependencies.' ) ) ;
throw new FatalReleaseActionError ( ) ;
}
} ) ;
}
/ * *
* @ 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
* /
/ * *
* Graphql Github API query that can be used to find forks of a given repository
* that are owned by the current viewer authenticated with the Github API .
* /
const findOwnedForksOfRepoQuery = typedGraphqlify . params ( {
$owner : 'String!' ,
$name : 'String!' ,
} , {
repository : typedGraphqlify . params ( { owner : '$owner' , name : '$name' } , {
forks : typedGraphqlify . params ( { affiliations : 'OWNER' , first : 1 } , {
nodes : [ {
owner : {
login : typedGraphqlify . types . string ,
} ,
name : typedGraphqlify . types . string ,
} ] ,
} ) ,
} ) ,
} ) ;
/ * *
* @ 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
* /
2020-12-17 15:17:38 -05:00
/** Thirty seconds in milliseconds. */
const THIRTY _SECONDS _IN _MS = 30000 ;
2020-10-01 19:06:56 -04:00
/** Gets whether a given pull request has been merged. */
function getPullRequestState ( api , id ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const { data } = yield api . github . pulls . get ( Object . assign ( Object . assign ( { } , api . remoteParams ) , { pull _number : id } ) ) ;
if ( data . merged ) {
return 'merged' ;
}
2020-12-17 15:17:38 -05:00
// Check if the PR was closed more than 30 seconds ago, this extra time gives Github time to
// update the closed pull request to be associated with the closing commit.
// Note: a Date constructed with `null` creates an object at 0 time, which will never be greater
// than the current date time.
if ( data . closed _at !== null &&
( new Date ( data . closed _at ) . getTime ( ) < Date . now ( ) - THIRTY _SECONDS _IN _MS ) ) {
2020-10-01 19:06:56 -04:00
return ( yield isPullRequestClosedWithAssociatedCommit ( api , id ) ) ? 'merged' : 'closed' ;
}
2020-12-17 15:17:38 -05:00
return 'open' ;
2020-10-01 19:06:56 -04:00
} ) ;
}
/ * *
* Whether the pull request has been closed with an associated commit . This is usually
* the case if a PR has been merged using the autosquash merge script strategy . Since
* 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.
* /
function isPullRequestClosedWithAssociatedCommit ( api , id ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const request = api . github . issues . listEvents . endpoint . merge ( Object . assign ( Object . assign ( { } , api . remoteParams ) , { issue _number : id } ) ) ;
const events = yield api . github . paginate ( request ) ;
// Iterate through the events of the pull request in reverse. We want to find the most
// recent events and check if the PR has been closed with a commit associated with it.
// If the PR has been closed through a commit, we assume that the PR has been merged
// using the autosquash merge strategy. For more details. See the `AutosquashMergeStrategy`.
for ( let i = events . length - 1 ; i >= 0 ; i -- ) {
const { event , commit _id } = events [ i ] ;
// If we come across a "reopened" event, we abort looking for referenced commits. Any
// commits that closed the PR before, are no longer relevant and did not close the PR.
if ( event === 'reopened' ) {
return false ;
}
// If a `closed` event is captured with a commit assigned, then we assume that
// this PR has been merged properly.
if ( event === 'closed' && commit _id ) {
return true ;
}
// If the PR has been referenced by a commit, check if the commit closes this pull
// request. Note that this is needed besides checking `closed` as PRs could be merged
// into any non-default branch where the `Closes <..>` keyword does not work and the PR
// is simply closed without an associated `commit_id`. For more details see:
// https://docs.github.com/en/enterprise/2.16/user/github/managing-your-work-on-github/closing-issues-using-keywords#:~:text=non-default.
if ( event === 'referenced' && commit _id &&
( yield isCommitClosingPullRequest ( api , commit _id , id ) ) ) {
return true ;
}
}
return false ;
} ) ;
}
/** Checks whether the specified commit is closing the given pull request. */
function isCommitClosingPullRequest ( api , sha , id ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const { data } = yield api . github . repos . getCommit ( Object . assign ( Object . assign ( { } , 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.
return data . commit . message . match ( new RegExp ( ` (?:close[sd]?|fix(?:e[sd]?)|resolve[sd]?):? # ${ id } (?! \\ d) ` , 'i' ) ) ;
} ) ;
}
/ * *
* @ 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
* /
2021-04-01 19:28:17 -04:00
/** List of types to be included in the release notes. */
const typesToIncludeInReleaseNotes = Object . values ( COMMIT _TYPES )
. filter ( type => type . releaseNotesLevel === ReleaseNotesLevel . Visible )
. map ( type => type . name ) ;
2021-04-19 14:58:32 -04:00
/** Context class used for rendering release notes. */
class RenderContext {
constructor ( data ) {
this . data = data ;
/** An array of group names in sort order if defined. */
this . groupOrder = this . data . groupOrder || [ ] ;
/** An array of scopes to hide from the release entry output. */
this . hiddenScopes = this . data . hiddenScopes || [ ] ;
/** The title of the release, or `false` if no title should be used. */
this . title = this . data . title ;
/** An array of commits in the release period. */
this . commits = this . data . commits ;
/** The version of the release. */
this . version = this . data . version ;
/** The date stamp string for use in the release notes entry. */
this . dateStamp = buildDateStamp ( this . data . date ) ;
}
/ * *
* Organizes and sorts the commits into groups of commits .
*
* Groups are sorted either by default ` Array.sort ` order , or using the provided group order from
* the configuration . Commits are order in the same order within each groups commit list as they
* appear in the provided list of commits .
* * /
asCommitGroups ( commits ) {
/** The discovered groups to organize into. */
const groups = new Map ( ) ;
// Place each commit in the list into its group.
commits . forEach ( commit => {
const key = commit . npmScope ? ` ${ commit . npmScope } / ${ commit . scope } ` : commit . scope ;
const groupCommits = groups . get ( key ) || [ ] ;
groups . set ( key , groupCommits ) ;
groupCommits . push ( commit ) ;
} ) ;
/ * *
* Array of CommitGroups containing the discovered commit groups . Sorted in alphanumeric order
* of the group title .
* /
const commitGroups = Array . from ( groups . entries ( ) )
. map ( ( [ title , commits ] ) => ( { title , commits } ) )
. sort ( ( a , b ) => a . title > b . title ? 1 : a . title < b . title ? - 1 : 0 ) ;
// If the configuration provides a sorting order, updated the sorted list of group keys to
// satisfy the order of the groups provided in the list with any groups not found in the list at
// the end of the sorted list.
if ( this . groupOrder . length ) {
for ( const groupTitle of this . groupOrder . reverse ( ) ) {
const currentIdx = commitGroups . findIndex ( k => k . title === groupTitle ) ;
if ( currentIdx !== - 1 ) {
const removedGroups = commitGroups . splice ( currentIdx , 1 ) ;
commitGroups . splice ( 0 , 0 , ... removedGroups ) ;
}
}
}
return commitGroups ;
}
/ * *
* A filter function for filtering a list of commits to only include commits which should appear
* in release notes .
* /
includeInReleaseNotes ( ) {
return ( commit ) => {
if ( ! typesToIncludeInReleaseNotes . includes ( commit . type ) ) {
return false ;
}
if ( this . hiddenScopes . includes ( commit . scope ) ) {
return false ;
}
return true ;
} ;
}
/ * *
* A filter function for filtering a list of commits to only include commits which contain a
* truthy value , or for arrays an array with 1 or more elements , for the provided field .
* /
contains ( field ) {
return ( commit ) => {
const fieldValue = commit [ field ] ;
if ( ! fieldValue ) {
return false ;
}
if ( Array . isArray ( fieldValue ) && fieldValue . length === 0 ) {
return false ;
}
return true ;
} ;
}
/ * *
* A filter function for filtering a list of commits to only include commits which contain a
* unique value for the provided field across all commits in the list .
* /
unique ( field ) {
const set = new Set ( ) ;
return ( commit ) => {
const include = ! set . has ( commit [ field ] ) ;
set . add ( commit [ field ] ) ;
return include ;
} ;
}
}
2020-10-01 19:06:56 -04:00
/ * *
2021-04-19 14:58:32 -04:00
* Builds a date stamp for stamping in release notes .
*
* Uses the current date , or a provided date in the format of YYYY - MM - DD , i . e . 1970 - 11 - 05.
2020-10-01 19:06:56 -04:00
* /
2021-04-19 14:58:32 -04:00
function buildDateStamp ( date = new Date ( ) ) {
const year = ` ${ date . getFullYear ( ) } ` ;
const month = ` ${ ( date . getMonth ( ) + 1 ) } ` . padStart ( 2 , '0' ) ;
const day = ` ${ date . getDate ( ) } ` . padStart ( 2 , '0' ) ;
return [ year , month , day ] . join ( '-' ) ;
2020-10-01 19:06:56 -04:00
}
2021-04-19 14:58:32 -04:00
2020-10-01 19:06:56 -04:00
/** Gets the path for the changelog file in a given project. */
function getLocalChangelogFilePath ( projectDir ) {
return path . join ( projectDir , changelogPath ) ;
}
2021-04-19 14:58:32 -04:00
/** Release note generation. */
class ReleaseNotes {
2021-05-06 15:30:35 -04:00
constructor ( version , startingRef , endingRef ) {
2021-04-19 14:58:32 -04:00
this . version = version ;
2021-05-06 15:30:35 -04:00
this . startingRef = startingRef ;
this . endingRef = endingRef ;
2021-04-19 14:58:32 -04:00
/** An instance of GitClient. */
this . git = GitClient . getInstance ( ) ;
/** A promise resolving to a list of Commits since the latest semver tag on the branch. */
2021-05-06 15:30:35 -04:00
this . commits = this . getCommitsInRange ( this . startingRef , this . endingRef ) ;
/** The configuration for release notes. */
this . config = this . getReleaseConfig ( ) . releaseNotes ;
2021-04-19 14:58:32 -04:00
}
2021-05-06 15:30:35 -04:00
static fromRange ( version , startingRef , endingRef ) {
2021-04-19 14:58:32 -04:00
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
2021-05-06 15:30:35 -04:00
return new ReleaseNotes ( version , startingRef , endingRef ) ;
2021-04-19 14:58:32 -04:00
} ) ;
}
/** Retrieve the release note generated for a Github Release. */
getGithubReleaseEntry ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
return ejs . renderFile ( path . join ( _ _dirname , 'templates/github-release.ejs' ) , yield this . generateRenderContext ( ) , { rmWhitespace : true } ) ;
} ) ;
}
/** Retrieve the release note generated for a CHANGELOG entry. */
getChangelogEntry ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
return ejs . renderFile ( path . join ( _ _dirname , 'templates/changelog.ejs' ) , yield this . generateRenderContext ( ) , { rmWhitespace : true } ) ;
} ) ;
}
/ * *
* Prompt the user for a title for the release , if the project ' s configuration is defined to use a
* title .
* /
promptForReleaseTitle ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
if ( this . title === undefined ) {
2021-05-06 15:30:35 -04:00
if ( this . config . useReleaseTitle ) {
2021-04-19 14:58:32 -04:00
this . title = yield promptInput ( 'Please provide a title for the release:' ) ;
}
else {
this . title = false ;
}
}
return this . title ;
} ) ;
}
/** Build the render context data object for constructing the RenderContext instance. */
generateRenderContext ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
if ( ! this . renderContext ) {
this . renderContext = new RenderContext ( {
commits : yield this . commits ,
github : this . git . remoteConfig ,
version : this . version . format ( ) ,
2021-05-06 15:30:35 -04:00
groupOrder : this . config . groupOrder ,
hiddenScopes : this . config . hiddenScopes ,
2021-04-19 14:58:32 -04:00
title : yield this . promptForReleaseTitle ( ) ,
} ) ;
}
return this . renderContext ;
} ) ;
}
2021-05-06 15:30:35 -04:00
// These methods are used for access to the utility functions while allowing them to be
// overwritten in subclasses during testing.
getCommitsInRange ( from , to ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
return getCommitsInRange ( from , to ) ;
} ) ;
}
getReleaseConfig ( config ) {
return getReleaseConfig ( config ) ;
}
2021-04-19 14:58:32 -04:00
}
2020-10-01 19:06:56 -04:00
/ * *
* @ 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
* /
/ * *
* Abstract base class for a release action . A release action is selectable by the caretaker
* if active , and can perform changes for releasing , such as staging a release , bumping the
* version , cherry - picking the changelog , branching off from master . etc .
* /
class ReleaseAction {
constructor ( active , git , config , projectDir ) {
this . active = active ;
this . git = git ;
this . config = config ;
this . projectDir = projectDir ;
/** Cached found fork of the configured project. */
this . _cachedForkRepo = null ;
}
/** Whether the release action is currently active. */
static isActive ( _trains ) {
throw Error ( 'Not implemented.' ) ;
}
/** Updates the version in the project top-level `package.json` file. */
updateProjectVersion ( newVersion ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const pkgJsonPath = path . join ( this . projectDir , packageJsonPath ) ;
const pkgJson = JSON . parse ( yield fs . promises . readFile ( pkgJsonPath , 'utf8' ) ) ;
pkgJson . version = newVersion . format ( ) ;
// Write the `package.json` file. Note that we add a trailing new line
// to avoid unnecessary diff. IDEs usually add a trailing new line.
yield fs . promises . writeFile ( pkgJsonPath , ` ${ JSON . stringify ( pkgJson , null , 2 ) } \n ` ) ;
info ( green ( ` ✓ Updated project version to ${ pkgJson . version } ` ) ) ;
} ) ;
}
/** Gets the most recent commit of a specified branch. */
_getCommitOfBranch ( branchName ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const { data : { commit } } = yield this . git . github . repos . getBranch ( Object . assign ( Object . assign ( { } , this . git . remoteParams ) , { branch : branchName } ) ) ;
return commit . sha ;
} ) ;
}
/** Verifies that the latest commit for the given branch is passing all statuses. */
verifyPassingGithubStatus ( branchName ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const commitSha = yield this . _getCommitOfBranch ( branchName ) ;
const { data : { state } } = yield this . git . github . repos . getCombinedStatusForRef ( Object . assign ( Object . assign ( { } , this . git . remoteParams ) , { ref : commitSha } ) ) ;
const branchCommitsUrl = getListCommitsInBranchUrl ( this . git , branchName ) ;
if ( state === 'failure' ) {
error ( red ( ` ✘ Cannot stage release. Commit " ${ commitSha } " does not pass all github ` +
'status checks. Please make sure this commit passes all checks before re-running.' ) ) ;
error ( ` Please have a look at: ${ branchCommitsUrl } ` ) ;
if ( yield promptConfirm ( 'Do you want to ignore the Github status and proceed?' ) ) {
info ( yellow ( ' ⚠ Upstream commit is failing CI checks, but status has been forcibly ignored.' ) ) ;
return ;
}
throw new UserAbortedReleaseActionError ( ) ;
}
else if ( state === 'pending' ) {
error ( red ( ` ✘ Commit " ${ commitSha } " still has pending github statuses that ` +
'need to succeed before staging a release.' ) ) ;
error ( red ( ` Please have a look at: ${ branchCommitsUrl } ` ) ) ;
if ( yield promptConfirm ( 'Do you want to ignore the Github status and proceed?' ) ) {
info ( yellow ( ' ⚠ Upstream commit is pending CI, but status has been forcibly ignored.' ) ) ;
return ;
}
throw new UserAbortedReleaseActionError ( ) ;
}
info ( green ( ' ✓ Upstream commit is passing all github status checks.' ) ) ;
} ) ;
}
/ * *
* Prompts the user for potential release notes edits that need to be made . Once
* confirmed , a new commit for the release point is created .
* /
waitForEditsAndCreateReleaseCommit ( newVersion ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
info ( yellow ( ' ⚠ Please review the changelog and ensure that the log contains only changes ' +
'that apply to the public API surface. Manual changes can be made. When done, please ' +
'proceed with the prompt below.' ) ) ;
if ( ! ( yield promptConfirm ( 'Do you want to proceed and commit the changes?' ) ) ) {
throw new UserAbortedReleaseActionError ( ) ;
}
// Commit message for the release point.
const commitMessage = getCommitMessageForRelease ( newVersion ) ;
// Create a release staging commit including changelog and version bump.
yield this . createCommit ( commitMessage , [ packageJsonPath , changelogPath ] ) ;
info ( green ( ` ✓ Created release commit for: " ${ newVersion } ". ` ) ) ;
} ) ;
}
/ * *
* Gets an owned fork for the configured project of the authenticated user . Aborts the
* process with an error if no fork could be found . Also caches the determined fork
* repository as the authenticated user cannot change during action execution .
* /
_getForkOfAuthenticatedUser ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
if ( this . _cachedForkRepo !== null ) {
return this . _cachedForkRepo ;
}
const { owner , name } = this . git . remoteConfig ;
2021-04-08 15:34:55 -04:00
const result = yield this . git . github . graphql ( findOwnedForksOfRepoQuery , { owner , name } ) ;
2020-10-01 19:06:56 -04:00
const forks = result . repository . forks . nodes ;
if ( forks . length === 0 ) {
error ( red ( ' ✘ Unable to find fork for currently authenticated user.' ) ) ;
error ( red ( ` Please ensure you created a fork of: ${ owner } / ${ name } . ` ) ) ;
throw new FatalReleaseActionError ( ) ;
}
const fork = forks [ 0 ] ;
return this . _cachedForkRepo = { owner : fork . owner . login , name : fork . name } ;
} ) ;
}
/** Checks whether a given branch name is reserved in the specified repository. */
_isBranchNameReservedInRepo ( repo , name ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
try {
yield this . git . github . repos . getBranch ( { owner : repo . owner , repo : repo . name , branch : name } ) ;
return true ;
}
catch ( e ) {
// If the error has a `status` property set to `404`, then we know that the branch
// does not exist. Otherwise, it might be an API error that we want to report/re-throw.
if ( e . status === 404 ) {
return false ;
}
throw e ;
}
} ) ;
}
/** Finds a non-reserved branch name in the repository with respect to a base name. */
_findAvailableBranchName ( repo , baseName ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
let currentName = baseName ;
let suffixNum = 0 ;
while ( yield this . _isBranchNameReservedInRepo ( repo , currentName ) ) {
suffixNum ++ ;
currentName = ` ${ baseName } _ ${ suffixNum } ` ;
}
return currentName ;
} ) ;
}
/ * *
* Creates a local branch from the current Git ` HEAD ` . Will override
* existing branches in case of a collision .
* /
createLocalBranchFromHead ( branchName ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
this . git . run ( [ 'checkout' , '-B' , branchName ] ) ;
} ) ;
}
/** Pushes the current Git `HEAD` to the given remote branch in the configured project. */
pushHeadToRemoteBranch ( branchName ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
// Push the local `HEAD` to the remote branch in the configured project.
2021-04-08 15:34:55 -04:00
this . git . run ( [ 'push' , this . git . getRepoGitUrl ( ) , ` HEAD:refs/heads/ ${ branchName } ` ] ) ;
2020-10-01 19:06:56 -04:00
} ) ;
}
/ * *
* Pushes the current Git ` HEAD ` to a fork for the configured project that is owned by
* the authenticated user . If the specified branch name exists in the fork already , a
* unique one will be generated based on the proposed name to avoid collisions .
* @ param proposedBranchName Proposed branch name for the fork .
* @ param trackLocalBranch Whether the fork branch should be tracked locally . i . e . whether
* a local branch with remote tracking should be set up .
* @ returns The fork and branch name containing the pushed changes .
* /
_pushHeadToFork ( proposedBranchName , trackLocalBranch ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const fork = yield this . _getForkOfAuthenticatedUser ( ) ;
// Compute a repository URL for pushing to the fork. Note that we want to respect
// the SSH option from the dev-infra github configuration.
const repoGitUrl = getRepositoryGitUrl ( Object . assign ( Object . assign ( { } , fork ) , { useSsh : this . git . remoteConfig . useSsh } ) , this . git . githubToken ) ;
const branchName = yield this . _findAvailableBranchName ( fork , proposedBranchName ) ;
const pushArgs = [ ] ;
// If a local branch should track the remote fork branch, create a branch matching
// the remote branch. Later with the `git push`, the remote is set for the branch.
if ( trackLocalBranch ) {
yield this . createLocalBranchFromHead ( branchName ) ;
pushArgs . push ( '--set-upstream' ) ;
}
// Push the local `HEAD` to the remote branch in the fork.
this . git . run ( [ 'push' , repoGitUrl , ` HEAD:refs/heads/ ${ branchName } ` , ... pushArgs ] ) ;
return { fork , branchName } ;
} ) ;
}
/ * *
* Pushes changes to a fork for the configured project that is owned by the currently
* authenticated user . A pull request is then created for the pushed changes on the
* configured project that targets the specified target branch .
* @ returns An object describing the created pull request .
* /
pushChangesToForkAndCreatePullRequest ( targetBranch , proposedForkBranchName , title , body ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const repoSlug = ` ${ this . git . remoteParams . owner } / ${ this . git . remoteParams . repo } ` ;
const { fork , branchName } = yield this . _pushHeadToFork ( proposedForkBranchName , true ) ;
const { data } = yield this . git . github . pulls . create ( Object . assign ( Object . assign ( { } , this . git . remoteParams ) , { head : ` ${ fork . owner } : ${ branchName } ` , base : targetBranch , body ,
title } ) ) ;
2021-01-19 15:37:12 -05:00
// Add labels to the newly created PR if provided in the configuration.
if ( this . config . releasePrLabels !== undefined ) {
yield this . git . github . issues . addLabels ( Object . assign ( Object . assign ( { } , this . git . remoteParams ) , { issue _number : data . number , labels : this . config . releasePrLabels } ) ) ;
}
2020-10-01 19:06:56 -04:00
info ( green ( ` ✓ Created pull request # ${ data . number } in ${ repoSlug } . ` ) ) ;
return {
id : data . number ,
url : data . html _url ,
fork ,
forkBranch : branchName ,
} ;
} ) ;
}
/ * *
* Waits for the given pull request to be merged . Default interval for checking the Github
* API is 10 seconds ( to not exceed any rate limits ) . If the pull request is closed without
* merge , the script will abort gracefully ( considering a manual user abort ) .
* /
waitForPullRequestToBeMerged ( id , interval = waitForPullRequestInterval ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
return new Promise ( ( resolve , reject ) => {
debug ( ` Waiting for pull request # ${ id } to be merged. ` ) ;
const spinner = ora . call ( undefined ) . start ( ` Waiting for pull request # ${ id } to be merged. ` ) ;
const intervalId = setInterval ( ( ) => tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const prState = yield getPullRequestState ( this . git , id ) ;
if ( prState === 'merged' ) {
spinner . stop ( ) ;
info ( green ( ` ✓ Pull request # ${ id } has been merged. ` ) ) ;
clearInterval ( intervalId ) ;
resolve ( ) ;
}
else if ( prState === 'closed' ) {
spinner . stop ( ) ;
warn ( yellow ( ` ✘ Pull request # ${ id } has been closed. ` ) ) ;
clearInterval ( intervalId ) ;
reject ( new UserAbortedReleaseActionError ( ) ) ;
}
} ) , interval ) ;
} ) ;
} ) ;
}
/ * *
* Prepend releases notes for a version published in a given branch to the changelog in
* the current Git ` HEAD ` . This is useful for cherry - picking the changelog .
* @ returns A boolean indicating whether the release notes have been prepended .
* /
2021-04-19 14:58:32 -04:00
prependReleaseNotesToChangelog ( releaseNotes ) {
2020-10-01 19:06:56 -04:00
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const localChangelogPath = getLocalChangelogFilePath ( this . projectDir ) ;
const localChangelog = yield fs . promises . readFile ( localChangelogPath , 'utf8' ) ;
2021-04-19 14:58:32 -04:00
const releaseNotesEntry = yield releaseNotes . getChangelogEntry ( ) ;
yield fs . promises . writeFile ( localChangelogPath , ` ${ releaseNotesEntry } \n \n ${ localChangelog } ` ) ;
info ( green ( ` ✓ Updated the changelog to capture changes for " ${ releaseNotes . version } ". ` ) ) ;
2020-10-01 19:06:56 -04:00
} ) ;
}
/** Checks out an upstream branch with a detached head. */
checkoutUpstreamBranch ( branchName ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
2021-04-08 15:34:55 -04:00
this . git . run ( [ 'fetch' , '-q' , this . git . getRepoGitUrl ( ) , branchName ] ) ;
2020-10-01 19:06:56 -04:00
this . git . run ( [ 'checkout' , 'FETCH_HEAD' , '--detach' ] ) ;
} ) ;
}
/ * *
* Creates a commit for the specified files with the given message .
* @ param message Message for the created commit
* @ param files List of project - relative file paths to be commited .
* /
createCommit ( message , files ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
this . git . run ( [ 'commit' , '--no-verify' , '-m' , message , ... files ] ) ;
} ) ;
}
/ * *
* Stages the specified new version for the current branch and creates a
* pull request that targets the given base branch .
* @ returns an object describing the created pull request .
* /
stageVersionForBranchAndCreatePullRequest ( newVersion , pullRequestBaseBranch ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
2021-05-06 15:30:35 -04:00
const releaseNotes = yield ReleaseNotes . fromRange ( newVersion , this . git . getLatestSemverTag ( ) . format ( ) , 'HEAD' ) ;
2020-10-01 19:06:56 -04:00
yield this . updateProjectVersion ( newVersion ) ;
2021-04-19 14:58:32 -04:00
yield this . prependReleaseNotesToChangelog ( releaseNotes ) ;
2020-10-01 19:06:56 -04:00
yield this . waitForEditsAndCreateReleaseCommit ( newVersion ) ;
const pullRequest = yield this . pushChangesToForkAndCreatePullRequest ( pullRequestBaseBranch , ` release-stage- ${ newVersion } ` , ` Bump version to "v ${ newVersion } " with changelog. ` ) ;
info ( green ( ' ✓ Release staging pull request has been created.' ) ) ;
info ( yellow ( ` Please ask team members to review: ${ pullRequest . url } . ` ) ) ;
2021-04-19 14:58:32 -04:00
return { releaseNotes , pullRequest } ;
2020-10-01 19:06:56 -04:00
} ) ;
}
/ * *
* Checks out the specified target branch , verifies its CI status and stages
* the specified new version in order to create a pull request .
* @ returns an object describing the created pull request .
* /
checkoutBranchAndStageVersion ( newVersion , stagingBranch ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
yield this . verifyPassingGithubStatus ( stagingBranch ) ;
yield this . checkoutUpstreamBranch ( stagingBranch ) ;
return yield this . stageVersionForBranchAndCreatePullRequest ( newVersion , stagingBranch ) ;
} ) ;
}
/ * *
* Cherry - picks the release notes of a version that have been pushed to a given branch
* into the ` next ` primary development branch . A pull request is created for this .
* @ returns a boolean indicating successful creation of the cherry - pick pull request .
* /
2021-04-19 14:58:32 -04:00
cherryPickChangelogIntoNextBranch ( releaseNotes , stagingBranch ) {
2020-10-01 19:06:56 -04:00
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const nextBranch = this . active . next . branchName ;
2021-04-19 14:58:32 -04:00
const commitMessage = getReleaseNoteCherryPickCommitMessage ( releaseNotes . version ) ;
2020-10-01 19:06:56 -04:00
// Checkout the next branch.
yield this . checkoutUpstreamBranch ( nextBranch ) ;
2021-04-19 14:58:32 -04:00
yield this . prependReleaseNotesToChangelog ( releaseNotes ) ;
// Create a changelog cherry-pick commit.
yield this . createCommit ( commitMessage , [ changelogPath ] ) ;
info ( green ( ` ✓ Created changelog cherry-pick commit for: " ${ releaseNotes . version } ". ` ) ) ;
2020-10-01 19:06:56 -04:00
// Create a cherry-pick pull request that should be merged by the caretaker.
2021-04-19 14:58:32 -04:00
const { url , id } = yield this . pushChangesToForkAndCreatePullRequest ( nextBranch , ` changelog-cherry-pick- ${ releaseNotes . version } ` , commitMessage , ` Cherry-picks the changelog from the " ${ stagingBranch } " branch to the next ` +
2020-10-01 19:06:56 -04:00
` branch ( ${ nextBranch } ). ` ) ;
info ( green ( ` ✓ Pull request for cherry-picking the changelog into " ${ nextBranch } " ` +
'has been created.' ) ) ;
info ( yellow ( ` Please ask team members to review: ${ url } . ` ) ) ;
2021-01-27 16:30:04 -05:00
// Wait for the Pull Request to be merged.
yield this . waitForPullRequestToBeMerged ( id ) ;
2020-10-01 19:06:56 -04:00
return true ;
} ) ;
}
/ * *
* Creates a Github release for the specified version in the configured project .
* The release is created by tagging the specified commit SHA .
* /
2021-05-05 12:11:05 -04:00
_createGithubReleaseForVersion ( releaseNotes , versionBumpCommitSha , prerelease ) {
2020-10-01 19:06:56 -04:00
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
2021-05-05 12:11:05 -04:00
const tagName = releaseNotes . version . format ( ) ;
2020-10-01 19:06:56 -04:00
yield this . git . github . git . createRef ( Object . assign ( Object . assign ( { } , this . git . remoteParams ) , { ref : ` refs/tags/ ${ tagName } ` , sha : versionBumpCommitSha } ) ) ;
2021-05-05 12:11:05 -04:00
info ( green ( ` ✓ Tagged v ${ releaseNotes . version } release upstream. ` ) ) ;
yield this . git . github . repos . createRelease ( Object . assign ( Object . assign ( { } , this . git . remoteParams ) , { name : ` v ${ releaseNotes . version } ` , tag _name : tagName , prerelease , body : yield releaseNotes . getGithubReleaseEntry ( ) } ) ) ;
info ( green ( ` ✓ Created v ${ releaseNotes . version } release in Github. ` ) ) ;
2020-10-01 19:06:56 -04:00
} ) ;
}
/ * *
* Builds and publishes the given version in the specified branch .
* @ param newVersion The new version to be published .
* @ param publishBranch Name of the branch that contains the new version .
* @ param npmDistTag NPM dist tag where the version should be published to .
* /
2021-05-05 12:11:05 -04:00
buildAndPublish ( releaseNotes , publishBranch , npmDistTag ) {
2020-10-01 19:06:56 -04:00
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const versionBumpCommitSha = yield this . _getCommitOfBranch ( publishBranch ) ;
2021-05-05 12:11:05 -04:00
if ( ! ( yield this . _isCommitForVersionStaging ( releaseNotes . version , versionBumpCommitSha ) ) ) {
2020-10-01 19:06:56 -04:00
error ( red ( ` ✘ Latest commit in " ${ publishBranch } " branch is not a staging commit. ` ) ) ;
error ( red ( ' Please make sure the staging pull request has been merged.' ) ) ;
throw new FatalReleaseActionError ( ) ;
}
// Checkout the publish branch and build the release packages.
yield this . checkoutUpstreamBranch ( publishBranch ) ;
// Install the project dependencies for the publish branch, and then build the release
// packages. Note that we do not directly call the build packages function from the release
// config. We only want to build and publish packages that have been configured in the given
// publish branch. e.g. consider we publish patch version and a new package has been
// created in the `next` branch. The new package would not be part of the patch branch,
// so we cannot build and publish it.
yield invokeYarnInstallCommand ( this . projectDir ) ;
const builtPackages = yield invokeReleaseBuildCommand ( ) ;
2020-11-20 14:05:40 -05:00
// Verify the packages built are the correct version.
2021-05-05 12:11:05 -04:00
yield this . _verifyPackageVersions ( releaseNotes . version , builtPackages ) ;
2020-10-01 19:06:56 -04:00
// Create a Github release for the new version.
2021-05-05 12:11:05 -04:00
yield this . _createGithubReleaseForVersion ( releaseNotes , versionBumpCommitSha , npmDistTag === 'next' ) ;
2020-10-01 19:06:56 -04:00
// Walk through all built packages and publish them to NPM.
for ( const builtPackage of builtPackages ) {
yield this . _publishBuiltPackageToNpm ( builtPackage , npmDistTag ) ;
}
info ( green ( ' ✓ Published all packages successfully' ) ) ;
} ) ;
}
/** Publishes the given built package to NPM with the specified NPM dist tag. */
_publishBuiltPackageToNpm ( pkg , npmDistTag ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
debug ( ` Starting publish of " ${ pkg . name } ". ` ) ;
const spinner = ora . call ( undefined ) . start ( ` Publishing " ${ pkg . name } " ` ) ;
try {
yield runNpmPublish ( pkg . outputPath , npmDistTag , this . config . publishRegistry ) ;
spinner . stop ( ) ;
info ( green ( ` ✓ Successfully published " ${ pkg . name } . ` ) ) ;
}
catch ( e ) {
spinner . stop ( ) ;
error ( e ) ;
error ( red ( ` ✘ An error occurred while publishing " ${ pkg . name } ". ` ) ) ;
throw new FatalReleaseActionError ( ) ;
}
} ) ;
}
/** Checks whether the given commit represents a staging commit for the specified version. */
_isCommitForVersionStaging ( version , commitSha ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const { data } = yield this . git . github . repos . getCommit ( Object . assign ( Object . assign ( { } , this . git . remoteParams ) , { ref : commitSha } ) ) ;
return data . commit . message . startsWith ( getCommitMessageForRelease ( version ) ) ;
} ) ;
}
2020-11-20 14:05:40 -05:00
/** Verify the version of each generated package exact matches the specified version. */
_verifyPackageVersions ( version , packages ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
for ( const pkg of packages ) {
const { version : packageJsonVersion } = JSON . parse ( yield fs . promises . readFile ( path . join ( pkg . outputPath , 'package.json' ) , 'utf8' ) ) ;
if ( version . compare ( packageJsonVersion ) !== 0 ) {
error ( red ( 'The built package version does not match the version being released.' ) ) ;
error ( ` Release Version: ${ version . version } ` ) ;
error ( ` Generated Version: ${ packageJsonVersion } ` ) ;
throw new FatalReleaseActionError ( ) ;
}
}
} ) ;
}
2020-10-01 19:06:56 -04:00
}
/ * *
* @ 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
* /
/ * *
* Release action that cuts a new patch release for an active release - train in the long - term
* support phase . The patch segment is incremented . The changelog is generated for the new
* patch version , but also needs to be cherry - picked into the next development branch .
* /
class CutLongTermSupportPatchAction extends ReleaseAction {
constructor ( ) {
super ( ... arguments ) ;
/** Promise resolving an object describing long-term support branches. */
this . ltsBranches = fetchLongTermSupportBranchesFromNpm ( this . config ) ;
}
getDescription ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const { active } = yield this . ltsBranches ;
return ` Cut a new release for an active LTS branch ( ${ active . length } active). ` ;
} ) ;
}
perform ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const ltsBranch = yield this . _promptForTargetLtsBranch ( ) ;
const newVersion = semverInc ( ltsBranch . version , 'patch' ) ;
2021-04-19 14:58:32 -04:00
const { pullRequest : { id } , releaseNotes } = yield this . checkoutBranchAndStageVersion ( newVersion , ltsBranch . name ) ;
2020-10-01 19:06:56 -04:00
yield this . waitForPullRequestToBeMerged ( id ) ;
2021-05-05 12:11:05 -04:00
yield this . buildAndPublish ( releaseNotes , ltsBranch . name , ltsBranch . npmDistTag ) ;
2021-04-19 14:58:32 -04:00
yield this . cherryPickChangelogIntoNextBranch ( releaseNotes , ltsBranch . name ) ;
2020-10-01 19:06:56 -04:00
} ) ;
}
/** Prompts the user to select an LTS branch for which a patch should but cut. */
_promptForTargetLtsBranch ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const { active , inactive } = yield this . ltsBranches ;
const activeBranchChoices = active . map ( branch => this . _getChoiceForLtsBranch ( branch ) ) ;
// If there are inactive LTS branches, we allow them to be selected. In some situations,
// patch releases are still cut for inactive LTS branches. e.g. when the LTS duration
// has been increased due to exceptional events ()
if ( inactive . length !== 0 ) {
activeBranchChoices . push ( { name : 'Inactive LTS versions (not recommended)' , value : null } ) ;
}
const { activeLtsBranch , inactiveLtsBranch } = yield inquirer . prompt ( [
{
name : 'activeLtsBranch' ,
type : 'list' ,
message : 'Please select a version for which you want to cut an LTS patch' ,
choices : activeBranchChoices ,
} ,
{
name : 'inactiveLtsBranch' ,
type : 'list' ,
when : o => o . activeLtsBranch === null ,
message : 'Please select an inactive LTS version for which you want to cut an LTS patch' ,
choices : inactive . map ( branch => this . _getChoiceForLtsBranch ( branch ) ) ,
}
] ) ;
return activeLtsBranch !== null && activeLtsBranch !== void 0 ? activeLtsBranch : inactiveLtsBranch ;
} ) ;
}
/** Gets an inquirer choice for the given LTS branch. */
_getChoiceForLtsBranch ( branch ) {
return { name : ` v ${ branch . version . major } (from ${ branch . name } ) ` , value : branch } ;
}
static isActive ( active ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
// LTS patch versions can be only cut if there are release trains in LTS phase.
// This action is always selectable as we support publishing of old LTS branches,
// and have prompt for selecting an LTS branch when the action performs.
return true ;
} ) ;
}
}
/ * *
* @ 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
* /
/ * *
* Release action that cuts a new patch release for the current latest release - train version
* branch ( i . e . the patch branch ) . The patch segment is incremented . The changelog is generated
* for the new patch version , but also needs to be cherry - picked into the next development branch .
* /
class CutNewPatchAction extends ReleaseAction {
constructor ( ) {
super ( ... arguments ) ;
this . _newVersion = semverInc ( this . active . latest . version , 'patch' ) ;
}
getDescription ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const { branchName } = this . active . latest ;
const newVersion = this . _newVersion ;
return ` Cut a new patch release for the " ${ branchName } " branch (v ${ newVersion } ). ` ;
} ) ;
}
perform ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const { branchName } = this . active . latest ;
const newVersion = this . _newVersion ;
2021-04-19 14:58:32 -04:00
const { pullRequest : { id } , releaseNotes } = yield this . checkoutBranchAndStageVersion ( newVersion , branchName ) ;
2020-10-01 19:06:56 -04:00
yield this . waitForPullRequestToBeMerged ( id ) ;
2021-05-05 12:11:05 -04:00
yield this . buildAndPublish ( releaseNotes , branchName , 'latest' ) ;
2021-04-19 14:58:32 -04:00
yield this . cherryPickChangelogIntoNextBranch ( releaseNotes , branchName ) ;
2020-10-01 19:06:56 -04:00
} ) ;
}
static isActive ( active ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
// Patch versions can be cut at any time. See:
// https://hackmd.io/2Le8leq0S6G_R5VEVTNK9A#Release-prompt-options.
return true ;
} ) ;
}
}
/ * *
* @ 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
* /
/** Computes the new pre-release version for the next release-train. */
function computeNewPrereleaseVersionForNext ( active , config ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const { version : nextVersion } = active . next ;
const isNextPublishedToNpm = yield isVersionPublishedToNpm ( nextVersion , config ) ;
// Special-case where the version in the `next` release-train is not published yet. This
// happens when we recently branched off for feature-freeze. We already bump the version to
// the next minor or major, but do not publish immediately. Cutting a release immediately would
// be not helpful as there are no other changes than in the feature-freeze branch. If we happen
// to detect this case, we stage the release as usual but do not increment the version.
if ( isNextPublishedToNpm ) {
return semverInc ( nextVersion , 'prerelease' ) ;
}
else {
return nextVersion ;
}
} ) ;
}
/ * *
* @ 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
* /
/ * *
* Release action that cuts a prerelease for the next branch . A version in the next
* branch can have an arbitrary amount of next pre - releases .
* /
class CutNextPrereleaseAction extends ReleaseAction {
constructor ( ) {
super ( ... arguments ) ;
/** Promise resolving with the new version if a NPM next pre-release is cut. */
this . _newVersion = this . _computeNewVersion ( ) ;
}
getDescription ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const { branchName } = this . _getActivePrereleaseTrain ( ) ;
const newVersion = yield this . _newVersion ;
return ` Cut a new next pre-release for the " ${ branchName } " branch (v ${ newVersion } ). ` ;
} ) ;
}
perform ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const releaseTrain = this . _getActivePrereleaseTrain ( ) ;
const { branchName } = releaseTrain ;
const newVersion = yield this . _newVersion ;
2021-04-19 14:58:32 -04:00
const { pullRequest : { id } , releaseNotes } = yield this . checkoutBranchAndStageVersion ( newVersion , branchName ) ;
2020-10-01 19:06:56 -04:00
yield this . waitForPullRequestToBeMerged ( id ) ;
2021-05-05 12:11:05 -04:00
yield this . buildAndPublish ( releaseNotes , branchName , 'next' ) ;
2020-10-01 19:06:56 -04:00
// If the pre-release has been cut from a branch that is not corresponding
// to the next release-train, cherry-pick the changelog into the primary
// development branch. i.e. the `next` branch that is usually `master`.
if ( releaseTrain !== this . active . next ) {
2021-04-19 14:58:32 -04:00
yield this . cherryPickChangelogIntoNextBranch ( releaseNotes , branchName ) ;
2020-10-01 19:06:56 -04:00
}
} ) ;
}
/** Gets the release train for which NPM next pre-releases should be cut. */
_getActivePrereleaseTrain ( ) {
var _a ;
return ( _a = this . active . releaseCandidate ) !== null && _a !== void 0 ? _a : this . active . next ;
}
/** Gets the new pre-release version for this release action. */
_computeNewVersion ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const releaseTrain = this . _getActivePrereleaseTrain ( ) ;
// If a pre-release is cut for the next release-train, the new version is computed
// with respect to special cases surfacing with FF/RC branches. Otherwise, the basic
// pre-release increment of the version is used as new version.
if ( releaseTrain === this . active . next ) {
return yield computeNewPrereleaseVersionForNext ( this . active , this . config ) ;
}
else {
return semverInc ( releaseTrain . version , 'prerelease' ) ;
}
} ) ;
}
static isActive ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
// Pre-releases for the `next` NPM dist tag can always be cut. Depending on whether
// there is a feature-freeze/release-candidate branch, the next pre-releases are either
// cut from such a branch, or from the actual `next` release-train branch (i.e. master).
return true ;
} ) ;
}
}
/ * *
* @ 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
* /
/ * *
* Cuts the first release candidate for a release - train currently in the
* feature - freeze phase . The version is bumped from ` next ` to ` rc.0 ` .
* /
class CutReleaseCandidateAction extends ReleaseAction {
constructor ( ) {
super ( ... arguments ) ;
this . _newVersion = semverInc ( this . active . releaseCandidate . version , 'prerelease' , 'rc' ) ;
}
getDescription ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const newVersion = this . _newVersion ;
return ` Cut a first release-candidate for the feature-freeze branch (v ${ newVersion } ). ` ;
} ) ;
}
perform ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const { branchName } = this . active . releaseCandidate ;
const newVersion = this . _newVersion ;
2021-04-19 14:58:32 -04:00
const { pullRequest : { id } , releaseNotes } = yield this . checkoutBranchAndStageVersion ( newVersion , branchName ) ;
2020-10-01 19:06:56 -04:00
yield this . waitForPullRequestToBeMerged ( id ) ;
2021-05-05 12:11:05 -04:00
yield this . buildAndPublish ( releaseNotes , branchName , 'next' ) ;
2021-04-19 14:58:32 -04:00
yield this . cherryPickChangelogIntoNextBranch ( releaseNotes , branchName ) ;
2020-10-01 19:06:56 -04:00
} ) ;
}
static isActive ( active ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
// A release-candidate can be cut for an active release-train currently
// in the feature-freeze phase.
return active . releaseCandidate !== null &&
active . releaseCandidate . version . prerelease [ 0 ] === 'next' ;
} ) ;
}
}
/ * *
* @ 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
* /
/ * *
* Release action that cuts a stable version for the current release - train in the release
* candidate phase . The pre - release release - candidate version label is removed .
* /
class CutStableAction extends ReleaseAction {
constructor ( ) {
super ( ... arguments ) ;
this . _newVersion = this . _computeNewVersion ( ) ;
}
getDescription ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const newVersion = this . _newVersion ;
return ` Cut a stable release for the release-candidate branch (v ${ newVersion } ). ` ;
} ) ;
}
perform ( ) {
var _a ;
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const { branchName } = this . active . releaseCandidate ;
const newVersion = this . _newVersion ;
const isNewMajor = ( _a = this . active . releaseCandidate ) === null || _a === void 0 ? void 0 : _a . isMajor ;
2021-04-19 14:58:32 -04:00
const { pullRequest : { id } , releaseNotes } = yield this . checkoutBranchAndStageVersion ( newVersion , branchName ) ;
2020-10-01 19:06:56 -04:00
yield this . waitForPullRequestToBeMerged ( id ) ;
2021-05-05 12:11:05 -04:00
yield this . buildAndPublish ( releaseNotes , branchName , 'latest' ) ;
2020-10-01 19:06:56 -04:00
// If a new major version is published and becomes the "latest" release-train, we need
// to set the LTS npm dist tag for the previous latest release-train (the current patch).
if ( isNewMajor ) {
2021-05-04 11:09:51 -04:00
const previousPatch = this . active . latest ;
const ltsTagForPatch = getLtsNpmDistTagOfMajor ( previousPatch . version . major ) ;
2020-10-01 19:06:56 -04:00
// Instead of directly setting the NPM dist tags, we invoke the ng-dev command for
// setting the NPM dist tag to the specified version. We do this because release NPM
// packages could be different in the previous patch branch, and we want to set the
// LTS tag for all packages part of the last major. It would not be possible to set the
// NPM dist tag for new packages part of the released major, nor would it be acceptable
// to skip the LTS tag for packages which are no longer part of the new major.
2021-05-04 11:09:51 -04:00
yield this . checkoutUpstreamBranch ( previousPatch . branchName ) ;
2020-10-01 19:06:56 -04:00
yield invokeYarnInstallCommand ( this . projectDir ) ;
2021-05-04 11:09:51 -04:00
yield invokeSetNpmDistCommand ( ltsTagForPatch , previousPatch . version ) ;
2020-10-01 19:06:56 -04:00
}
2021-04-19 14:58:32 -04:00
yield this . cherryPickChangelogIntoNextBranch ( releaseNotes , branchName ) ;
2020-10-01 19:06:56 -04:00
} ) ;
}
/** Gets the new stable version of the release candidate release-train. */
_computeNewVersion ( ) {
const { version } = this . active . releaseCandidate ;
return semver . parse ( ` ${ version . major } . ${ version . minor } . ${ version . patch } ` ) ;
}
static isActive ( active ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
// A stable version can be cut for an active release-train currently in the
// release-candidate phase. Note: It is not possible to directly release from
// feature-freeze phase into a stable version.
return active . releaseCandidate !== null &&
active . releaseCandidate . version . prerelease [ 0 ] === 'rc' ;
} ) ;
}
}
/ * *
* @ 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
* /
/ * *
* Release action that moves the next release - train into the feature - freeze phase . This means
* that a new version branch is created from the next branch , and a new next pre - release is
* cut indicating the started feature - freeze .
* /
class MoveNextIntoFeatureFreezeAction extends ReleaseAction {
constructor ( ) {
super ( ... arguments ) ;
this . _newVersion = computeNewPrereleaseVersionForNext ( this . active , this . config ) ;
}
getDescription ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const { branchName } = this . active . next ;
const newVersion = yield this . _newVersion ;
return ` Move the " ${ branchName } " branch into feature-freeze phase (v ${ newVersion } ). ` ;
} ) ;
}
perform ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const newVersion = yield this . _newVersion ;
const newBranch = ` ${ newVersion . major } . ${ newVersion . minor } .x ` ;
// Branch-off the next branch into a feature-freeze branch.
yield this . _createNewVersionBranchFromNext ( newBranch ) ;
// Stage the new version for the newly created branch, and push changes to a
// fork in order to create a staging pull request. Note that we re-use the newly
// created branch instead of re-fetching from the upstream.
2021-04-19 14:58:32 -04:00
const { pullRequest : { id } , releaseNotes } = yield this . stageVersionForBranchAndCreatePullRequest ( newVersion , newBranch ) ;
2020-10-01 19:06:56 -04:00
// Wait for the staging PR to be merged. Then build and publish the feature-freeze next
// pre-release. Finally, cherry-pick the release notes into the next branch in combination
// with bumping the version to the next minor too.
2021-04-19 14:58:32 -04:00
yield this . waitForPullRequestToBeMerged ( id ) ;
2021-05-05 12:11:05 -04:00
yield this . buildAndPublish ( releaseNotes , newBranch , 'next' ) ;
2021-04-19 14:58:32 -04:00
yield this . _createNextBranchUpdatePullRequest ( releaseNotes , newVersion ) ;
2020-10-01 19:06:56 -04:00
} ) ;
}
/** Creates a new version branch from the next branch. */
_createNewVersionBranchFromNext ( newBranch ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const { branchName : nextBranch } = this . active . next ;
yield this . verifyPassingGithubStatus ( nextBranch ) ;
yield this . checkoutUpstreamBranch ( nextBranch ) ;
yield this . createLocalBranchFromHead ( newBranch ) ;
yield this . pushHeadToRemoteBranch ( newBranch ) ;
info ( green ( ` ✓ Version branch " ${ newBranch } " created. ` ) ) ;
} ) ;
}
/ * *
* Creates a pull request for the next branch that bumps the version to the next
* minor , and cherry - picks the changelog for the newly branched - off feature - freeze version .
* /
2021-04-19 14:58:32 -04:00
_createNextBranchUpdatePullRequest ( releaseNotes , newVersion ) {
2020-10-01 19:06:56 -04:00
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const { branchName : nextBranch , version } = this . active . next ;
// We increase the version for the next branch to the next minor. The team can decide
// later if they want next to be a major through the `Configure Next as Major` release action.
const newNextVersion = semver . parse ( ` ${ version . major } . ${ version . minor + 1 } .0-next.0 ` ) ;
const bumpCommitMessage = getCommitMessageForExceptionalNextVersionBump ( newNextVersion ) ;
yield this . checkoutUpstreamBranch ( nextBranch ) ;
yield this . updateProjectVersion ( newNextVersion ) ;
// Create an individual commit for the next version bump. The changelog should go into
// a separate commit that makes it clear where the changelog is cherry-picked from.
yield this . createCommit ( bumpCommitMessage , [ packageJsonPath ] ) ;
2021-04-19 14:58:32 -04:00
yield this . prependReleaseNotesToChangelog ( releaseNotes ) ;
const commitMessage = getReleaseNoteCherryPickCommitMessage ( releaseNotes . version ) ;
yield this . createCommit ( commitMessage , [ changelogPath ] ) ;
2020-10-01 19:06:56 -04:00
let nextPullRequestMessage = ` The previous "next" release-train has moved into the ` +
` release-candidate phase. This PR updates the next branch to the subsequent ` +
2021-04-19 14:58:32 -04:00
` release-train. \n \n Also this PR cherry-picks the changelog for ` +
` v ${ newVersion } into the ${ nextBranch } branch so that the changelog is up to date. ` ;
2020-10-01 19:06:56 -04:00
const nextUpdatePullRequest = yield this . pushChangesToForkAndCreatePullRequest ( nextBranch , ` next-release-train- ${ newNextVersion } ` , ` Update next branch to reflect new release-train "v ${ newNextVersion } ". ` , nextPullRequestMessage ) ;
info ( green ( ` ✓ Pull request for updating the " ${ nextBranch } " branch has been created. ` ) ) ;
info ( yellow ( ` Please ask team members to review: ${ nextUpdatePullRequest . url } . ` ) ) ;
} ) ;
}
static isActive ( active ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
// A new feature-freeze/release-candidate branch can only be created if there
// is no active release-train in feature-freeze/release-candidate phase.
return active . releaseCandidate === null ;
} ) ;
}
}
/ * *
* @ 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
* /
/ * *
* List of release actions supported by the release staging tool . These are sorted
* by priority . Actions which are selectable are sorted based on this declaration order .
* /
const actions = [
CutStableAction ,
CutReleaseCandidateAction ,
CutNewPatchAction ,
CutNextPrereleaseAction ,
MoveNextIntoFeatureFreezeAction ,
CutLongTermSupportPatchAction ,
] ;
/ * *
* @ 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
* /
var CompletionState ;
( function ( CompletionState ) {
CompletionState [ CompletionState [ "SUCCESS" ] = 0 ] = "SUCCESS" ;
CompletionState [ CompletionState [ "FATAL_ERROR" ] = 1 ] = "FATAL_ERROR" ;
CompletionState [ CompletionState [ "MANUALLY_ABORTED" ] = 2 ] = "MANUALLY_ABORTED" ;
} ) ( CompletionState || ( CompletionState = { } ) ) ;
class ReleaseTool {
2021-04-18 05:46:57 -04:00
constructor ( _config , _github , _projectRoot ) {
2020-10-01 19:06:56 -04:00
this . _config = _config ;
this . _github = _github ;
this . _projectRoot = _projectRoot ;
2021-04-08 15:34:55 -04:00
/** The singleton instance of the GitClient. */
this . _git = GitClient . getAuthenticatedInstance ( ) ;
2021-01-19 11:29:46 -05:00
/** The previous git commit to return back to after the release tool runs. */
this . previousGitBranchOrRevision = this . _git . getCurrentBranchOrRevision ( ) ;
2020-10-01 19:06:56 -04:00
}
/** Runs the interactive release tool. */
run ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
log ( ) ;
log ( yellow ( '--------------------------------------------' ) ) ;
log ( yellow ( ' Angular Dev-Infra release staging script' ) ) ;
log ( yellow ( '--------------------------------------------' ) ) ;
log ( ) ;
2021-04-21 13:46:17 -04:00
if ( ! ( yield this . _verifyEnvironmentHasPython3Symlink ( ) ) ||
! ( yield this . _verifyNoUncommittedChanges ( ) ) || ! ( yield this . _verifyRunningFromNextBranch ( ) ) ) {
2020-10-01 19:06:56 -04:00
return CompletionState . FATAL _ERROR ;
}
2021-01-19 11:29:46 -05:00
if ( ! ( yield this . _verifyNpmLoginState ( ) ) ) {
return CompletionState . MANUALLY _ABORTED ;
}
2020-10-01 19:06:56 -04:00
const { owner , name } = this . _github ;
const repo = { owner , name , api : this . _git . github } ;
const releaseTrains = yield fetchActiveReleaseTrains ( repo ) ;
// Print the active release trains so that the caretaker can access
// the current project branching state without switching context.
yield printActiveReleaseTrains ( releaseTrains , this . _config ) ;
const action = yield this . _promptForReleaseAction ( releaseTrains ) ;
try {
yield action . perform ( ) ;
}
catch ( e ) {
if ( e instanceof UserAbortedReleaseActionError ) {
return CompletionState . MANUALLY _ABORTED ;
}
// Only print the error message and stack if the error is not a known fatal release
// action error (for which we print the error gracefully to the console with colors).
if ( ! ( e instanceof FatalReleaseActionError ) && e instanceof Error ) {
console . error ( e ) ;
}
return CompletionState . FATAL _ERROR ;
}
finally {
2021-01-19 11:29:46 -05:00
yield this . cleanup ( ) ;
2020-10-01 19:06:56 -04:00
}
return CompletionState . SUCCESS ;
} ) ;
}
2021-01-19 11:29:46 -05:00
/** Run post release tool cleanups. */
cleanup ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
// Return back to the git state from before the release tool ran.
this . _git . checkout ( this . previousGitBranchOrRevision , true ) ;
// Ensure log out of NPM.
yield npmLogout ( this . _config . publishRegistry ) ;
} ) ;
}
2020-10-01 19:06:56 -04:00
/** Prompts the caretaker for a release action that should be performed. */
_promptForReleaseAction ( activeTrains ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const choices = [ ] ;
// Find and instantiate all release actions which are currently valid.
for ( let actionType of actions ) {
if ( yield actionType . isActive ( activeTrains ) ) {
const action = new actionType ( activeTrains , this . _git , this . _config , this . _projectRoot ) ;
choices . push ( { name : yield action . getDescription ( ) , value : action } ) ;
}
}
info ( 'Please select the type of release you want to perform.' ) ;
const { releaseAction } = yield inquirer . prompt ( {
name : 'releaseAction' ,
message : 'Please select an action:' ,
type : 'list' ,
choices ,
} ) ;
return releaseAction ;
} ) ;
}
/ * *
* Verifies that there are no uncommitted changes in the project .
* @ returns a boolean indicating success or failure .
* /
_verifyNoUncommittedChanges ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
if ( this . _git . hasUncommittedChanges ( ) ) {
error ( red ( ' ✘ There are changes which are not committed and should be discarded.' ) ) ;
return false ;
}
return true ;
} ) ;
}
2021-04-21 13:46:17 -04:00
/ * *
2021-05-04 10:02:54 -04:00
* Verifies that Python can be resolved within scripts and points to a compatible version . Python
* is required in Bazel actions as there can be tools ( such as ` skydoc ` ) that rely on it .
2021-04-21 13:46:17 -04:00
* @ returns a boolean indicating success or failure .
* /
_verifyEnvironmentHasPython3Symlink ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
try {
2021-05-04 10:02:54 -04:00
// Note: We do not rely on `/usr/bin/env` but rather access the `env` binary directly as it
// should be part of the shell's `$PATH`. This is necessary for compatibility with Windows.
const pyVersion = yield spawnWithDebugOutput ( 'env' , [ 'python' , '--version' ] , { mode : 'silent' } ) ;
2021-04-21 13:46:17 -04:00
const version = pyVersion . stdout . trim ( ) || pyVersion . stderr . trim ( ) ;
if ( version . startsWith ( 'Python 3.' ) ) {
debug ( ` Local python version: ${ version } ` ) ;
return true ;
}
error ( red ( ` ✘ \` /usr/bin/python \` is currently symlinked to " ${ version } ", please update ` ) ) ;
error ( red ( ' the symlink to link instead to Python3' ) ) ;
error ( ) ;
error ( red ( ' Googlers: please run the following command to symlink python to python3:' ) ) ;
error ( red ( ' sudo ln -s /usr/bin/python3 /usr/bin/python' ) ) ;
return false ;
}
catch ( _a ) {
error ( red ( ' ✘ `/usr/bin/python` does not exist, please ensure `/usr/bin/python` is' ) ) ;
error ( red ( ' symlinked to Python3.' ) ) ;
error ( ) ;
error ( red ( ' Googlers: please run the following command to symlink python to python3:' ) ) ;
error ( red ( ' sudo ln -s /usr/bin/python3 /usr/bin/python' ) ) ;
}
return false ;
} ) ;
}
2020-10-01 19:06:56 -04:00
/ * *
* Verifies that the next branch from the configured repository is checked out .
* @ returns a boolean indicating success or failure .
* /
_verifyRunningFromNextBranch ( ) {
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const headSha = this . _git . run ( [ 'rev-parse' , 'HEAD' ] ) . stdout . trim ( ) ;
const { data } = yield this . _git . github . repos . getBranch ( Object . assign ( Object . assign ( { } , this . _git . remoteParams ) , { branch : nextBranchName } ) ) ;
if ( headSha !== data . commit . sha ) {
error ( red ( ' ✘ Running release tool from an outdated local branch.' ) ) ;
error ( red ( ` Please make sure you are running from the " ${ nextBranchName } " branch. ` ) ) ;
return false ;
}
return true ;
} ) ;
}
2021-01-19 11:29:46 -05:00
/ * *
* Verifies that the user is logged into NPM at the correct registry , if defined for the release .
* @ returns a boolean indicating whether the user is logged into NPM .
* /
_verifyNpmLoginState ( ) {
2021-04-01 18:34:14 -04:00
var _a , _b ;
2021-01-19 11:29:46 -05:00
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const registry = ` NPM at the ${ ( _a = this . _config . publishRegistry ) !== null && _a !== void 0 ? _a : 'default NPM' } registry ` ;
2021-04-01 18:34:14 -04:00
// TODO(josephperrott): remove wombat specific block once wombot allows `npm whoami` check to
// check the status of the local token in the .npmrc file.
if ( ( _b = this . _config . publishRegistry ) === null || _b === void 0 ? void 0 : _b . includes ( 'wombat-dressing-room.appspot.com' ) ) {
info ( 'Unable to determine NPM login state for wombat proxy, requiring login now.' ) ;
try {
yield npmLogin ( this . _config . publishRegistry ) ;
}
catch ( _c ) {
return false ;
}
return true ;
}
2021-01-19 11:29:46 -05:00
if ( yield npmIsLoggedIn ( this . _config . publishRegistry ) ) {
debug ( ` Already logged into ${ registry } . ` ) ;
return true ;
}
error ( red ( ` ✘ Not currently logged into ${ registry } . ` ) ) ;
const shouldLogin = yield promptConfirm ( 'Would you like to log into NPM now?' ) ;
if ( shouldLogin ) {
debug ( 'Starting NPM login.' ) ;
try {
yield npmLogin ( this . _config . publishRegistry ) ;
}
2021-04-01 18:34:14 -04:00
catch ( _d ) {
2021-01-19 11:29:46 -05:00
return false ;
}
return true ;
}
return false ;
} ) ;
}
2020-10-01 19:06:56 -04:00
}
/ * *
* @ 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
* /
/** Yargs command builder for configuring the `ng-dev release publish` command. */
2021-03-19 13:01:11 -04:00
function builder$8 ( argv ) {
2020-10-01 19:06:56 -04:00
return addGithubTokenOption ( argv ) ;
}
/** Yargs command handler for staging a release. */
2021-04-18 05:46:57 -04:00
function handler$8 ( ) {
2020-10-01 19:06:56 -04:00
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
2021-04-12 17:50:50 -04:00
const git = GitClient . getInstance ( ) ;
2020-10-01 19:06:56 -04:00
const config = getConfig ( ) ;
const releaseConfig = getReleaseConfig ( config ) ;
2021-04-12 17:50:50 -04:00
const projectDir = git . baseDir ;
2021-04-18 05:46:57 -04:00
const task = new ReleaseTool ( releaseConfig , config . github , projectDir ) ;
2020-10-01 19:06:56 -04:00
const result = yield task . run ( ) ;
switch ( result ) {
case CompletionState . FATAL _ERROR :
error ( red ( ` Release action has been aborted due to fatal errors. See above. ` ) ) ;
2021-01-22 12:54:07 -05:00
process . exitCode = 2 ;
2020-10-01 19:06:56 -04:00
break ;
case CompletionState . MANUALLY _ABORTED :
info ( yellow ( ` Release action has been manually aborted. ` ) ) ;
2021-01-22 12:54:07 -05:00
process . exitCode = 1 ;
2020-10-01 19:06:56 -04:00
break ;
case CompletionState . SUCCESS :
info ( green ( ` Release action has completed successfully. ` ) ) ;
break ;
}
} ) ;
}
/** CLI command module for publishing a release. */
const ReleasePublishCommandModule = {
2021-03-19 13:01:11 -04:00
builder : builder$8 ,
handler : handler$8 ,
2020-10-01 19:06:56 -04:00
command : 'publish' ,
describe : 'Publish new releases and configure version branches.' ,
} ;
/ * *
* @ 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
* /
2021-03-19 13:01:11 -04:00
function builder$9 ( args ) {
2020-10-01 19:06:56 -04:00
return args
. positional ( 'tagName' , {
type : 'string' ,
demandOption : true ,
description : 'Name of the NPM dist tag.' ,
} )
. positional ( 'targetVersion' , {
type : 'string' ,
demandOption : true ,
description : 'Version to which the dist tag should be set.'
} ) ;
}
/** Yargs command handler for building a release. */
2021-03-19 13:01:11 -04:00
function handler$9 ( args ) {
2020-10-01 19:06:56 -04:00
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
const { targetVersion : rawVersion , tagName } = args ;
const { npmPackages , publishRegistry } = getReleaseConfig ( ) ;
const version = semver . parse ( rawVersion ) ;
if ( version === null ) {
error ( red ( ` Invalid version specified ( ${ rawVersion } ). Unable to set NPM dist tag. ` ) ) ;
process . exit ( 1 ) ;
}
const spinner = ora . call ( undefined ) . start ( ) ;
debug ( ` Setting " ${ tagName } " NPM dist tag for release packages to v ${ version } . ` ) ;
for ( const pkgName of npmPackages ) {
spinner . text = ` Setting NPM dist tag for " ${ pkgName } " ` ;
spinner . render ( ) ;
try {
yield setNpmTagForPackage ( pkgName , tagName , version , publishRegistry ) ;
debug ( ` Successfully set " ${ tagName } " NPM dist tag for " ${ pkgName } ". ` ) ;
}
catch ( e ) {
spinner . stop ( ) ;
error ( e ) ;
error ( red ( ` ✘ An error occurred while setting the NPM dist tag for " ${ pkgName } ". ` ) ) ;
process . exit ( 1 ) ;
}
}
spinner . stop ( ) ;
info ( green ( ` ✓ Set NPM dist tag for all release packages. ` ) ) ;
info ( green ( ` ${ bold ( tagName ) } will now point to ${ bold ( ` v ${ version } ` ) } . ` ) ) ;
} ) ;
}
/** CLI command module for setting an NPM dist tag. */
const ReleaseSetDistTagCommand = {
2021-03-19 13:01:11 -04:00
builder : builder$9 ,
handler : handler$9 ,
2020-10-01 19:06:56 -04:00
command : 'set-dist-tag <tag-name> <target-version>' ,
describe : 'Sets a given NPM dist tag for all release packages.' ,
} ;
/ * *
* @ 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
* /
/ * *
* Log the environment variables expected by bazel for stamping .
*
* See the section on stamping in docs / BAZEL . md
*
* This script must be a NodeJS script in order to be cross - platform .
* See https : //github.com/bazelbuild/bazel/issues/5958
* Note : git operations , especially git status , take a long time inside mounted docker volumes
* in Windows or OSX hosts ( https : //github.com/docker/for-win/issues/188).
* /
2020-12-11 15:34:44 -05:00
function buildEnvStamp ( mode ) {
2020-10-01 19:06:56 -04:00
console . info ( ` BUILD_SCM_BRANCH ${ getCurrentBranch ( ) } ` ) ;
console . info ( ` BUILD_SCM_COMMIT_SHA ${ getCurrentSha ( ) } ` ) ;
console . info ( ` BUILD_SCM_HASH ${ getCurrentSha ( ) } ` ) ;
console . info ( ` BUILD_SCM_LOCAL_CHANGES ${ hasLocalChanges ( ) } ` ) ;
console . info ( ` BUILD_SCM_USER ${ getCurrentGitUser ( ) } ` ) ;
2020-12-11 15:34:44 -05:00
console . info ( ` BUILD_SCM_VERSION ${ getSCMVersion ( mode ) } ` ) ;
2020-10-01 19:06:56 -04:00
process . exit ( 0 ) ;
}
/** Run the exec command and return the stdout as a trimmed string. */
function exec$1 ( cmd ) {
return exec ( cmd ) . trim ( ) ;
}
/** Whether the repo has local changes. */
function hasLocalChanges ( ) {
return ! ! exec$1 ( ` git status --untracked-files=no --porcelain ` ) ;
}
2020-12-11 15:34:44 -05:00
/ * *
* Get the version for generated packages .
*
* In snapshot mode , the version is based on the most recent semver tag .
* In release mode , the version is based on the base package . json version .
* /
function getSCMVersion ( mode ) {
if ( mode === 'release' ) {
2021-04-12 17:50:50 -04:00
const git = GitClient . getInstance ( ) ;
const packageJsonPath = path . join ( git . baseDir , 'package.json' ) ;
2020-12-11 15:34:44 -05:00
const { version } = require ( packageJsonPath ) ;
return version ;
}
if ( mode === 'snapshot' ) {
const version = exec$1 ( ` git describe --match [0-9]*.[0-9]*.[0-9]* --abbrev=7 --tags HEAD ` ) ;
return ` ${ version . replace ( /-([0-9]+)-g/ , '+$1.sha-' ) } ${ ( hasLocalChanges ( ) ? '.with-local-changes' : '' ) } ` ;
}
return '0.0.0' ;
2020-10-01 19:06:56 -04:00
}
/** Get the current SHA of HEAD. */
function getCurrentSha ( ) {
return exec$1 ( ` git rev-parse HEAD ` ) ;
}
/** Get the currently checked out branch. */
function getCurrentBranch ( ) {
return exec$1 ( ` git symbolic-ref --short HEAD ` ) ;
}
/** Get the current git user based on the git config. */
function getCurrentGitUser ( ) {
const userName = exec$1 ( ` git config user.name ` ) ;
const userEmail = exec$1 ( ` git config user.email ` ) ;
return ` ${ userName } < ${ userEmail } > ` ;
}
2020-12-11 15:34:44 -05:00
/ * *
* @ 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
* /
2021-03-19 13:01:11 -04:00
function builder$a ( args ) {
2020-12-11 15:34:44 -05:00
return args . option ( 'mode' , {
demandOption : true ,
description : 'Whether the env-stamp should be built for a snapshot or release' ,
choices : [ 'snapshot' , 'release' ]
} ) ;
}
2021-03-19 13:01:11 -04:00
function handler$a ( { mode } ) {
2020-12-11 15:34:44 -05:00
return tslib . _ _awaiter ( this , void 0 , void 0 , function * ( ) {
buildEnvStamp ( mode ) ;
} ) ;
}
/** CLI command module for building the environment stamp. */
const BuildEnvStampCommand = {
2021-03-19 13:01:11 -04:00
builder : builder$a ,
handler : handler$a ,
2020-12-11 15:34:44 -05:00
command : 'build-env-stamp' ,
describe : 'Build the environment stamping information' ,
} ;
2020-10-01 19:06:56 -04:00
/** Build the parser for the release commands. */
function buildReleaseParser ( localYargs ) {
return localYargs . help ( )
. strict ( )
. demandCommand ( )
. command ( ReleasePublishCommandModule )
. command ( ReleaseBuildCommandModule )
. command ( ReleaseSetDistTagCommand )
2020-12-11 15:34:44 -05:00
. command ( BuildEnvStampCommand ) ;
2020-10-01 19:06:56 -04:00
}
/ * *
* @ 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
* /
/** Gets the status of the specified file. Returns null if the file does not exist. */
function getFileStatus ( filePath ) {
try {
return fs . statSync ( filePath ) ;
}
catch ( _a ) {
return null ;
}
}
/** Ensures that the specified path uses forward slashes as delimiter. */
function convertPathToForwardSlash ( path ) {
return path . replace ( /\\/g , '/' ) ;
}
/ * *
* @ 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
* /
/ * *
* Finds all module references in the specified source file .
* @ param node Source file which should be parsed .
* @ returns List of import specifiers in the source file .
* /
function getModuleReferences ( node ) {
const references = [ ] ;
const visitNode = ( node ) => {
if ( ( ts . isImportDeclaration ( node ) || ts . isExportDeclaration ( node ) ) &&
node . moduleSpecifier !== undefined && ts . isStringLiteral ( node . moduleSpecifier ) ) {
references . push ( node . moduleSpecifier . text ) ;
}
ts . forEachChild ( node , visitNode ) ;
} ;
ts . forEachChild ( node , visitNode ) ;
return references ;
}
/ * *
* @ 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
* /
/** Default extensions that the analyzer uses for resolving imports. */
const DEFAULT _EXTENSIONS = [ 'ts' , 'js' , 'd.ts' ] ;
/ * *
* Analyzer that can be used to detect import cycles within source files . It supports
* custom module resolution , source file caching and collects unresolved specifiers .
* /
class Analyzer {
constructor ( resolveModuleFn , extensions = DEFAULT _EXTENSIONS ) {
this . resolveModuleFn = resolveModuleFn ;
this . extensions = extensions ;
this . _sourceFileCache = new Map ( ) ;
this . unresolvedModules = new Set ( ) ;
this . unresolvedFiles = new Map ( ) ;
}
/** Finds all cycles in the specified source file. */
findCycles ( sf , visited = new WeakSet ( ) , path = [ ] ) {
const previousIndex = path . indexOf ( sf ) ;
// If the given node is already part of the current path, then a cycle has
// been found. Add the reference chain which represents the cycle to the results.
if ( previousIndex !== - 1 ) {
return [ path . slice ( previousIndex ) ] ;
}
// If the node has already been visited, then it's not necessary to go check its edges
// again. Cycles would have been already detected and collected in the first check.
if ( visited . has ( sf ) ) {
return [ ] ;
}
path . push ( sf ) ;
visited . add ( sf ) ;
// Go through all edges, which are determined through import/exports, and collect cycles.
const result = [ ] ;
for ( const ref of getModuleReferences ( sf ) ) {
const targetFile = this . _resolveImport ( ref , sf . fileName ) ;
if ( targetFile !== null ) {
result . push ( ... this . findCycles ( this . getSourceFile ( targetFile ) , visited , path . slice ( ) ) ) ;
}
}
return result ;
}
/** Gets the TypeScript source file of the specified path. */
getSourceFile ( filePath ) {
const resolvedPath = path . resolve ( filePath ) ;
if ( this . _sourceFileCache . has ( resolvedPath ) ) {
return this . _sourceFileCache . get ( resolvedPath ) ;
}
const fileContent = fs . readFileSync ( resolvedPath , 'utf8' ) ;
const sourceFile = ts . createSourceFile ( resolvedPath , fileContent , ts . ScriptTarget . Latest , false ) ;
this . _sourceFileCache . set ( resolvedPath , sourceFile ) ;
return sourceFile ;
}
/** Resolves the given import specifier with respect to the specified containing file path. */
_resolveImport ( specifier , containingFilePath ) {
if ( specifier . charAt ( 0 ) === '.' ) {
const resolvedPath = this . _resolveFileSpecifier ( specifier , containingFilePath ) ;
if ( resolvedPath === null ) {
this . _trackUnresolvedFileImport ( specifier , containingFilePath ) ;
}
return resolvedPath ;
}
if ( this . resolveModuleFn ) {
const targetFile = this . resolveModuleFn ( specifier ) ;
if ( targetFile !== null ) {
const resolvedPath = this . _resolveFileSpecifier ( targetFile ) ;
if ( resolvedPath !== null ) {
return resolvedPath ;
}
}
}
this . unresolvedModules . add ( specifier ) ;
return null ;
}
/** Tracks the given file import as unresolved. */
_trackUnresolvedFileImport ( specifier , originFilePath ) {
if ( ! this . unresolvedFiles . has ( originFilePath ) ) {
this . unresolvedFiles . set ( originFilePath , [ specifier ] ) ;
}
this . unresolvedFiles . get ( originFilePath ) . push ( specifier ) ;
}
/** Resolves the given import specifier to the corresponding source file. */
_resolveFileSpecifier ( specifier , containingFilePath ) {
const importFullPath = containingFilePath !== undefined ? path . join ( path . dirname ( containingFilePath ) , specifier ) : specifier ;
const stat = getFileStatus ( importFullPath ) ;
if ( stat && stat . isFile ( ) ) {
return importFullPath ;
}
for ( const extension of this . extensions ) {
const pathWithExtension = ` ${ importFullPath } . ${ extension } ` ;
const stat = getFileStatus ( pathWithExtension ) ;
if ( stat && stat . isFile ( ) ) {
return pathWithExtension ;
}
}
// Directories should be considered last. TypeScript first looks for source files, then
// falls back to directories if no file with appropriate extension could be found.
if ( stat && stat . isDirectory ( ) ) {
return this . _resolveFileSpecifier ( path . join ( importFullPath , 'index' ) ) ;
}
return null ;
}
}
/ * *
* @ 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
* /
/ * *
* Loads the configuration for the circular dependencies test . If the config cannot be
* loaded , an error will be printed and the process exists with a non - zero exit code .
* /
function loadTestConfig ( configPath ) {
const configBaseDir = path . dirname ( configPath ) ;
const resolveRelativePath = ( relativePath ) => path . resolve ( configBaseDir , relativePath ) ;
try {
const config = require ( configPath ) ;
if ( ! path . isAbsolute ( config . baseDir ) ) {
config . baseDir = resolveRelativePath ( config . baseDir ) ;
}
if ( ! path . isAbsolute ( config . goldenFile ) ) {
config . goldenFile = resolveRelativePath ( config . goldenFile ) ;
}
if ( ! path . isAbsolute ( config . glob ) ) {
config . glob = resolveRelativePath ( config . glob ) ;
}
return config ;
}
catch ( e ) {
error ( 'Could not load test configuration file at: ' + configPath ) ;
error ( ` Failed with: ${ e . message } ` ) ;
process . exit ( 1 ) ;
}
}
/ * *
* @ 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
* /
/ * *
* Converts a list of reference chains to a JSON - compatible golden object . Reference chains
* by default use TypeScript source file objects . In order to make those chains printable ,
* the source file objects are mapped to their relative file names .
* /
function convertReferenceChainToGolden ( refs , baseDir ) {
return refs
. map (
// Normalize cycles as the paths can vary based on which node in the cycle is visited
// first in the analyzer. The paths represent cycles. Hence we can shift nodes in a
// deterministic way so that the goldens don't change unnecessarily and cycle comparison
// is simpler.
chain => normalizeCircularDependency ( chain . map ( ( { fileName } ) => convertPathToForwardSlash ( path . relative ( baseDir , fileName ) ) ) ) )
// Sort cycles so that the golden doesn't change unnecessarily when cycles are detected
// in different order (e.g. new imports cause cycles to be detected earlier or later).
. sort ( compareCircularDependency ) ;
}
/ * *
* Compares the specified goldens and returns two lists that describe newly
* added circular dependencies , or fixed circular dependencies .
* /
function compareGoldens ( actual , expected ) {
const newCircularDeps = [ ] ;
const fixedCircularDeps = [ ] ;
actual . forEach ( a => {
if ( ! expected . find ( e => isSameCircularDependency ( a , e ) ) ) {
newCircularDeps . push ( a ) ;
}
} ) ;
expected . forEach ( e => {
if ( ! actual . find ( a => isSameCircularDependency ( e , a ) ) ) {
fixedCircularDeps . push ( e ) ;
}
} ) ;
return { newCircularDeps , fixedCircularDeps } ;
}
/ * *
* Normalizes the a circular dependency by ensuring that the path starts with the first
* node in alphabetical order . Since the path array represents a cycle , we can make a
* specific node the first element in the path that represents the cycle .
*
* This method is helpful because the path of circular dependencies changes based on which
* file in the path has been visited first by the analyzer . e . g . Assume we have a circular
* dependency represented as : ` A -> B -> C ` . The analyzer will detect this cycle when it
* visits ` A ` . Though when a source file that is analyzed before ` A ` starts importing ` B ` ,
* the cycle path will detected as ` B -> C -> A ` . This represents the same cycle , but is just
* different due to a limitation of using a data structure that can be written to a text - based
* golden file .
*
* To account for this non - deterministic behavior in goldens , we shift the circular
* dependency path to the first node based on alphabetical order . e . g . ` A ` will always
* be the first node in the path that represents the cycle .
* /
function normalizeCircularDependency ( path ) {
if ( path . length <= 1 ) {
return path ;
}
let indexFirstNode = 0 ;
let valueFirstNode = path [ 0 ] ;
// Find a node in the cycle path that precedes all other elements
// in terms of alphabetical order.
for ( let i = 1 ; i < path . length ; i ++ ) {
const value = path [ i ] ;
if ( value . localeCompare ( valueFirstNode , 'en' ) < 0 ) {
indexFirstNode = i ;
valueFirstNode = value ;
}
}
// If the alphabetically first node is already at start of the path, just
// return the actual path as no changes need to be made.
if ( indexFirstNode === 0 ) {
return path ;
}
// Move the determined first node (as of alphabetical order) to the start of a new
// path array. The nodes before the first node in the old path are then concatenated
// to the end of the new path. This is possible because the path represents a cycle.
return [ ... path . slice ( indexFirstNode ) , ... path . slice ( 0 , indexFirstNode ) ] ;
}
/** Checks whether the specified circular dependencies are equal. */
function isSameCircularDependency ( actual , expected ) {
if ( actual . length !== expected . length ) {
return false ;
}
for ( let i = 0 ; i < actual . length ; i ++ ) {
if ( actual [ i ] !== expected [ i ] ) {
return false ;
}
}
return true ;
}
/ * *
* Compares two circular dependencies by respecting the alphabetic order of nodes in the
* cycle paths . The first nodes which don ' t match in both paths are decisive on the order .
* /
function compareCircularDependency ( a , b ) {
// Go through nodes in both cycle paths and determine whether `a` should be ordered
// before `b`. The first nodes which don't match decide on the order.
for ( let i = 0 ; i < Math . min ( a . length , b . length ) ; i ++ ) {
const compareValue = a [ i ] . localeCompare ( b [ i ] , 'en' ) ;
if ( compareValue !== 0 ) {
return compareValue ;
}
}
// If all nodes are equal in the cycles, the order is based on the length of both cycles.
return a . length - b . length ;
}
/ * *
* @ 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
* /
function tsCircularDependenciesBuilder ( localYargs ) {
return localYargs . help ( )
. strict ( )
. demandCommand ( )
. option ( 'config' , { type : 'string' , demandOption : true , description : 'Path to the configuration file.' } )
. option ( 'warnings' , { type : 'boolean' , description : 'Prints all warnings.' } )
. command ( 'check' , 'Checks if the circular dependencies have changed.' , args => args , argv => {
const { config : configArg , warnings } = argv ;
const configPath = path . isAbsolute ( configArg ) ? configArg : path . resolve ( configArg ) ;
const config = loadTestConfig ( configPath ) ;
process . exit ( main ( false , config , ! ! warnings ) ) ;
} )
. command ( 'approve' , 'Approves the current circular dependencies.' , args => args , argv => {
const { config : configArg , warnings } = argv ;
const configPath = path . isAbsolute ( configArg ) ? configArg : path . resolve ( configArg ) ;
const config = loadTestConfig ( configPath ) ;
process . exit ( main ( true , config , ! ! warnings ) ) ;
} ) ;
}
/ * *
* Runs the ts - circular - dependencies tool .
* @ param approve Whether the detected circular dependencies should be approved .
* @ param config Configuration for the current circular dependencies test .
* @ param printWarnings Whether warnings should be printed out .
* @ returns Status code .
* /
function main ( approve , config , printWarnings ) {
const { baseDir , goldenFile , glob : glob$1 , resolveModule , approveCommand } = config ;
const analyzer = new Analyzer ( resolveModule ) ;
const cycles = [ ] ;
const checkedNodes = new WeakSet ( ) ;
2021-04-01 13:00:04 -04:00
glob . sync ( glob$1 , { absolute : true , ignore : [ '**/node_modules/**' ] } ) . forEach ( filePath => {
2020-10-01 19:06:56 -04:00
const sourceFile = analyzer . getSourceFile ( filePath ) ;
cycles . push ( ... analyzer . findCycles ( sourceFile , checkedNodes ) ) ;
} ) ;
const actual = convertReferenceChainToGolden ( cycles , baseDir ) ;
info ( green ( ` Current number of cycles: ${ yellow ( cycles . length . toString ( ) ) } ` ) ) ;
if ( approve ) {
fs . writeFileSync ( goldenFile , JSON . stringify ( actual , null , 2 ) ) ;
info ( green ( '✅ Updated golden file.' ) ) ;
return 0 ;
}
else if ( ! fs . existsSync ( goldenFile ) ) {
error ( red ( ` ❌ Could not find golden file: ${ goldenFile } ` ) ) ;
return 1 ;
}
const warningsCount = analyzer . unresolvedFiles . size + analyzer . unresolvedModules . size ;
// By default, warnings for unresolved files or modules are not printed. This is because
// it's common that third-party modules are not resolved/visited. Also generated files
// from the View Engine compiler (i.e. factories, summaries) cannot be resolved.
if ( printWarnings && warningsCount !== 0 ) {
info ( yellow ( '⚠ The following imports could not be resolved:' ) ) ;
Array . from ( analyzer . unresolvedModules ) . sort ( ) . forEach ( specifier => info ( ` • ${ specifier } ` ) ) ;
analyzer . unresolvedFiles . forEach ( ( value , key ) => {
info ( ` • ${ getRelativePath ( baseDir , key ) } ` ) ;
value . sort ( ) . forEach ( specifier => info ( ` ${ specifier } ` ) ) ;
} ) ;
}
else {
info ( yellow ( ` ⚠ ${ warningsCount } imports could not be resolved. ` ) ) ;
info ( yellow ( ` Please rerun with "--warnings" to inspect unresolved imports. ` ) ) ;
}
const expected = JSON . parse ( fs . readFileSync ( goldenFile , 'utf8' ) ) ;
const { fixedCircularDeps , newCircularDeps } = compareGoldens ( actual , expected ) ;
const isMatching = fixedCircularDeps . length === 0 && newCircularDeps . length === 0 ;
if ( isMatching ) {
info ( green ( '✅ Golden matches current circular dependencies.' ) ) ;
return 0 ;
}
error ( red ( '❌ Golden does not match current circular dependencies.' ) ) ;
if ( newCircularDeps . length !== 0 ) {
error ( yellow ( ` New circular dependencies which are not allowed: ` ) ) ;
newCircularDeps . forEach ( c => error ( ` • ${ convertReferenceChainToString ( c ) } ` ) ) ;
error ( ) ;
}
if ( fixedCircularDeps . length !== 0 ) {
error ( yellow ( ` Fixed circular dependencies that need to be removed from the golden: ` ) ) ;
fixedCircularDeps . forEach ( c => error ( ` • ${ convertReferenceChainToString ( c ) } ` ) ) ;
info ( yellow ( ` \n Total: ${ newCircularDeps . length } new cycle(s), ${ fixedCircularDeps . length } fixed cycle(s). \n ` ) ) ;
if ( approveCommand ) {
info ( yellow ( ` Please approve the new golden with: ${ approveCommand } ` ) ) ;
}
else {
info ( yellow ( ` Please update the golden. The following command can be ` +
` run: yarn ts-circular-deps approve ${ getRelativePath ( process . cwd ( ) , goldenFile ) } . ` ) ) ;
}
}
return 1 ;
}
/** Gets the specified path relative to the base directory. */
function getRelativePath ( baseDir , path$1 ) {
return convertPathToForwardSlash ( path . relative ( baseDir , path$1 ) ) ;
}
/** Converts the given reference chain to its string representation. */
function convertReferenceChainToString ( chain ) {
return chain . join ( ' → ' ) ;
}
yargs . scriptName ( 'ng-dev' )
. middleware ( captureLogOutputForCommand )
. demandCommand ( )
. recommendCommands ( )
. command ( 'commit-message <command>' , '' , buildCommitMessageParser )
. command ( 'format <command>' , '' , buildFormatParser )
. command ( 'pr <command>' , '' , buildPrParser )
. command ( 'pullapprove <command>' , '' , buildPullapproveParser )
. command ( 'release <command>' , '' , buildReleaseParser )
. command ( 'ts-circular-deps <command>' , '' , tsCircularDependenciesBuilder )
. command ( 'caretaker <command>' , '' , buildCaretakerParser )
. command ( 'ngbot <command>' , false , buildNgbotParser )
. wrap ( 120 )
. strict ( )
. parse ( ) ;