@@ -17,6 +17,8 @@ module pkl.impl.ghactions.PklCI
1717
1818import "@com.github.actions/actions/checkout/v6/Checkout.pkl"
1919import "@com.github.actions/context.pkl"
20+ import "@com.github.actions/Job.pkl"
21+ import "@com.github.actions/Step.pkl"
2022import "@com.github.actions/Workflow.pkl"
2123import "@com.github.dependabot/v2/Dependabot.pkl"
2224import "@pkl.github.dependabotManagedActions/DependabotManagedActions.pkl"
@@ -54,7 +56,7 @@ catalog: Catalog
5456/// * [Workflow.concurrency]
5557///
5658/// This turns into a workflow called "Pull Request".
57- prb : Workflow
59+ prb : Workflow(isValid)
5860
5961/// The workflow to run for commits that land on the main branch.
6062///
@@ -65,7 +67,7 @@ prb: Workflow
6567/// * [Workflow.concurrency]
6668///
6769/// This turns into a workflow called "Build (main)".
68- main : Workflow
70+ main : Workflow(isValid)
6971
7072/// The workflow to run for commits that land on all branches except for `main` and `release/*`
7173///
@@ -76,7 +78,7 @@ main: Workflow
7678/// * [Workflow.concurrency]
7779///
7880/// This turns into a workflow called "Build".
79- build : Workflow
81+ build : Workflow(isValid)
8082
8183/// The workflow that runs when tags are pushed.
8284///
@@ -87,7 +89,7 @@ build: Workflow
8789/// * [Workflow.concurrency]
8890///
8991/// This turns into a workflow called "Release".
90- release : Workflow?
92+ release : Workflow(isValid) ?
9193
9294/// Tag glob patterns to ignore for releases
9395releaseIgnoreTags : Listing<String>?
@@ -101,7 +103,7 @@ releaseIgnoreTags: Listing<String>?
101103/// * [Workflow.concurrency]
102104///
103105/// This turns into a workflow called "Build (release branch)".
104- releaseBranch : Workflow?
106+ releaseBranch : Workflow(isValid) ?
105107
106108/// Test reports produced by [build] and [release].
107109///
@@ -133,7 +135,7 @@ triggerDocsBuild: "none" | "release" | "both" = "none"
133135triggerPackageDocsBuild: "none" | "main" | "release" = "none"
134136
135137/// Any additional workflows beyond the built-in ones (prb, build, release, etc)
136- workflows : Mapping<String, Workflow>
138+ workflows : Mapping<String, Workflow(isValid) >
137139
138140/// Any additional dependabot configuration beyond GitHub Actions
139141dependabot : Dependabot
@@ -332,14 +334,14 @@ local testReportWorkflow: Workflow = new {
332334 ["path" ] = "artifacts"
333335 ["name" ] = "test-results-.*"
334336 ["name_is_regexp" ] = true
335- ["run_id" ] = "${{ github.event. workflow_run.id }}"
337+ ["run_id" ] = context. github.event( " workflow_run.id" )
336338 }
337339 }
338340 new PublishUnitTestResult {
339341 name = "Publish test results"
340342 with {
341- commit = "${{ github.event. workflow_run.head_sha }}"
342- event_name = "${{ github.event. workflow_run.event }}"
343+ commit = context. github.event( " workflow_run.head_sha" )
344+ event_name = context. github.event( " workflow_run.event" )
343345 event_file = "artifacts/\(TEST_RESULT_EVENT_FILE_ARTIFACT_NAME) /event.json"
344346 file_patterns { "artifacts/**/*.xml" }
345347 comment_mode = "off"
@@ -485,6 +487,38 @@ local withPublishTestResults: Mixin<Workflow.Jobs> = (it) ->
485487 }
486488 }
487489
490+ /// Validate a Workflow; for use as a type constraint.
491+ ///
492+ /// Validations
493+ /// * Steps must not contain known template injection vectors.
494+ hidden const isValid: (Workflow) -> Boolean = (workflow) ->
495+ let (
496+ errors =
497+ workflow.jobs
498+ .toMap()
499+ .entries
500+ .flatMap((job) ->
501+ if (job.value is Job)
502+ job.value.steps
503+ .toList()
504+ .flatMapIndexed((stepIndex, step) ->
505+ if (step is Step && step.run != null && step.run.contains("${{" ))
506+ // there are other actions that have fields susceptible to template injection
507+ // but we don't use them
508+ // the zizmor project has a great tool for identifying these: https://github.com/zizmorcore/zizmor/blob/main/support/codeql-injection-sinks.py
509+ // and a few manually maintained entries: https://github.com/zizmorcore/zizmor/blob/cdc816b480a895144197fab903efc819810aca17/crates/zizmor/src/audit/template_injection.rs#L59-L74
510+ List(
511+ "[job:\(job.key) step:\(stepIndex) field:run] Potential template injection, use expressions via `env` instead"
512+ )
513+ else
514+ List()
515+ )
516+ else
517+ List()
518+ )
519+ )
520+ if (errors.isEmpty) true else throw ("Invalid workflow:\n\(errors.join("\n") )" )
521+
488522local function withTriggerWorkflows (triggerType : String ): Mixin<Workflow.Jobs> = (it) -> (it) {
489523 when (
490524 triggerType == triggerDocsBuild
@@ -514,14 +548,16 @@ local function withTriggerWorkflows(triggerType: String): Mixin<Workflow.Jobs> =
514548 new {
515549 name = "Trigger pkl-lang.org build"
516550 env {
517- ["GH_TOKEN" ] = "${{ steps.app-token.outputs.token }}"
551+ ["GH_TOKEN" ] = context.steps.outputs("app-token" , "token" )
552+ ["SOURCE_RUN" ] =
553+ "\(context.github.serverUrl) /\(context.github.repository) /actions/runs/\(context.github.runId) "
518554 }
519555 run =
520556 #"""
521557 gh workflow run \
522558 --repo apple/pkl-lang.org \
523559 --ref main \
524- --field source_run="\#(context.github.serverUrl) / \#(context.github.repository) /actions/runs/ \#(context.github.runId) " \
560+ --field source_run="${SOURCE_RUN} " \
525561 main.yml
526562 """#
527563 }
@@ -530,14 +566,16 @@ local function withTriggerWorkflows(triggerType: String): Mixin<Workflow.Jobs> =
530566 new {
531567 name = "Trigger pkl-package-docs build"
532568 env {
533- ["GH_TOKEN" ] = "${{ steps.app-token.outputs.token }}"
569+ ["GH_TOKEN" ] = context.steps.outputs("app-token" , "token" )
570+ ["SOURCE_RUN" ] =
571+ "\(context.github.serverUrl) /\(context.github.repository) /actions/runs/\(context.github.runId) "
534572 }
535573 run =
536574 #"""
537575 gh workflow run \
538576 --repo apple/pkl-package-docs \
539577 --ref main \
540- --field source_run="\#(context.github.serverUrl) / \#(context.github.repository) /actions/runs/ \#(context.github.runId) " \
578+ --field source_run="${SOURCE_RUN} " \
541579 main.yml
542580 """#
543581 }
0 commit comments