@@ -39,23 +39,21 @@ class WorkflowStmt extends Statement instanceof Actions::Workflow {
39
39
}
40
40
41
41
class ReusableWorkflowStmt extends WorkflowStmt {
42
- YamlMapping parameters ;
42
+ YamlValue workflow_call ;
43
43
44
44
ReusableWorkflowStmt ( ) {
45
- exists ( Actions:: On on |
46
- on .getWorkflow ( ) = this and
47
- on .getNode ( "workflow_call" ) .( YamlMapping ) .lookup ( "inputs" ) = parameters
48
- )
45
+ this .( Actions:: Workflow ) .getOn ( ) .getNode ( "workflow_call" ) = workflow_call
49
46
}
50
47
51
- ParamsStmt getParams ( ) { result = parameters }
48
+ InputsStmt getInputs ( ) { result = workflow_call .( YamlMapping ) .lookup ( "inputs" ) }
49
+
50
+ OutputsStmt getOutputs ( ) { result = workflow_call .( YamlMapping ) .lookup ( "outputs" ) }
52
51
53
- // TODO: implemnt callable name
54
52
string getName ( ) { result = this .getLocation ( ) .getFile ( ) .getRelativePath ( ) }
55
53
}
56
54
57
- class ParamsStmt extends Statement instanceof YamlMapping {
58
- ParamsStmt ( ) {
55
+ class InputsStmt extends Statement instanceof YamlMapping {
56
+ InputsStmt ( ) {
59
57
exists ( Actions:: On on | on .getNode ( "workflow_call" ) .( YamlMapping ) .lookup ( "inputs" ) = this )
60
58
}
61
59
@@ -72,12 +70,38 @@ class ParamsStmt extends Statement instanceof YamlMapping {
72
70
* token:
73
71
* required: true
74
72
*/
75
- ParamExpr getParamExpr ( string name ) {
76
- this .( YamlMapping ) .maps ( any ( YamlScalar s | s .getValue ( ) = name ) , result )
73
+ InputExpr getInputExpr ( string name ) {
74
+ result .( YamlString ) .getValue ( ) = name and
75
+ this .( YamlMapping ) .maps ( result , _)
77
76
}
78
77
}
79
78
80
- class ParamExpr extends Expression instanceof YamlValue { }
79
+ class InputExpr extends Expression instanceof YamlString { }
80
+
81
+ class OutputsStmt extends Statement instanceof YamlMapping {
82
+ OutputsStmt ( ) {
83
+ exists ( Actions:: On on | on .getNode ( "workflow_call" ) .( YamlMapping ) .lookup ( "outputs" ) = this )
84
+ }
85
+
86
+ /**
87
+ * Gets a specific parameter expression (YamlMapping) by name.
88
+ * eg:
89
+ * on:
90
+ * workflow_call:
91
+ * outputs:
92
+ * firstword:
93
+ * description: "The first output string"
94
+ * value: ${{ jobs.example_job.outputs.output1 }}
95
+ * secondword:
96
+ * description: "The second output string"
97
+ * value: ${{ jobs.example_job.outputs.output2 }}
98
+ */
99
+ OutputExpr getOutputExpr ( string name ) {
100
+ this .( YamlMapping ) .lookup ( name ) .( YamlMapping ) .lookup ( "value" ) = result
101
+ }
102
+ }
103
+
104
+ class OutputExpr extends Expression instanceof YamlString { }
81
105
82
106
/**
83
107
* A Job is a collection of steps that run in an execution environment.
@@ -117,8 +141,13 @@ class JobStmt extends Statement instanceof Actions::Job {
117
141
118
142
/**
119
143
* Reusable workflow jobs may have Uses children
144
+ * eg:
145
+ * call-job:
146
+ * uses: ./.github/workflows/reusable_workflow.yml
147
+ * with:
148
+ * arg1: value1
120
149
*/
121
- JobUsesExpr getUsesExpr ( ) { result = this . ( Actions :: Job ) . lookup ( "uses" ) }
150
+ JobUsesExpr getUsesExpr ( ) { result . getJob ( ) = this }
122
151
}
123
152
124
153
/**
@@ -152,8 +181,11 @@ class StepStmt extends Statement instanceof Actions::Step {
152
181
JobStmt getJob ( ) { result = super .getJob ( ) }
153
182
}
154
183
184
+ /**
185
+ * Abstract class representing a call to a 3rd party action or reusable workflow.
186
+ */
155
187
abstract class UsesExpr extends Expression {
156
- abstract string getTarget ( ) ;
188
+ abstract string getCallee ( ) ;
157
189
158
190
abstract string getVersion ( ) ;
159
191
@@ -168,7 +200,7 @@ class StepUsesExpr extends StepStmt, UsesExpr {
168
200
169
201
StepUsesExpr ( ) { uses .getStep ( ) = this }
170
202
171
- override string getTarget ( ) { result = uses .getGitHubRepository ( ) }
203
+ override string getCallee ( ) { result = uses .getGitHubRepository ( ) }
172
204
173
205
override string getVersion ( ) { result = uses .getVersion ( ) }
174
206
@@ -183,12 +215,12 @@ class StepUsesExpr extends StepStmt, UsesExpr {
183
215
/**
184
216
* A Uses step represents a call to an action that is defined in a GitHub repository.
185
217
*/
186
- class JobUsesExpr extends UsesExpr instanceof YamlScalar {
187
- JobStmt job ;
188
-
189
- JobUsesExpr ( ) { job . ( YamlMapping ) . lookup ( "uses" ) = this }
218
+ class JobUsesExpr extends UsesExpr instanceof YamlMapping {
219
+ JobUsesExpr ( ) {
220
+ this instanceof JobStmt and this . maps ( any ( YamlString s | s . getValue ( ) = "uses" ) , _ )
221
+ }
190
222
191
- JobStmt getJob ( ) { result = job }
223
+ JobStmt getJob ( ) { result = this }
192
224
193
225
/**
194
226
* Gets a regular expression that parses an `owner/repo@version` reference within a `uses` field in an Actions job step.
@@ -200,31 +232,31 @@ class JobUsesExpr extends UsesExpr instanceof YamlScalar {
200
232
201
233
private string pathUsesParser ( ) { result = "\\./(.+)" }
202
234
203
- override string getTarget ( ) {
204
- exists ( string name |
205
- this .( YamlScalar ) . getValue ( ) = name and
206
- if name .matches ( "./%" )
207
- then result = name .regexpCapture ( this .pathUsesParser ( ) , 1 )
235
+ override string getCallee ( ) {
236
+ exists ( YamlString name |
237
+ this .( YamlMapping ) . lookup ( "uses" ) = name and
238
+ if name .getValue ( ) . matches ( "./%" )
239
+ then result = name .getValue ( ) . regexpCapture ( this .pathUsesParser ( ) , 1 )
208
240
else
209
241
result =
210
- name .regexpCapture ( this .repoUsesParser ( ) , 1 ) + "/" +
211
- name .regexpCapture ( this .repoUsesParser ( ) , 2 ) + "/" +
212
- name .regexpCapture ( this .repoUsesParser ( ) , 3 )
242
+ name .getValue ( ) . regexpCapture ( this .repoUsesParser ( ) , 1 ) + "/" +
243
+ name .getValue ( ) . regexpCapture ( this .repoUsesParser ( ) , 2 ) + "/" +
244
+ name .getValue ( ) . regexpCapture ( this .repoUsesParser ( ) , 3 )
213
245
)
214
246
}
215
247
216
248
/** Gets the version reference used when checking out the Action, e.g. `v2` in `actions/checkout@v2`. */
217
249
override string getVersion ( ) {
218
- exists ( string name |
219
- this .( YamlScalar ) . getValue ( ) = name and
220
- if not name .matches ( "\\.%" )
221
- then result = this . ( YamlScalar ) .getValue ( ) .regexpCapture ( this .repoUsesParser ( ) , 4 )
250
+ exists ( YamlString name |
251
+ this .( YamlMapping ) . lookup ( "uses" ) = name and
252
+ if not name .getValue ( ) . matches ( "\\.%" )
253
+ then result = name .getValue ( ) .regexpCapture ( this .repoUsesParser ( ) , 4 )
222
254
else none ( )
223
255
)
224
256
}
225
257
226
258
override Expression getArgument ( string key ) {
227
- job .( YamlMapping ) .lookup ( "with" ) .( YamlMapping ) .lookup ( key ) = result
259
+ this .( YamlMapping ) .lookup ( "with" ) .( YamlMapping ) .lookup ( key ) = result
228
260
}
229
261
}
230
262
@@ -287,16 +319,19 @@ class StepOutputAccessExpr extends ExprAccessExpr {
287
319
/**
288
320
* A ExprAccessExpr where the expression evaluated is a job output read.
289
321
* eg: `${{ needs.job1.outputs.foo}}`
322
+ * eg: `${{ jobs.job1.outputs.foo}}` (for reusable workflows)
290
323
*/
291
324
class JobOutputAccessExpr extends ExprAccessExpr {
292
325
string jobId ;
293
326
string varName ;
294
327
295
328
JobOutputAccessExpr ( ) {
296
329
jobId =
297
- this .getExpression ( ) .regexpCapture ( "needs\\.([A-Za-z0-9_-]+)\\.outputs\\.[A-Za-z0-9_-]+" , 1 ) and
330
+ this .getExpression ( )
331
+ .regexpCapture ( "(needs|jobs)\\.([A-Za-z0-9_-]+)\\.outputs\\.[A-Za-z0-9_-]+" , 2 ) and
298
332
varName =
299
- this .getExpression ( ) .regexpCapture ( "needs\\.[A-Za-z0-9_-]+\\.outputs\\.([A-Za-z0-9_-]+)" , 1 )
333
+ this .getExpression ( )
334
+ .regexpCapture ( "(needs|jobs)\\.[A-Za-z0-9_-]+\\.outputs\\.([A-Za-z0-9_-]+)" , 2 )
300
335
}
301
336
302
337
string getVarName ( ) { result = varName }
@@ -305,7 +340,35 @@ class JobOutputAccessExpr extends ExprAccessExpr {
305
340
exists ( JobStmt job |
306
341
job .getId ( ) = jobId and
307
342
job .getLocation ( ) .getFile ( ) = this .getLocation ( ) .getFile ( ) and
308
- job .getOutputStmt ( ) .getOutputExpr ( varName ) = result
343
+ (
344
+ // A Job can have multiple outputs, so we need to check both
345
+ // jobs.<job_id>.outputs.<output_name>
346
+ job .getOutputStmt ( ) .getOutputExpr ( varName ) = result
347
+ or
348
+ // jobs.<job_id>.uses (variables returned from the reusable workflow
349
+ job .getUsesExpr ( ) = result
350
+ )
351
+ )
352
+ }
353
+ }
354
+
355
+ /**
356
+ * A ExprAccessExpr where the expression evaluated is a reusable workflow input read.
357
+ * eg: `${{ inputs.foo}}`
358
+ */
359
+ class ReusableWorkflowInputAccessExpr extends ExprAccessExpr {
360
+ string paramName ;
361
+
362
+ ReusableWorkflowInputAccessExpr ( ) {
363
+ paramName = this .getExpression ( ) .regexpCapture ( "inputs\\.([A-Za-z0-9_-]+)" , 1 )
364
+ }
365
+
366
+ string getParamName ( ) { result = paramName }
367
+
368
+ Expression getInputExpr ( ) {
369
+ exists ( ReusableWorkflowStmt w |
370
+ w .getLocation ( ) .getFile ( ) = this .getLocation ( ) .getFile ( ) and
371
+ w .getInputs ( ) .getInputExpr ( paramName ) = result
309
372
)
310
373
}
311
374
}
0 commit comments