Fix exit code to return 0 when all findings are auto-fixable (#1242)

Co-authored-by: William Woodruff <william@yossarian.net>
This commit is contained in:
Chase Naples 2025-10-14 20:57:34 -04:00 committed by GitHub
parent a4c6c3bb9f
commit eeac63b339
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 101 additions and 18 deletions

View file

@ -769,12 +769,24 @@ fn run(app: &mut App) -> Result<ExitCode, Error> {
} }
}; };
if let Some(fix_mode) = app.fix { let all_fixed = if let Some(fix_mode) = app.fix {
output::fix::apply_fixes(fix_mode, &results, &registry).map_err(Error::Fix)?; let fix_result =
} output::fix::apply_fixes(fix_mode, &results, &registry).map_err(Error::Fix)?;
// If all findings have applicable fixes and all were successfully applied,
// we should exit with success.
results.all_findings_have_applicable_fixes(fix_mode)
&& fix_result.failed_count == 0
&& fix_result.applied_count > 0
} else {
false
};
if app.no_exit_codes || matches!(app.format, OutputFormat::Sarif) { if app.no_exit_codes || matches!(app.format, OutputFormat::Sarif) {
Ok(ExitCode::SUCCESS) Ok(ExitCode::SUCCESS)
} else if all_fixed {
// All findings were auto-fixed, no manual intervention needed
Ok(ExitCode::SUCCESS)
} else { } else {
Ok(results.exit_code()) Ok(results.exit_code())
} }

View file

@ -13,12 +13,21 @@ use crate::{
registry::{FindingRegistry, input::InputKey, input::InputRegistry}, registry::{FindingRegistry, input::InputKey, input::InputRegistry},
}; };
/// Result of applying fixes.
#[derive(Debug)]
pub struct FixResult {
/// Number of fixes that were successfully applied.
pub applied_count: usize,
/// Number of fixes that failed to apply.
pub failed_count: usize,
}
/// Apply all fixes associated with findings, filtered by the specified fix mode. /// Apply all fixes associated with findings, filtered by the specified fix mode.
pub fn apply_fixes( pub fn apply_fixes(
fix_mode: FixMode, fix_mode: FixMode,
results: &FindingRegistry, results: &FindingRegistry,
registry: &InputRegistry, registry: &InputRegistry,
) -> Result<()> { ) -> Result<FixResult> {
let mut fixes_by_input: HashMap<&InputKey, Vec<(&Fix, &Finding)>> = HashMap::new(); let mut fixes_by_input: HashMap<&InputKey, Vec<(&Fix, &Finding)>> = HashMap::new();
let mut total_fixes = 0; let mut total_fixes = 0;
for finding in results.fixable_findings() { for finding in results.fixable_findings() {
@ -46,12 +55,16 @@ pub fn apply_fixes(
} else { } else {
anstream::eprintln!("No fixes available to apply."); anstream::eprintln!("No fixes available to apply.");
} }
return Ok(()); return Ok(FixResult {
applied_count: 0,
failed_count: 0,
});
} }
// Process each file // Process each file
let mut applied_fixes = Vec::new(); let mut applied_fixes = Vec::new();
let mut failed_fixes = Vec::new(); let mut failed_fixes = Vec::new();
let mut total_applied = 0;
for (input_key, fixes) in &fixes_by_input { for (input_key, fixes) in &fixes_by_input {
let InputKey::Local(local) = input_key else { let InputKey::Local(local) = input_key else {
@ -72,6 +85,7 @@ pub fn apply_fixes(
Ok(new_document) => { Ok(new_document) => {
current_document = new_document; current_document = new_document;
file_applied_fixes.push((finding.ident, fix, finding)); file_applied_fixes.push((finding.ident, fix, finding));
total_applied += 1;
} }
Err(e) => { Err(e) => {
// If the fix fails on modified content, it might be due to conflicts // If the fix fails on modified content, it might be due to conflicts
@ -101,7 +115,10 @@ pub fn apply_fixes(
print_summary(&applied_fixes, &failed_fixes); print_summary(&applied_fixes, &failed_fixes);
} }
Ok(()) Ok(FixResult {
applied_count: total_applied,
failed_count: failed_fixes.len(),
})
} }
fn print_summary( fn print_summary(

View file

@ -199,6 +199,30 @@ impl<'a> FindingRegistry<'a> {
}) })
} }
/// Checks if all findings have at least one fix matching the given fix mode.
///
/// Returns true if every finding has at least one applicable fix based on the mode,
/// meaning no manual intervention would be required if all fixes are applied successfully.
pub(crate) fn all_findings_have_applicable_fixes(&self, fix_mode: crate::FixMode) -> bool {
use crate::finding::FixDisposition;
if self.findings.is_empty() {
return true;
}
self.findings.iter().all(|finding| {
finding.fixes.iter().any(|fix| {
let disposition_matches = match fix_mode {
crate::FixMode::Safe => matches!(fix.disposition, FixDisposition::Safe),
crate::FixMode::UnsafeOnly => matches!(fix.disposition, FixDisposition::Unsafe),
crate::FixMode::All => true,
};
disposition_matches && matches!(fix.key, InputKey::Local(_))
})
})
}
/// All ignored findings. /// All ignored findings.
pub(crate) fn ignored(&self) -> &[Finding<'a>] { pub(crate) fn ignored(&self) -> &[Finding<'a>] {
&self.ignored &self.ignored

View file

@ -9,6 +9,14 @@ of `zizmor`.
## Next (UNRELEASED) ## Next (UNRELEASED)
### Enhancements 🌱
* When running in `--fix` mode and all fixes are successfully applied,
`zizmor` now has similar [exit code] behavior as the `--no-exit-codes`
and `--format=sarif` flags (#1242)
Many thanks to @cnaples79 for implementing this improvement!
## 1.15.2 ## 1.15.2
### Bug Fixes 🐛 ### Bug Fixes 🐛
@ -1161,3 +1169,5 @@ This is one of `zizmor`'s bigger recent releases! Key enhancements include:
[ref-version-mismatch]: ./audits.md#ref-version-mismatch [ref-version-mismatch]: ./audits.md#ref-version-mismatch
[dependabot-execution]: ./audits.md#dependabot-execution [dependabot-execution]: ./audits.md#dependabot-execution
[dependabot-cooldown]: ./audits.md#dependabot-cooldown [dependabot-cooldown]: ./audits.md#dependabot-cooldown
[exit code]: ./usage.md#exit-codes

View file

@ -393,18 +393,8 @@ annotations.
## Exit codes ## Exit codes
!!! note `zizmor` will exit with a variety of exit codes, depending on how
you invoke it and what happens during the run. In general:
Exit codes 11 and above are **not used** if `--no-exit-codes` or
`--format sarif` is passed.
!!! warning "Removal"
Versions of zizmor prior to `v1.14.0` used exit code `10` to indicate
the highest finding having "unknown" severity. This exit code is
no longer used as of `v1.14.0`.
`zizmor` uses various exit codes to summarize the results of a run:
| Code | Meaning | | Code | Meaning |
| ---- | ------- | | ---- | ------- |
@ -418,6 +408,36 @@ annotations.
All other exit codes are currently reserved. All other exit codes are currently reserved.
!!! warning "Removal"
Versions of zizmor prior to `v1.14.0` used exit code `10` to indicate
the highest finding having "unknown" severity. This exit code is
no longer used as of `v1.14.0`.
Beyond this general table, the following modes of operation also
change the exit code behavior:
* If you run with `--no-exit-codes`, `zizmor` will **not** use exit codes 11
and above.
* If you use `--format=sarif`, `zizmor` will **not** use exit codes 11 and
above.
!!! tip "Why?"
Because SARIF consumers generally don't expect SARIF producers
(like `zizmor`) to exit with a non-zero code *except* in the case
of an internal failure. SARIF consumers expect semantic results
to be communicated within the SARIF itself.
* If you run with `--fix` *and* all findings are successfully auto-fixed,
`zizmor` will **not** use exit codes 11 and above.
!!! tip "Why?"
Because the higher exit codes indicate that action needs to be taken,
but a successful application of all fixes means that no action
is required.
## Using personas ## Using personas
!!! tip !!! tip