Extract yamlpatch into a support crate (#1001)

Co-authored-by: William Woodruff <william@yossarian.net>
This commit is contained in:
Mostafa Moradian 2025-07-03 01:35:48 +02:00 committed by GitHub
parent 32558743e2
commit 8f7e3eeb8d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 3429 additions and 3210 deletions

View file

@ -7,6 +7,7 @@ on:
- "github-actions-expressions/v*"
- "github-actions-models/v*"
- "yamlpath/v*"
- "yamlpatch/v*"
workflow_dispatch:
inputs:
package-name:

16
Cargo.lock generated
View file

@ -3663,6 +3663,21 @@ version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
[[package]]
name = "yamlpatch"
version = "0.1.0"
dependencies = [
"indexmap",
"insta",
"line-index",
"pretty_assertions",
"serde",
"serde_json",
"serde_yaml",
"thiserror 2.0.12",
"yamlpath",
]
[[package]]
name = "yamlpath"
version = "0.23.1"
@ -3843,5 +3858,6 @@ dependencies = [
"tree-sitter",
"tree-sitter-bash",
"tree-sitter-powershell",
"yamlpatch",
"yamlpath",
]

View file

@ -3,6 +3,7 @@ resolver = "2"
members = [
"crates/github-actions-expressions",
"crates/github-actions-models",
"crates/yamlpatch",
"crates/yamlpath",
"crates/zizmor",
]
@ -64,6 +65,7 @@ tree-sitter = "0.25.6"
tree-sitter-bash = "0.23.3"
tree-sitter-powershell = "0.25.6"
yamlpath = { path = "crates/yamlpath", version = "0.23.1" }
yamlpatch = { path = "crates/yamlpatch", version = "0.1.0" }
tree-sitter-yaml = "0.7.1"
[workspace.lints.clippy]

View file

@ -0,0 +1,30 @@
[package]
name = "yamlpatch"
version = "0.1.0"
description = "Comment and format-preserving YAML patch operations"
repository = "https://github.com/zizmorcore/zizmor/tree/main/crates/yamlpatch"
keywords = ["yaml", "patch"]
authors = [
"Mostafa Moradian <mstfmoradian@gmail.com>",
"William Woodruff <william@yossarian.net>"
]
homepage.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true
[dependencies]
indexmap.workspace = true
line-index.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
serde_yaml.workspace = true
thiserror.workspace = true
yamlpath.workspace = true
[dev-dependencies]
insta.workspace = true
pretty_assertions.workspace = true

21
crates/yamlpatch/LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2025-present, Mostafa Moradian <mstfmoradian @ gmail.com>, William Woodruff <william @ yossarian.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,56 @@
# yamlpatch
[![CI](https://github.com/zizmorcore/zizmor/actions/workflows/ci.yml/badge.svg)](https://github.com/zizmorcore/zizmor/actions/workflows/ci.yml)
[![Crates.io](https://img.shields.io/crates/v/yamlpatch)](https://crates.io/crates/yamlpatch)
[![docs.rs](https://img.shields.io/docsrs/yamlpatch)](https://docs.rs/yamlpatch)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/woodruffw?style=flat&logo=githubsponsors&labelColor=white&color=white)](https://github.com/sponsors/woodruffw)
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.com/invite/PGU3zGZuGG)
Comment and format-preserving YAML patch operations.
`yamlpatch` builds on [`yamlpath`] to provide surgical modification capabilities
while preserving comments, formatting, and structure.
[`yamlpath`]: https://github.com/zizmorcore/zizmor/tree/main/crates/yamlpath
> [!IMPORTANT]
>
> This is not a substitute for comprehensive YAML processing libraries.
> It's designed for targeted modifications that preserve the original
> document's formatting and comments.
## Why?
When working with YAML configuration files, it's often necessary to make
programmatic changes while preserving the human-readable aspects of the
file: comments, formatting, indentation, and style choices.
Traditional YAML processing involves parsing to a document model, making
changes, and re-serializing. This approach *destroys* the original formatting
and comments, making the result less suitable for version control and
human review.
`yamlpatch` solves this by providing targeted patch operations that:
- Preserve comments and their positioning
- Maintain original indentation and formatting
- Respect different YAML styles (block vs. flow, single vs. multi-line)
- Support precise fragment rewriting within string values
- Handle complex nested structures gracefully
## Operations
`yamlpatch` supports several types of patch operations:
- **Replace**: Replace a value at a specific path
- **Add**: Add new key-value pairs to mappings
- **Remove**: Remove keys or elements
- **MergeInto**: Merge values into existing mappings
- **RewriteFragment**: Rewrite portions of string values (useful for templating)
Each operation is designed to work with the existing document structure
and formatting, making minimal changes while achieving the desired result.
## License
MIT License.

1041
crates/yamlpatch/src/lib.rs Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -70,6 +70,7 @@ tree-sitter.workspace = true
tree-sitter-bash.workspace = true
tree-sitter-powershell.workspace = true
yamlpath.workspace = true
yamlpatch.workspace = true
[build-dependencies]
csv.workspace = true

View file

@ -8,8 +8,8 @@ use crate::{
models::{StepBodyCommon, StepCommon, uses::RepositoryUsesExt as _},
state::AuditState,
utils::split_patterns,
yaml_patch::{Op, Patch},
};
use yamlpatch::{Op, Patch};
pub(crate) struct Artipacked;
@ -148,7 +148,7 @@ impl Artipacked {
key: step.location().key,
disposition: Default::default(),
patches: vec![Patch {
route: step.route(),
route: step.route().into(),
operation: Op::MergeInto {
key: "with".to_string(),
updates: indexmap::IndexMap::from_iter([(

View file

@ -17,8 +17,8 @@ use crate::{
},
models::workflow::{JobExt, Workflow},
utils::ExtractedExpr,
yaml_patch::{Op, Patch},
};
use yamlpatch::{Op, Patch};
pub(crate) struct BotConditions;
@ -385,7 +385,7 @@ impl BotConditions {
key: &workflow.key,
disposition: FixDisposition::Safe,
patches: vec![Patch {
route: if_route,
route: if_route.into(),
operation: Op::RewriteFragment {
from: spoofable_context_raw.into(),
to: safe_context.into(),

View file

@ -37,8 +37,8 @@ use crate::{
},
state::AuditState,
utils::{DEFAULT_ENVIRONMENT_VARIABLES, ExtractedExpr, extract_expressions},
yaml_patch::{Op, Patch},
};
use yamlpatch::{Op, Patch};
pub(crate) struct TemplateInjection;
@ -284,7 +284,7 @@ impl TemplateInjection {
let mut patches = vec![];
patches.push(Patch {
route: step.route().with_keys(&["run".into()]),
route: step.route().with_keys(&["run".into()]).into(),
operation: Op::RewriteFragment {
from: raw.as_raw().to_string().into(),
to: format!("${{{env_var}}}").into(),
@ -300,7 +300,7 @@ impl TemplateInjection {
.contains(&env_var.as_str())
{
patches.push(Patch {
route: step.route(),
route: step.route().into(),
operation: Op::MergeInto {
key: "env".to_string(),
updates: indexmap::IndexMap::from_iter([(

View file

@ -5,11 +5,8 @@ use clap::ValueEnum;
use serde::{Deserialize, Serialize};
use self::location::{Location, SymbolicLocation};
use crate::{
InputKey,
models::AsDocument,
yaml_patch::{self, Patch},
};
use crate::{InputKey, models::AsDocument};
use yamlpatch::{self, Patch};
pub(crate) mod location;
@ -137,7 +134,7 @@ impl Fix<'_> {
&self,
document: &yamlpath::Document,
) -> anyhow::Result<yamlpath::Document> {
match yaml_patch::apply_yaml_patches(document, &self.patches) {
match yamlpatch::apply_yaml_patches(document, &self.patches) {
Ok(new_document) => Ok(new_document),
Err(e) => Err(anyhow!("fix failed: {e}")),
}

View file

@ -96,6 +96,20 @@ impl<'doc> From<Vec<RouteComponent<'doc>>> for Route<'doc> {
}
}
impl<'doc> From<Route<'doc>> for yamlpatch::Route<'doc> {
fn from(route: Route<'doc>) -> Self {
let yamlpatch_components: Vec<yamlpatch::RouteComponent<'doc>> = route
.components
.iter()
.map(|comp| match comp {
RouteComponent::Key(key) => yamlpatch::RouteComponent::Key(key),
RouteComponent::Index(idx) => yamlpatch::RouteComponent::Index(*idx),
})
.collect();
yamlpatch::Route::from(yamlpatch_components)
}
}
#[macro_export]
macro_rules! route {
($($key:expr),* $(,)?) => {

View file

@ -38,7 +38,6 @@ mod output;
mod registry;
mod state;
mod utils;
mod yaml_patch;
// TODO: Dedupe this with the top-level `sponsors.json` used by the
// README + docs site.

File diff suppressed because it is too large Load diff