@@ -39,23 +39,21 @@ class WorkflowStmt extends Statement instanceof Actions::Workflow {
3939}
4040
4141class ReusableWorkflowStmt extends WorkflowStmt {
42- YamlMapping parameters ;
42+ YamlValue workflow_call ;
4343
4444 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
4946 }
5047
51- ParamsStmt getParams ( ) { result = parameters }
48+ InputsStmt getInputs ( ) { result = workflow_call .( YamlMapping ) .lookup ( "inputs" ) }
49+
50+ OutputsStmt getOutputs ( ) { result = workflow_call .( YamlMapping ) .lookup ( "outputs" ) }
5251
53- // TODO: implemnt callable name
5452 string getName ( ) { result = this .getLocation ( ) .getFile ( ) .getRelativePath ( ) }
5553}
5654
57- class ParamsStmt extends Statement instanceof YamlMapping {
58- ParamsStmt ( ) {
55+ class InputsStmt extends Statement instanceof YamlMapping {
56+ InputsStmt ( ) {
5957 exists ( Actions:: On on | on .getNode ( "workflow_call" ) .( YamlMapping ) .lookup ( "inputs" ) = this )
6058 }
6159
@@ -72,12 +70,38 @@ class ParamsStmt extends Statement instanceof YamlMapping {
7270 * token:
7371 * required: true
7472 */
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 , _)
7776 }
7877}
7978
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 { }
81105
82106/**
83107 * A Job is a collection of steps that run in an execution environment.
@@ -117,8 +141,13 @@ class JobStmt extends Statement instanceof Actions::Job {
117141
118142 /**
119143 * Reusable workflow jobs may have Uses children
144+ * eg:
145+ * call-job:
146+ * uses: ./.github/workflows/reusable_workflow.yml
147+ * with:
148+ * arg1: value1
120149 */
121- JobUsesExpr getUsesExpr ( ) { result = this . ( Actions :: Job ) . lookup ( "uses" ) }
150+ JobUsesExpr getUsesExpr ( ) { result . getJob ( ) = this }
122151}
123152
124153/**
@@ -152,8 +181,11 @@ class StepStmt extends Statement instanceof Actions::Step {
152181 JobStmt getJob ( ) { result = super .getJob ( ) }
153182}
154183
184+ /**
185+ * Abstract class representing a call to a 3rd party action or reusable workflow.
186+ */
155187abstract class UsesExpr extends Expression {
156- abstract string getTarget ( ) ;
188+ abstract string getCallee ( ) ;
157189
158190 abstract string getVersion ( ) ;
159191
@@ -168,7 +200,7 @@ class StepUsesExpr extends StepStmt, UsesExpr {
168200
169201 StepUsesExpr ( ) { uses .getStep ( ) = this }
170202
171- override string getTarget ( ) { result = uses .getGitHubRepository ( ) }
203+ override string getCallee ( ) { result = uses .getGitHubRepository ( ) }
172204
173205 override string getVersion ( ) { result = uses .getVersion ( ) }
174206
@@ -183,12 +215,12 @@ class StepUsesExpr extends StepStmt, UsesExpr {
183215/**
184216 * A Uses step represents a call to an action that is defined in a GitHub repository.
185217 */
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+ }
190222
191- JobStmt getJob ( ) { result = job }
223+ JobStmt getJob ( ) { result = this }
192224
193225 /**
194226 * 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 {
200232
201233 private string pathUsesParser ( ) { result = "\\./(.+)" }
202234
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 )
208240 else
209241 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 )
213245 )
214246 }
215247
216248 /** Gets the version reference used when checking out the Action, e.g. `v2` in `actions/checkout@v2`. */
217249 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 )
222254 else none ( )
223255 )
224256 }
225257
226258 override Expression getArgument ( string key ) {
227- job .( YamlMapping ) .lookup ( "with" ) .( YamlMapping ) .lookup ( key ) = result
259+ this .( YamlMapping ) .lookup ( "with" ) .( YamlMapping ) .lookup ( key ) = result
228260 }
229261}
230262
@@ -287,16 +319,19 @@ class StepOutputAccessExpr extends ExprAccessExpr {
287319/**
288320 * A ExprAccessExpr where the expression evaluated is a job output read.
289321 * eg: `${{ needs.job1.outputs.foo}}`
322+ * eg: `${{ jobs.job1.outputs.foo}}` (for reusable workflows)
290323 */
291324class JobOutputAccessExpr extends ExprAccessExpr {
292325 string jobId ;
293326 string varName ;
294327
295328 JobOutputAccessExpr ( ) {
296329 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
298332 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 )
300335 }
301336
302337 string getVarName ( ) { result = varName }
@@ -305,7 +340,35 @@ class JobOutputAccessExpr extends ExprAccessExpr {
305340 exists ( JobStmt job |
306341 job .getId ( ) = jobId and
307342 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
309372 )
310373 }
311374}
0 commit comments