Skip to content

Commit e134107

Browse files
authored
fix: allow expressions in shell: clauses (#1336)
1 parent fadb2de commit e134107

File tree

15 files changed

+104
-33
lines changed

15 files changed

+104
-33
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ rust-version = "1.88.0"
2121
[workspace.dependencies]
2222
anyhow = "1.0.100"
2323
github-actions-expressions = { path = "crates/github-actions-expressions", version = "0.0.11" }
24-
github-actions-models = { path = "crates/github-actions-models", version = "0.38.0" }
24+
github-actions-models = { path = "crates/github-actions-models", version = "0.39.0" }
2525
itertools = "0.14.0"
2626
pest = "2.8.3"
2727
pest_derive = "2.8.3"

crates/github-actions-models/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "github-actions-models"
3-
version = "0.38.0"
3+
version = "0.39.0"
44
description = "Unofficial, high-quality data models for GitHub Actions workflows, actions, and related components"
55
repository = "https://github.com/zizmorcore/zizmor/tree/main/crates/github-actions-models"
66
keywords = ["github", "ci"]

crates/github-actions-models/src/action.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ pub enum StepBody {
152152
run: String,
153153

154154
/// The shell to run in.
155-
shell: String,
155+
shell: LoE<String>,
156156

157157
/// An optional working directory to run from.
158158
working_directory: Option<String>,

crates/github-actions-models/src/workflow/job.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ pub enum StepBody {
134134

135135
/// An optional shell to run in. Defaults to the job or workflow's
136136
/// default shell.
137-
shell: Option<String>,
137+
shell: Option<LoE<String>>,
138138
},
139139
}
140140

crates/github-actions-models/src/workflow/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ pub struct Defaults {
7474
#[derive(Deserialize, Debug)]
7575
#[serde(rename_all = "kebab-case")]
7676
pub struct RunDefaults {
77-
pub shell: Option<String>,
77+
pub shell: Option<LoE<String>>,
7878
pub working_directory: Option<String>,
7979
}
8080

crates/zizmor/src/audit/github_env.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use crate::audit::AuditError;
1212
use crate::config::Config;
1313
use crate::finding::location::Locatable as _;
1414
use crate::finding::{Confidence, Finding, Severity};
15+
use crate::models::StepCommon;
1516
use crate::models::{workflow::JobExt as _, workflow::Step};
1617
use crate::state::AuditState;
1718
use crate::utils;
@@ -383,8 +384,8 @@ impl Audit for GitHubEnv {
383384
if let StepBody::Run { run, .. } = &step.deref().body {
384385
let shell = step.shell().unwrap_or_else(|| {
385386
tracing::warn!(
386-
"github-env: couldn't determine shell type for {workflow}:{job} step {stepno}",
387-
workflow = step.workflow().key.filename(),
387+
"github-env: couldn't determine shell type for {workflow}:{job} step {stepno}; assuming bash",
388+
workflow = step.workflow().key.presentation_path(),
388389
job = step.parent.id(),
389390
stepno = step.index
390391
);
@@ -423,10 +424,23 @@ impl Audit for GitHubEnv {
423424
) -> Result<Vec<Finding<'doc>>, AuditError> {
424425
let mut findings = vec![];
425426

426-
let action::StepBody::Run { run, shell, .. } = &step.body else {
427+
let action::StepBody::Run { run, .. } = &step.body else {
427428
return Ok(findings);
428429
};
429430

431+
let shell = step.shell().unwrap_or_else(|| {
432+
tracing::warn!(
433+
"github-env: couldn't determine shell type for {action} step {stepno}; assuming bash",
434+
action = step.action().key.presentation_path(),
435+
stepno = step.index
436+
);
437+
438+
// The only way shell inference can fail for a `run:` in a
439+
// composition action is if a user specifies an expression instead
440+
// of a string literal. In that case, assume bash.
441+
"bash"
442+
});
443+
430444
// TODO: actually use the spanning information here.
431445
for (dest, _span) in self.uses_github_env(run, shell)? {
432446
findings.push(

crates/zizmor/src/models.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use github_actions_expressions::context;
55
use github_actions_models::common;
66
use github_actions_models::common::Env;
7+
use github_actions_models::common::expr::LoE;
78
use github_actions_models::workflow::job::Strategy;
89

910
use crate::finding::location::Locatable;
@@ -30,7 +31,7 @@ pub(crate) enum StepBodyCommon<'s> {
3031
Run {
3132
run: &'s str,
3233
_working_directory: Option<&'s str>,
33-
_shell: Option<&'s str>,
34+
_shell: Option<&'s LoE<String>>,
3435
},
3536
}
3637

@@ -59,7 +60,10 @@ pub(crate) trait StepCommon<'doc>: Locatable<'doc> + HasInputs {
5960

6061
/// Returns the effective shell for this step, if it can be determined.
6162
/// This includes the step's explicit shell, job defaults, workflow defaults,
62-
/// and runner defaults. Returns `None` if the shell cannot be statically determined.
63+
/// and runner defaults.
64+
///
65+
/// Returns `None` if the shell cannot be statically determined, including
66+
/// if the shell is specified via an expression.
6367
fn shell(&self) -> Option<&str>;
6468
}
6569

crates/zizmor/src/models/action.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
//! providing higher-level APIs for zizmor to use.
55
66
use github_actions_expressions::context;
7-
use github_actions_models::{action, common, workflow::job::Strategy};
7+
use github_actions_models::{
8+
action,
9+
common::{self, expr::LoE},
10+
workflow::job::Strategy,
11+
};
812
use terminal_link::Link;
913

1014
use crate::{
@@ -226,8 +230,12 @@ impl<'doc> StepCommon<'doc> for CompositeStep<'doc> {
226230
}
227231

228232
fn shell(&self) -> Option<&str> {
229-
// For composite action steps, shell is always explicitly specified in the YAML
230-
if let action::StepBody::Run { shell, .. } = &self.inner.body {
233+
// For composite action steps, shell is always explicitly specified in the YAML.
234+
if let action::StepBody::Run {
235+
shell: LoE::Literal(shell),
236+
..
237+
} = &self.inner.body
238+
{
231239
Some(shell)
232240
} else {
233241
None

crates/zizmor/src/models/workflow.rs

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,7 @@ impl<'doc> StepCommon<'doc> for Step<'doc> {
662662
} => StepBodyCommon::Run {
663663
run,
664664
_working_directory: working_directory.as_deref(),
665-
_shell: shell.as_deref(),
665+
_shell: shell.as_ref(),
666666
},
667667
}
668668
}
@@ -713,21 +713,33 @@ impl<'doc> Step<'doc> {
713713
// The steps's own `shell:` takes precedence, followed by the
714714
// job's default, followed by the entire workflow's default,
715715
// followed by the runner's default.
716-
shell
717-
.as_deref()
718-
.or_else(|| {
719-
self.job()
716+
// If any of these is an expression, we can't infer the shell
717+
// statically, so we terminate early with `None`.
718+
let shell = match shell {
719+
Some(LoE::Literal(shell)) => Some(shell.as_str()),
720+
Some(LoE::Expr(_)) => return None,
721+
None => match self
722+
.job()
723+
.defaults
724+
.as_ref()
725+
.and_then(|d| d.run.as_ref().and_then(|r| r.shell.as_ref()))
726+
{
727+
Some(LoE::Literal(shell)) => Some(shell.as_str()),
728+
Some(LoE::Expr(_)) => return None,
729+
None => match self
730+
.workflow()
720731
.defaults
721732
.as_ref()
722-
.and_then(|d| d.run.as_ref().and_then(|r| r.shell.as_deref()))
723-
})
724-
.or_else(|| {
725-
self.workflow()
726-
.defaults
727-
.as_ref()
728-
.and_then(|d| d.run.as_ref().and_then(|r| r.shell.as_deref()))
729-
})
730-
.or_else(|| self.parent.runner_default_shell())
733+
.and_then(|d| d.run.as_ref().and_then(|r| r.shell.as_ref()))
734+
{
735+
Some(LoE::Literal(shell)) => Some(shell.as_str()),
736+
Some(LoE::Expr(_)) => return None,
737+
None => None,
738+
},
739+
},
740+
};
741+
742+
shell.or_else(|| self.parent.runner_default_shell())
731743
}
732744
}
733745

0 commit comments

Comments
 (0)