@@ -10,10 +10,20 @@ import * as Octokit from '@octokit/rest';
10
10
import { spawnSync , SpawnSyncOptions , SpawnSyncReturns } from 'child_process' ;
11
11
import { Config } from './config' ;
12
12
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
+
13
20
/** Error for failed Git commands. */
14
21
export 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 ( ' ' ) ) } ` ) ;
17
27
}
18
28
}
19
29
@@ -29,15 +39,23 @@ export class GitClient {
29
39
/** Instance of the authenticated Github octokit API. */
30
40
api : Octokit ;
31
41
42
+ /** Regular expression that matches the provided Github token. */
43
+ private _tokenRegex = new RegExp ( this . _githubToken , 'g' ) ;
44
+
32
45
constructor ( private _githubToken : string , private _config : Config ) {
33
46
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
+ } ) ;
34
52
}
35
53
36
54
/** Executes the given git command. Throws if the command fails. */
37
55
run ( args : string [ ] , options ?: SpawnSyncOptions ) : Omit < SpawnSyncReturns < string > , 'status' > {
38
56
const result = this . runGraceful ( args , options ) ;
39
57
if ( result . status !== 0 ) {
40
- throw new GitCommandError ( args ) ;
58
+ throw new GitCommandError ( this , args ) ;
41
59
}
42
60
// Omit `status` from the type so that it's obvious that the status is never
43
61
// non-zero as explained in the method description.
@@ -46,21 +64,32 @@ export class GitClient {
46
64
47
65
/**
48
66
* 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.
51
69
*/
52
70
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 , {
57
77
cwd : this . _config . projectRoot ,
58
- stdio : [ 'pipe' , 'pipe' , 'inherit' ] ,
78
+ stdio : 'pipe' ,
59
79
...options ,
60
80
// Encoding is always `utf8` and not overridable. This ensures that this method
61
81
// always returns `string` as output instead of buffers.
62
82
encoding : 'utf8' ,
63
83
} ) ;
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 ;
64
93
}
65
94
66
95
/** Whether the given branch contains the specified SHA. */
@@ -77,4 +106,9 @@ export class GitClient {
77
106
hasUncommittedChanges ( ) : boolean {
78
107
return this . runGraceful ( [ 'diff-index' , '--quiet' , 'HEAD' ] ) . status !== 0 ;
79
108
}
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
+ }
80
114
}
0 commit comments