Skip to content

Replace serde_yaml with serde-saphyr to drop unsafe-libyaml - #142

Open
dduugg wants to merge 1 commit into
mainfrom
replace-serde-yaml-with-serde-saphyr
Open

dduugg wants to merge 1 commit into
mainfrom
replace-serde-yaml-with-serde-saphyr

Conversation

@dduugg

@dduugg dduugg commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #141.

unsafe-libyaml is unmaintained (RUSTSEC-2024-0449) and reaches us only through serde_yaml, which is itself deprecated. This replaces serde_yaml with serde-saphyr 2.0.0, the crate the advisory names for serde users. serde-saphyr and its parser, granit-parser, are pure Rust with #![forbid(unsafe_code)], though granit-parser depends on arraydeque, which uses unsafe. serde-saphyr is actively maintained: 32 releases in the past year, outside issues closed in a median of under a day, and kube-rs among its 400+ dependents. Both crates have a single maintainer, though, and their security policy covers only the latest release. 2.0.0 shipped about ten weeks after 1.0.0, so we should expect to take major versions promptly, which is why Dependabot now opens serde-saphyr bumps on their own.

Alternatives considered

Crate Why not
noyalib 0.0.58 (the advisory's other suggestion) 50 of the 178 existing tests fail, even through its serde_yaml compatibility feature, mostly on the empty unowned_globs: that four fixtures use. It is still 0.0.x, so every release is semver-incompatible with the last.
serde_yaml_ng, serde_norway, serde_yaml_bw Still parse through unsafe-libyaml or a fork of it.
saphyr, yaml-rust2 No serde support, so every struct would need hand-written mapping code.

What changed

All YAML parsing goes through the new src/yaml.rs:

  • Callers read the file with fs::read and parse it with from_slice_with_options, which accepts the same encodings serde_yaml did (UTF-8, with or without a BOM). from_reader would also accept UTF-16 and silently replace invalid UTF-8 after a BOM, and it parses about 2.3x slower than serde_yaml.
  • Each option that differs from serde-saphyr's defaults has a one-line reason next to it. The one that matters is duplicate_keys: LastWins. The default rejects a repeated key anywhere in a file, including keys we never read, and one such package.yml would abort generate and validate for the whole repo. These exist in the wild: this public packwerk app repeats enforce_privacy. The other options keep serde_yaml's behavior for <<, for !!binary, for .inf and .nan in values we ignore, for YAML 1.1 booleans (yes and no stay errors), and for single-line error messages.
  • Resource budgets are tighter than serde-saphyr's defaults: nesting depth 16 instead of 64, and 20,000 nodes instead of 250,000. Caps on scalar bytes, parser events and anchor copies grow with the node limit. At the default the scalar-byte and anchor caps are tighter than serde-saphyr's (about 5 MB of scalars), and the event cap equals its 1,000,000, so raising the node limit never hits a fixed event cap first. LastWins buffers each mapping read into one of our structs before serde sees it, and buffers a nested mapping again at each level where it follows an integer key, so under the defaults a crafted 480 KB package.yml made generate peak at about 2.7 GB, against 35 MB on main. Under these budgets that file fails with a budget error. The worst file found that fits, 68 KB of 14 nested mappings that each start with 0: x around 9,750 []: [] entries, holds about 115 MB of heap at its peak and makes generate peak at about 290 MB on macOS, against 12 MB on main. With only string keys the same nesting holds about 12 MB of heap. The depth limit also keeps nested input from overflowing the stack. The 1,015 team and package.yml files in two large private monorepos nest at most 5 deep, with fewer than 2,200 nodes and no anchors, so the node limit leaves 9x headroom. It is the new yaml_max_nodes config key, so a repo whose files outgrow it can raise it, and the caps that grow with it, without waiting for a release. A file over the node limit or any of those caps fails like a malformed file, with a hint naming the key. The worst case grows with the limit, by about 14 KB of resident memory per node, so a 50,000-node default would give 23x headroom but raise it to about 710 MB. The 290 MB and 710 MB figures are for a file crafted to waste memory. Ordinary files add almost nothing: on the larger monorepo, generate peaks at the same memory as main, within about 2%.
  • serde-saphyr rejects an empty, ~ or null value in a String field or list item, which serde_yaml accepted. Two small deserialize_with helpers keep accepting them, reading all three as "", where serde_yaml kept ~ and null as text. The list helper also reads an empty, ~ or null list as empty itself, rather than relying on serde-saphyr to, and package.yml is read as an optional struct, so an empty file still means no owner. serde-saphyr's master branch (2.1.0, not yet released) stops reading null as an empty list (serde-saphyr#233) or as an empty struct, and our 2.0.0 requirement admits 2.1. The tests pass against that branch too.
  • The config tests and the helper in src/common_test.rs now go through Config::load_from_path, so they exercise the production path.
  • 28 new tests pin these behaviors, 13 of them directly in src/yaml.rs, next to the options they cover. Ten assert what serde_yaml already did (checked by running the same assertions on main). The other 18 pin intended changes: a repeated owner takes the last value; a BOM before several keys now parses; a ~ or null list reads as empty, also through the list helper on its own; an empty, ~, null or ... package.yml means no owner; 16 levels of nesting parse and 17 don't; and a list of 100,000 items is rejected unless yaml_max_nodes, read from the config, raises the limit. That last part is checked through generate, for-file and for-file --from-codeowners, and for the team cache and the team map that --from-codeowners memoizes, which are both keyed on the limit. The rest check that each cap that should grow with yaml_max_nodes does, that a breach of the node limit or any of those caps, even through an alias, gets the hint while a depth breach doesn't, and that an error inside an alias is printed once.
  • Mutation testing backs this up. Of 94 mutations to the options, each budget field and multiplier, the hint, the helpers, all 15 deserialize_with attributes and the 19 places that pass yaml_max_nodes on, 89 fail at least one test. The 5 that pass change nothing serde-saphyr 2.0.0 lets a test observe: two other event-cap multipliers, since the event cap is reached first only at a limit of 0, where every multiplier gives 0; non_finite_float_policy: AsString, since we read no floats; unwrapping one AliasError instead of looping, since serde-saphyr nests at most one; and reading package.yml as the struct instead of an Option, which the empty-file test catches under serde-saphyr 2.1.
  • Dependabot's cargo group now excludes serde-saphyr, so its bumps arrive as their own pull requests and never wait on unrelated breakage. granit-parser is a transitive dependency, which Dependabot version updates skip, so a granit-parser fix reaches Cargo.lock when a serde-saphyr release requires it, through a RustSec advisory, or through a manual cargo update -p granit-parser.
  • Release archives now also hold a licenses/ directory next to the codeowners binary, which stays at the archive root, where dotslash-config.json looks for it. The new dev/license-notices script copies into it, verbatim, the license files of two crates serde-saphyr brings in, whose licenses require redistributions of the binary to carry their notices: granit-parser, whose LICENSE asks for all inherited and new notices, and encoding_rs, whose WHATWG-derived data is under BSD-3-Clause. The script takes their versions from Cargo.lock and fails if a listed crate leaves the dependency graph or ships no license files. The jobs that build release archives run only on main after a version bump, so a new step in the Check job runs the script on pull requests. Today's macOS binaries don't link encoding_rs, since we parse only byte slices, so its notice is a precaution for a later switch to reader input. The README's installation section mentions the new directory.

Behavior changes

Verified unchanged:

  • Exit codes, stdout and every CODEOWNERS file written, across 74 CLI runs in 37 small projects: valid and broken config, team and package.yml files, the empty-file cases in the last bullet, and all 13 fixtures (including docs/CODEOWNERS from the custom-path fixture).
  • 2,356 real files parse to the same values, or fail in both: the 42 fixtures, 1,134 public package.yml, team and config files from GitHub, and about 1,200 files from nine private repositories. The one exception is two public GitHub Actions workflows named package.yml that start with a UTF-8 BOM, which serde_yaml rejected and serde-saphyr accepts. For the seven private repositories with a config, generate gives byte-identical CODEOWNERS, stdout, stderr and exit codes.
  • With the final budgets: generate on two large private monorepos, which parses 1,015 team and package.yml files, gives byte-identical CODEOWNERS, empty stdout and stderr and exit code 0, the same as main, and peaks at the same memory as main, within about 2%. validate, crosscheck-owners, and for-file with and without --from-codeowners on 1,719 sampled files also give the same exit codes and output lines as main. None of a wider local corpus of 1,615 package.yml, team and config files, or the 42 fixtures, hits a budget.
  • An empty, comment-only or ----only package.yml still means "no owner", also under serde-saphyr 2.1.

Changed: error text on every failure path, and a handful of rare inputs that are now rejected, accepted, or read differently. Apart from the error text of files that fail in both, and the two BOM files above, none of these occur in the 2,356 files.

Every change, with examples

Error text changes on every failure path. Messages no longer start with the key path, type errors no longer say what was found, and locations read line L, column C, including the line 1, column 1 that serde_yaml left out:

Input Before After
team file without github missing field `github` missing field `github` at line 1, column 1
owned_globs: 5 owned_globs: invalid type: integer `5`, expected a sequence at line 1 column 14 expected sequence start at line 1, column 14
do_not_add_to_codeowners_file: yes github.do_not_add_to_codeowners_file: invalid type: string "yes", expected a boolean at line 4 column 34 invalid boolean (strict mode expects true/false) at line 4, column 34
empty team file missing field `name` unexpected end of input at line 1, column 1
unterminated quote found unexpected end of stream at line 4 column 1, while scanning a quoted scalar at line 3 column 9 invalid indentation in multiline quoted scalar at line 4, column 1
a second document deserializing from YAML containing more than one document is not supported multiple YAML documents detected; use from_str_multiple or from_str_multiple_with_options at line 3, column 1
~: x at the top level (accepted) cannot deserialize null into string; use Option<String> at line 1, column 1

The last two messages give advice meant for Rust callers of serde-saphyr. A budget breach reads like budget breached: Depth { depth: 17 } at line 2, column 70, under the path of the file. A breach of the node limit or a cap that grows with it adds the hint raise `yaml_max_nodes` (now 20000) in config/code_ownership.yml to allow larger files. An error inside an alias also says where the anchor was defined and used.

A missing field is also reported at a different line. serde_yaml gave the line where the mapping starts. serde-saphyr gives the line of the last key it read in that mapping, so a longer team file without github can point at an unrelated key such as ruby:.

Newly rejected. serde_yaml accepts these, and so does Psych except where noted:

  • A quoted string continued on a line indented no deeper than its own key, or for a list item its -, such as:
    metadata:
      description: "first line
      second line"
    serde-saphyr applies the YAML 1.2 rule here and has no option to relax it. Indent the continuation past the key, or use a block scalar. A continuation line that starts with a tab is rejected the same way.
  • A line inside a multi-line [...] or {...} value that has a tab before it is indented past its key, such as dependencies: [ followed by a tab-indented "packs/a",. libyaml treats that tab as whitespace. Spaces before the tab, or a whole-file JSON document indented with tabs, still parse.
  • A comment not separated by a space from the token before it: a closing quote or bracket (name: "Payroll"# note), a block scalar header (description: |# note), a comma in a flow list ([a,# note), or a directive (%YAML 1.1# note).
  • A null key such as ~: x directly in a mapping we read: the top level, metadata, github or ruby. Nested deeper inside a value we ignore, null keys still parse.
  • A core tag that serde-saphyr cannot apply, even under keys we ignore: !!timestamp with any value (Psych's load_file rejects it too), !!int with a value outside 64 bits or in YAML 1.1 form such as 017 or 1:20, !!float 1_000.5, or a value that does not fit, such as !!int abc. In a field we read, a non-string core tag such as name: !!int 123 is rejected too, and so is !!str true for a boolean. serde-saphyr also types some local tags: !int, !float, !bool, !str, !seq, !map, !timestamp and !time on a value that doesn't fit them fail too, as do !include on a list or mapping and !degrees or !radians on any value. So do a key with a non-string core tag such as !!int 1: in a mapping we read, and metadata: ! or metadata: !!map with no value.
  • Nesting deeper than 16 levels, counting the top level, or more than 20,000 nodes (configurable with yaml_max_nodes), anywhere in the file. serde_yaml had no limits for values we ignore.
  • Files with at least 100 aliases and more than 10 aliases per anchor, an alias with the colon attached, such as *a: 1, and a recursive alias such as &a [*a], even under keys we ignore. That last one fails with recursive references require weak recursion types, more advice meant for Rust callers. Psych 4's load_file rejects aliases by default.
  • A BOM at the start of a line once the document has begun, after --- or after the first key: before an indented key or a - entry, on a blank or comment line, or as the file's last bytes. A BOM at the start of a value, too. libyaml skips a BOM at the start of a line, so serde_yaml and Psych accept these. A BOM directly before a key at column 0 already failed on main.
  • U+0085, U+2028 or U+2029 used as a line break, which YAML 1.2 no longer allows.

Newly accepted:

  • A repeated key for a field we read takes the last value, as in Psych. serde_yaml failed with duplicate field `owner` .
  • A UTF-8 BOM before --- or before the first of several keys. serde_yaml failed on these, usually with "deserializing from YAML containing more than one document is not supported".
  • ~ for a list field, read as an empty list, and for a boolean a quoted or block-scalar true, with or without surrounding spaces, or an odd-case tRuE. serde-saphyr 2.1 rejects tRuE again.
  • A package.yml that is only ~ or null, read as no owner, as in Psych, or only ..., which Psych rejects.
  • Syntax libyaml rejects but YAML 1.2 allows: tabs in more places, such as before a key under metadata:; anchor names containing /, . or non-ASCII characters; empty keys and :symbol items in flow lists, in values we ignore; a ... line before the content, which Psych rejects too; unknown % directives; several leading BOMs; and surrogate-pair escapes such as "😀".

Different values:

  • ~, null in any capitalization such as nUlL, a !!null-tagged value, or a local-tagged one such as !foo ~ or an empty !foo, in a string field or list item reads as "" instead of the literal text. In owner, ~, null, Null, NULL and an empty value already meant no owner, and now the rest do too. In a glob list such as owned_globs, generate writes that "" as a / line in CODEOWNERS, where main wrote /~ for a ~ item. An empty item already gave / on main. Psych reads these as nil, except that it keeps the text of a !!null-tagged value that isn't null, such as !!null x. serde-saphyr 2.1 reads odd capitalizations such as nUlL as text again.
  • A |, |+, > or >+ block scalar (not |- or >-) that ends the file without a trailing newline now ends in \n, so owner: | followed by TeamA at end of file reads as "TeamA\n".
  • U+0085, U+2028 and U+2029 are no longer line breaks, so name: TeamA followed by U+0085 reads as "TeamA\u{85}". Ruby's YAML.dump writes a U+2028 or U+2029 in a quoted string as a raw break followed by indentation, so such a dumped value now keeps the indentation spaces. After a comment, one of them no longer ends the comment, so a key after it on the same line is lost: # header, U+0085, owner: TeamA reads as no owner.
  • An anchor name followed directly by : or ?, such as &a:b TeamA, now includes them, so the value reads TeamA rather than :b TeamA.

In an owner or a team name, the block scalar and line break changes mean the name no longer matches its team, the way a typo would. In code_ownership, which compares the names Rust reads against the ones Psych reads, for_file then returns nil for that team's files, or raises with allow_raise: true.

Cost

  • The release binary grows by about 0.8 MB after strip -x, from 3.71 MB to 4.50 MB on aarch64-apple-darwin. Unstripped, each architecture's binary grows by 0.9 to 1.0 MB, and the universal mac binary the release job uploads by 1.9 MB. Most of it is serde-saphyr's deserializer, plus annotate-snippets, which stays linked even with snippets off.
  • Parsing takes about 1.3x as long per file on a large private corpus, about 6 µs more, and up to about 1.9x on small or synthetic files. End-to-end generate on a large monorepo is unchanged within noise, because walking the tree dominates.
  • serde-saphyr needs Rust 1.89, under our 1.97.1 pin and the Rust 1.98 in code_ownership's release images.

Verification

  • cargo audit: no warnings, down from RUSTSEC-2024-0449. cargo tree -i unsafe-libyaml and cargo tree -i serde_yaml match nothing.
  • cargo fmt --all -- --check, RUSTFLAGS=-Dwarnings cargo clippy --all-targets --all-features, and cargo test: 206 passed, 0 failed, 3 ignored (178 before, plus the 28 new tests). The same 206 pass with serde-saphyr's unreleased 2.1.0 (its master at 2454b53) patched in.
  • The release workflow's two-target mac build and its lipo step succeed. Its cross build succeeds for x86_64-unknown-linux-gnu, and for aarch64-unknown-linux-gnu it builds and links codeowners.
  • Release packaging, run locally: the mac job's build, lipo and tar blocks ran as written, minus gh release upload, and the archive lists codeowners at its root plus licenses/ with 9 files, byte-identical to the crates' own. The extracted binary runs. The linux job's upload block, minus the uploads, ran on stand-in binaries under BSD and GNU userlands, and the script also ran with uutils cp, mkdir and dirname 0.10.0 and 0.8.0, which ubuntu-26.04 uses by default, all giving identical trees. DotSlash 0.5.9 fetched a new-layout archive from localhost and ran codeowners from its root. The new Check step exits 1 when a listed crate is missing. zizmor reports no findings, the same as main, and actionlint and shellcheck report nothing new. nm finds granit_parser symbols but no encoding_rs symbols in either mac binary. No Linux binary was built for these checks, since Docker wasn't working locally.
  • Cargo.lock is exactly what cargo build resolves from main's lockfile, with no cargo update: 6 packages removed, 11 added.
  • The release job's grep "\+version" check finds nothing in this diff with BSD grep or GNU grep, so merging does not cut a release.
  • rubyatscale/code_ownership's specs, at 8ac6a59, pass with its native extension built against this branch: 96 examples, 0 failures, 1 pending, the same as against main, in both the dev profile bundle exec rake uses and the release profile, on Ruby 4.0.7. The pending example runs only on Ruby before 3.4. They pass the same way with serde-saphyr's unreleased 2.1.0 (its master at 2454b53) patched in. They have no empty package.yml case, but through code_ownership's Ruby API, validate! accepts an empty, BOM-only, comment-only, ---, ~, null or ... package.yml on both 2.0.0 and 2.1.0.
  • About 56 million fuzzed inputs, parsed as each of our structs and as IgnoredAny with this PR's options, caused no panics.

Notes

  • No version bump. The new yaml_max_nodes key is documented in the README's configuration section.
  • Add allow_unowned_globs to permit files with no owner #135 now conflicts with this PR in README.md, src/config.rs, src/ownership/file_owner_resolver.rs and src/project_builder.rs. Resolving them means keeping both new Config fields, in the struct and in the test Config literals, and taking this PR's ruby_package_owner. Add allow_unowned_globs to permit files with no owner #135 also adds a config test that calls serde_yaml::from_reader, which needs switching to Config::load_from_path, and its allow_unowned_globs field needs deserialize_with = "crate::yaml::strings_or_nulls" to match the other list fields. Without it, an empty allow_unowned_globs: fails under serde-saphyr 2.1. Merging this first means Add allow_unowned_globs to permit files with no owner #135's 0.6.0 release ships the fix.
  • rubyatscale/code_ownership publishes precompiled native gems that statically link codeowners-rs, which it pins at v0.5.0. Once it moves to a release with this change, those gems redistribute granit-parser too and need its notices.

unsafe-libyaml is unmaintained (RUSTSEC-2024-0449) and reaches us only
through serde_yaml, which is itself deprecated. serde-saphyr is a
maintained serde deserializer written in safe Rust, and it is the crate
the advisory names for serde users.

All YAML parsing now goes through src/yaml.rs. Callers read the whole
file and parse the bytes with options that keep serde_yaml's behavior
where serde-saphyr allows it: `<<` stays an ordinary key, `!!binary` is
neither decoded nor validated, `.inf`, `.nan` and `1e999` still parse in
values we ignore, YAML 1.1 booleans such as `yes` are still rejected,
and errors stay on one line. A repeated key takes its last value, as in
Psych. serde-saphyr's default rejects one anywhere in a file, and
serde_yaml rejected one only in a field we read. Error text also
changes, and a few rare inputs are now rejected, accepted or read
differently, as the pull request describes.

Two deserialize_with helpers keep accepting an empty, `~` or `null`
string field or list item, but read all three as "", where serde_yaml
kept `~` and `null` as text. The list helper also reads an empty, `~` or
`null` list as empty, where serde_yaml rejected the last two.
package.yml is read as an Option, so an empty file still means no owner,
and now so does a file that is only `~`, `null` or `...`. serde-saphyr
2.0 does both of these itself, but its unreleased 2.1, which our "2.0.0"
requirement admits, stops (serde-saphyr#233). Errors from team files and
package.yml are wrapped in a type with no source(), since error_stack
would otherwise print an error inside an alias twice.

With duplicate keys set to last-wins, serde-saphyr buffers each mapping
it reads into one of our structs before serde sees it, and buffers a
nested mapping again at each level where it follows an integer key, so
under its default budget a crafted 480 KB package.yml made `generate`
peak at about 2.7 GB, against 35 MB on main. Nesting is now capped at 16
levels and nodes at 20,000, and the caps on scalar bytes, parser events,
recorded anchor events and recorded anchor bytes grow with the node
limit, at 256, 50, 5 and 64 per node. The worst file found that fits,
68 KB, makes `generate` peak at about 290 MB on macOS, against 12 MB on
main. Ordinary files are unaffected: the 1,015 team and package.yml
files in two large private monorepos nest at most 5 deep with fewer than
2,200 nodes, and `generate` gives the same output for both as on main,
with the same peak memory within about 2%.

The node limit is the new `yaml_max_nodes` key in
config/code_ownership.yml, so a repo whose team files or package.yml
files outgrow it can raise it, and the caps that grow with it, without
waiting for a release. Every command that reads team files or
package.yml honors it, and the team cache and the memoized team map are
keyed on it. A file over it, or over a cap that grows with it, fails
like a malformed file, with a hint naming the key. The config file
itself is always read with the default.

Release archives now also hold a licenses/ directory next to the
codeowners binary, which stays at the archive root, where
dotslash-config.json looks for it. dev/license-notices copies into it
the license files of two crates serde-saphyr brings in, whose licenses
require binary redistributions to carry their notices: granit-parser,
its parser, which asks for all inherited and new notices, and
encoding_rs, whose WHATWG-derived data is under BSD-3-Clause. Today's
macOS binaries don't link encoding_rs, since we parse only byte slices,
so its notice is a precaution. The script takes versions from Cargo.lock
and fails if a listed crate leaves the dependency graph or ships no
license files. The jobs that build release archives run only on main
after a version bump, so the Check job also runs the script on pull
requests.

Dependabot's cargo group now excludes serde-saphyr, so its bumps get
their own pull request.

New tests pin each option, budget and hint, both helpers and every
deserialize_with field, empty package.yml files, and the node limit
through the CLI. For the event cap they check only that it grows with
the node limit. The config tests go through Config::load_from_path, as
production does.

Fixes #141
@dduugg
dduugg force-pushed the replace-serde-yaml-with-serde-saphyr branch from 1721171 to 25be0f1 Compare October 21, 2026 07:28
dduugg added a commit to rubyatscale/code_ownership that referenced this pull request Oct 21, 2026
* Fix inverted stage_changes in CodeOwnership.validate!

validate! is documented to stage the regenerated .github/CODEOWNERS by
default and to leave it unstaged with stage_changes: false, which is
what bin/codeownership validate --skip-stage passes. It did the
opposite.

validate! passed !stage_changes to the extension, whose
generate_and_validate took it as skip_stage, as the RBI declared, but
handed it unchanged to codeowners-rs's runner::generate_and_validate,
whose third parameter is git_stage. That has been the case since the
extension was added in #126: the codeowners-rs it pinned already took
git_stage (rubyatscale/codeowners-rs#65). codeowners-rs ignores the
result of its git add, so nothing ever failed.

Pass stage_changes through, and rename the extension's parameter and
the RBI's to git_stage to match codeowners-rs, so the flag means the
same thing at every layer. The extension's behavior is unchanged, so
the tracked bundle needs no rebuild: the fix works with it as
committed, and the specs that call
RustCodeOwners.generate_and_validate(nil, false) to write a CODEOWNERS
file still don't stage it.

Add specs that validate! stages CODEOWNERS by default in a git
repository and leaves it unstaged with stage_changes: false. Both fail
without the fix. They clear git's repo-local environment variables
first, so a run from a hook or a linked worktree can't stage into the
enclosing repository. The CLI specs only check the arguments that
reach validate!, so nothing covered what it does to the index.

Add a regression guard for an empty package.yml.
rubyatscale/codeowners-rs#142 moves YAML parsing to serde-saphyr, whose
unreleased 2.1.0 no longer reads a null document as an empty struct
(the fix for bourumir-wyngs/serde-saphyr#233). That PR reads
package.yml as an Option so an empty file still means no owner. The
spec covers an empty file, a comment-only file and a bare "---".
validate! raises if such a file fails to parse, while for_file only
prints the error and falls back to the file's annotation, so the spec
also checks that for_file prints nothing. It passes today.

* Bump version to 2.2.1

* Clear git's repo-local env vars for the whole suite

The fix makes every real validate! call with default arguments run git
add, not only the new staging specs. Two cli_spec examples do that, and
under a hook's GIT_DIR they staged .github/CODEOWNERS into the enclosing
repository, since the scrub only wrapped the staging context. Move it
to a before(:suite) hook so every example's git runs against its own
temp project.

Split the empty package.yml example in two, so validation and the
for_file parse-error check pass or fail separately, without nesting
one expect inside another.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

RUSTSEC-2024-0449: unsafe-libyaml is unmaintained

1 participant