1
1
const { spawnSync } = require ( 'child_process' ) ;
2
2
const fs = require ( 'fs' ) ;
3
3
const os = require ( 'os' ) ;
4
+ const path = require ( 'path' ) ;
5
+ const { isUtf8 } = require ( "buffer" ) ;
4
6
5
7
// Note that we are not using the `@actions/core` package as it is not available
6
8
// without either committing node_modules/ to the repository, or using something
@@ -14,6 +16,35 @@ const escapeData = (s) => {
14
16
. replace ( / \n / g, '%0A' )
15
17
}
16
18
19
+ const stringify = ( value ) => {
20
+ if ( typeof value === 'string' ) return value ;
21
+ if ( Buffer . isBuffer ( value ) && isUtf8 ( value ) ) return value . toString ( 'utf-8' ) ;
22
+ return undefined ;
23
+ }
24
+
25
+ const trimEOL = ( buf ) => {
26
+ let l = buf . length
27
+ if ( l > 0 && buf [ l - 1 ] === 0x0a ) {
28
+ l -= l > 1 && buf [ l - 2 ] === 0x0d ? 2 : 1
29
+ }
30
+ return buf . slice ( 0 , l )
31
+ }
32
+
33
+ const writeBufToFile = ( buf , file ) => {
34
+ out = fs . createWriteStream ( file )
35
+ out . write ( buf )
36
+ out . end ( )
37
+ }
38
+
39
+ const logInfo = ( message ) => {
40
+ process . stdout . write ( `${ message } ${ os . EOL } ` ) ;
41
+ }
42
+
43
+ const setFailed = ( error ) => {
44
+ process . stdout . write ( `::error::${ escapeData ( error . message ) } ${ os . EOL } ` ) ;
45
+ process . exitCode = 1 ;
46
+ }
47
+
17
48
const writeCommand = ( file , name , value ) => {
18
49
// Unique delimiter to avoid conflicts with actual values
19
50
let delim ;
@@ -28,24 +59,38 @@ const writeCommand = (file, name, value) => {
28
59
}
29
60
30
61
const setSecret = ( value ) => {
31
- process . stdout . write ( `::add-mask::${ escapeData ( value ) } ${ os . EOL } ` ) ;
62
+ value = stringify ( value ) ;
63
+
64
+ // Masking a secret that is not a valid UTF-8 string or buffer is not useful
65
+ if ( value === undefined ) return ;
66
+
67
+ process . stdout . write (
68
+ value
69
+ . split ( / \r ? \n / g)
70
+ . filter ( line => line . length > 0 ) // Cannot mask empty lines
71
+ . map (
72
+ value => `::add-mask::${ escapeData ( value ) } ${ os . EOL } `
73
+ )
74
+ . join ( '' )
75
+ ) ;
32
76
}
33
77
34
78
const setOutput = ( name , value ) => {
79
+ value = stringify ( value ) ;
80
+ if ( value === undefined ) {
81
+ throw new Error ( `Output value '${ name } ' is not a valid UTF-8 string or buffer` ) ;
82
+ }
83
+
35
84
writeCommand ( process . env . GITHUB_OUTPUT , name , value ) ;
36
85
}
37
86
38
87
const exportVariable = ( name , value ) => {
39
- writeCommand ( process . env . GITHUB_ENV , name , value ) ;
40
- }
41
-
42
- const logInfo = ( message ) => {
43
- process . stdout . write ( `${ message } ${ os . EOL } ` ) ;
44
- }
88
+ value = stringify ( value ) ;
89
+ if ( value === undefined ) {
90
+ throw new Error ( `Environment variable '${ name } ' is not a valid UTF-8 string or buffer` ) ;
91
+ }
45
92
46
- const setFailed = ( error ) => {
47
- process . stdout . write ( `::error::${ escapeData ( error . message ) } ${ os . EOL } ` ) ;
48
- process . exitCode = 1 ;
93
+ writeCommand ( process . env . GITHUB_ENV , name , value ) ;
49
94
}
50
95
51
96
( async ( ) => {
@@ -67,9 +112,7 @@ const setFailed = (error) => {
67
112
68
113
// Fetch secrets from Azure Key Vault
69
114
for ( const { input : secretName , encoding, output } of secretMappings ) {
70
- let secretValue = '' ;
71
-
72
- const az = spawnSync ( 'az' ,
115
+ let az = spawnSync ( 'az' ,
73
116
[
74
117
'keyvault' ,
75
118
'secret' ,
@@ -92,10 +135,12 @@ const setFailed = (error) => {
92
135
if ( az . error ) throw new Error ( az . error , { cause : az . error } ) ;
93
136
if ( az . status !== 0 ) throw new Error ( `az failed with status ${ az . status } ` ) ;
94
137
95
- secretValue = az . stdout . toString ( 'utf-8' ) . trim ( ) ;
138
+ // az keyvault secret show --output tsv returns a buffer with trailing \n
139
+ // (or \r\n on Windows), so we need to trim it specifically.
140
+ let secretBuf = trimEOL ( az . stdout ) ;
96
141
97
142
// Mask the raw secret value in logs
98
- setSecret ( secretValue ) ;
143
+ setSecret ( secretBuf ) ;
99
144
100
145
// Handle encoded values if specified
101
146
// Sadly we cannot use the `--encoding` parameter of the `az keyvault
@@ -105,31 +150,46 @@ const setFailed = (error) => {
105
150
if ( encoding ) {
106
151
switch ( encoding . toLowerCase ( ) ) {
107
152
case 'base64' :
108
- secretValue = Buffer . from ( secretValue , 'base64' ) . toString ( ) ;
153
+ secretBuf = Buffer . from ( secretBuf . toString ( 'utf-8' ) , 'base64' ) ;
154
+ break ;
155
+ case 'ascii' :
156
+ case 'utf8' :
157
+ case 'utf-8' :
158
+ // No need to decode the existing buffer from the az command
109
159
break ;
110
160
default :
111
- // No decoding needed
112
- }
161
+ throw new Error ( `Unsupported encoding: ${ encoding } ` ) ;
162
+ }
113
163
114
- setSecret ( secretValue ) ; // Mask the decoded value as well
164
+ // Mask the decoded value
165
+ setSecret ( secretBuf ) ;
115
166
}
116
167
117
- if ( output . startsWith ( '$env:' ) ) {
118
- // Environment variable
119
- const envVarName = output . replace ( '$env:' , '' ) . trim ( ) ;
120
- exportVariable ( envVarName , secretValue ) ;
121
- logInfo ( `Secret set as environment variable: ${ envVarName } ` ) ;
122
- } else if ( output . startsWith ( '$output:' ) ) {
123
- // GitHub Actions output variable
124
- const outputName = output . replace ( '$output:' , '' ) . trim ( ) ;
125
- setOutput ( outputName , secretValue ) ;
126
- logInfo ( `Secret set as output variable: ${ outputName } ` ) ;
127
- } else {
128
- // File output
129
- const filePath = output . trim ( ) ;
130
- fs . mkdirSync ( path . dirname ( filePath ) , { recursive : true } ) ;
131
- fs . writeFileSync ( filePath , secretValue ) ;
132
- logInfo ( `Secret written to file: ${ filePath } ` ) ;
168
+ const outputType = output . startsWith ( '$env:' )
169
+ ? 'env'
170
+ : output . startsWith ( '$output:' )
171
+ ? 'output'
172
+ : 'file' ;
173
+
174
+ switch ( outputType ) {
175
+ case 'env' :
176
+ const varName = output . replace ( '$env:' , '' ) . trim ( ) ;
177
+ exportVariable ( varName , secretBuf ) ;
178
+ logInfo ( `Secret set as environment variable: ${ varName } ` ) ;
179
+ break ;
180
+
181
+ case 'output' :
182
+ const outputName = output . replace ( '$output:' , '' ) . trim ( ) ;
183
+ setOutput ( outputName , secretBuf ) ;
184
+ logInfo ( `Secret set as output variable: ${ outputName } ` ) ;
185
+ break ;
186
+
187
+ case 'file' :
188
+ const filePath = output . trim ( ) ;
189
+ fs . mkdirSync ( path . dirname ( filePath ) , { recursive : true } ) ;
190
+ writeBufToFile ( secretBuf , filePath ) ;
191
+ logInfo ( `Secret written to file: ${ filePath } ` ) ;
192
+ break ;
133
193
}
134
194
}
135
195
} ) ( ) . catch ( setFailed ) ;
0 commit comments