@@ -10,10 +10,20 @@ import * as Octokit from '@octokit/rest';
1010import { spawnSync , SpawnSyncOptions , SpawnSyncReturns } from 'child_process' ;
1111import { Config } from './config' ;
1212
13+ /** Error for failed Github API requests. */
14+ export class GithubApiRequestError extends Error {
15+ constructor ( public status : number , message : string ) {
16+ super ( message ) ;
17+ }
18+ }
19+
1320/** Error for failed Git commands. */
1421export class GitCommandError extends Error {
15- constructor ( public commandArgs : string [ ] ) {
16- super ( `Command failed: git ${ commandArgs . join ( ' ' ) } ` ) ;
22+ constructor ( client : GitClient , public args : string [ ] ) {
23+ // Errors are not guaranteed to be caught. To ensure that we don't
24+ // accidentally leak the Github token that might be used in a command,
25+ // we sanitize the command that will be part of the error message.
26+ super ( `Command failed: git ${ client . omitGithubTokenFromMessage ( args . join ( ' ' ) ) } ` ) ;
1727 }
1828}
1929
@@ -29,15 +39,23 @@ export class GitClient {
2939 /** Instance of the authenticated Github octokit API. */
3040 api : Octokit ;
3141
42+ /** Regular expression that matches the provided Github token. */
43+ private _tokenRegex = new RegExp ( this . _githubToken , 'g' ) ;
44+
3245 constructor ( private _githubToken : string , private _config : Config ) {
3346 this . api = new Octokit ( { auth : _githubToken } ) ;
47+ this . api . hook . error ( 'request' , error => {
48+ // Wrap API errors in a known error class. This allows us to
49+ // expect Github API errors better and in a non-ambiguous way.
50+ throw new GithubApiRequestError ( error . status , error . message ) ;
51+ } ) ;
3452 }
3553
3654 /** Executes the given git command. Throws if the command fails. */
3755 run ( args : string [ ] , options ?: SpawnSyncOptions ) : Omit < SpawnSyncReturns < string > , 'status' > {
3856 const result = this . runGraceful ( args , options ) ;
3957 if ( result . status !== 0 ) {
40- throw new GitCommandError ( args ) ;
58+ throw new GitCommandError ( this , args ) ;
4159 }
4260 // Omit `status` from the type so that it's obvious that the status is never
4361 // non-zero as explained in the method description.
@@ -46,21 +64,32 @@ export class GitClient {
4664
4765 /**
4866 * Spawns a given Git command process. Does not throw if the command fails. Additionally,
49- * the " stderr" output is inherited and will be printed in case of errors . This makes it
50- * easier to debug failed commands.
67+ * if there is any stderr output, the output will be printed. This makes it easier to
68+ * debug failed commands.
5169 */
5270 runGraceful ( args : string [ ] , options : SpawnSyncOptions = { } ) : SpawnSyncReturns < string > {
53- // To improve the debugging experience in case something fails, we print
54- // all executed Git commands.
55- console . info ( 'Executing: git' , ...args ) ;
56- return spawnSync ( 'git' , args , {
71+ // To improve the debugging experience in case something fails, we print all executed
72+ // Git commands. Note that we do not want to print the token if is contained in the
73+ // command. It's common to share errors with others if the tool failed.
74+ console . info ( 'Executing: git' , this . omitGithubTokenFromMessage ( args . join ( ' ' ) ) ) ;
75+
76+ const result = spawnSync ( 'git' , args , {
5777 cwd : this . _config . projectRoot ,
58- stdio : [ 'pipe' , 'pipe' , 'inherit' ] ,
78+ stdio : 'pipe' ,
5979 ...options ,
6080 // Encoding is always `utf8` and not overridable. This ensures that this method
6181 // always returns `string` as output instead of buffers.
6282 encoding : 'utf8' ,
6383 } ) ;
84+
85+ if ( result . stderr !== null ) {
86+ // Git sometimes prints the command if it failed. This means that it could
87+ // potentially leak the Github token used for accessing the remote. To avoid
88+ // printing a token, we sanitize the string before printing the stderr output.
89+ process . stderr . write ( this . omitGithubTokenFromMessage ( result . stderr ) ) ;
90+ }
91+
92+ return result ;
6493 }
6594
6695 /** Whether the given branch contains the specified SHA. */
@@ -77,4 +106,9 @@ export class GitClient {
77106 hasUncommittedChanges ( ) : boolean {
78107 return this . runGraceful ( [ 'diff-index' , '--quiet' , 'HEAD' ] ) . status !== 0 ;
79108 }
109+
110+ /** Sanitizes a given message by omitting the provided Github token if present. */
111+ omitGithubTokenFromMessage ( value : string ) : string {
112+ return value . replace ( this . _tokenRegex , '<TOKEN>' ) ;
113+ }
80114}
0 commit comments