From 05a2648292eb123082345f3bcceda866116800a6 Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Tue, 15 Sep 2026 17:37:33 +0200 Subject: [PATCH 1/2] chore(deps): bump flate2, quick-xml, jsonschema (minor/patch) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit flate2 1.1.9 -> 1.1.10, quick-xml 0.41 -> 0.42, jsonschema 0.49 -> 0.55. Supersedes renovate's #1792, which never got past `renovate/artifacts`: the bot regenerates Cargo.lock itself and couldn't, so every downstream job (rust, MSRV, cargo-deny, wheels, smoke tests, ci-gate) was red for want of a lockfile, not a real break in the bump. quick-xml 0.42 replaces its whole byte-oriented name/text API with `&str` (`QName` is now `QName<'a>(pub &'a str)`, and `BytesText` / `BytesCData` / `BytesRef` all implement `AsRef` instead of `AsRef<[u8]>`), since the crate only ever accepted UTF-8 XML anyway. Update the JUnit parser's `local_name()` and every attribute/tag match in junit.rs from byte-string patterns to string patterns to match. jsonschema 0.49 -> 0.55 is six minor versions of a pre-1.0 crate; read the crate's changelog end to end rather than trusting a clean compile. The one behavior-relevant note (0.52.0: with `default-features = false`, `idn-hostname`/`idn-email` need the new `idna` feature or they're unknown formats) doesn't apply here — the Mergify config schema's own `format` keywords (`date-time`, `duration`, `regex`, `simple-template`, `template`) are all custom or already outside that set, so they were unknown formats (silently accepted) before this bump too. No code changes needed for `mergify-config`'s `validate_against_schema`, which only uses `options().build()` / `iter_errors()` / `instance_path()` — none of which changed shape across the six releases. Cargo.lock's other moves (jsonschema-regex, jsonschema-value, referencing pinned in lockstep with jsonschema; fraction and num-bigint pulled in by jsonschema's own updated deps; miniz_oxide and an inactive optional zlib-rs pulled in by flate2) are all forced by these three bumps, not separate dependency moves. Verified locally against the exact CI steps: `cargo fmt --all --check`, `cargo clippy --workspace --all-targets --all-features --locked -- -D warnings`, `cargo test --workspace --all-features --locked` (668 passed), `cargo build --workspace --all-features --locked`, all green. Co-Authored-By: Claude Sonnet 5 Change-Id: I34925c0376293e993db7b1048faa55005f3d1b9e --- Cargo.lock | 52 ++++++----- Cargo.toml | 4 +- crates/mergify-ci/src/junit_process/junit.rs | 97 +++++++++----------- 3 files changed, 77 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9a347f08..0586678a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -909,12 +909,13 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -951,12 +952,12 @@ dependencies = [ [[package]] name = "fraction" -version = "0.15.4" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872" +checksum = "e246562084dde8ebbcc943b261c406ce4f68e5032ec28029a251a47d6a295500" dependencies = [ - "lazy_static", "num", + "num-bigint", ] [[package]] @@ -1586,9 +1587,9 @@ dependencies = [ [[package]] name = "jsonschema" -version = "0.49.9" +version = "0.55.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ec8a241beed129f06114aa68007e905ca350e7baeb6e17a7631bb7978d91b2" +checksum = "b68339c3d874e48151d74ffe256d93a58cffa240983cb0967d3cbaea083a44fe" dependencies = [ "ahash", "bytecount", @@ -1597,7 +1598,6 @@ dependencies = [ "fancy-regex", "fraction", "getrandom 0.3.4", - "idna", "itoa", "jsonschema-regex", "jsonschema-value", @@ -1615,25 +1615,27 @@ dependencies = [ [[package]] name = "jsonschema-regex" -version = "0.49.9" +version = "0.55.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91994f45017ed5e66aa8e59b8415f4cb033a6380d7200387b7cf117595fbdf85" +checksum = "6307b5b51216ec9b941b52244c74043fa0b1d6b657b56199f57cb1416d3641c5" dependencies = [ "regex-syntax", ] [[package]] name = "jsonschema-value" -version = "0.49.9" +version = "0.55.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec7637f83e510868ae6ed625f7ebfbbde4554ee8ce49854caa5126a8b9b9ecb" +checksum = "0230ac05e09c6111e96c147b75c390579f5cbd45654b980c68ac60fe17b3f129" dependencies = [ "ahash", "bytecount", "fraction", + "getrandom 0.3.4", "num-cmp", "num-traits", "serde_json", + "zmij", ] [[package]] @@ -1945,9 +1947,9 @@ checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74" [[package]] name = "miniz_oxide" -version = "0.8.9" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" dependencies = [ "adler2", "simd-adler32", @@ -2001,9 +2003,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", @@ -2351,9 +2353,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.41.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" dependencies = [ "memchr", ] @@ -2533,9 +2535,9 @@ dependencies = [ [[package]] name = "referencing" -version = "0.49.9" +version = "0.55.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6efa2154ea6f5ce0fdecdd2a8d18f2fa1a39a8fbba91564f555a592e4dce8278" +checksum = "a196a5b4a8a12f46b6353174df865a05d41a6055aff212ec30877492788618b6" dependencies = [ "ahash", "fluent-uri", @@ -3979,11 +3981,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zvariant" diff --git a/Cargo.toml b/Cargo.toml index fa80cf4d..6e876729 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ getrandom = "0.4" glob = "0.3" globset = "0.4" iana-time-zone = "0.1" -jsonschema = { version = "0.49", default-features = false } +jsonschema = { version = "0.55", default-features = false } # OS credential store for the token `mergify auth login` mints. The # default feature set is the one that keeps the Linux build free of C: # `zbus-secret-service-keyring-store` talks to the Secret Service over @@ -49,7 +49,7 @@ jsonschema = { version = "0.49", default-features = false } keyring = "4" opentelemetry-proto = { version = "0.32", default-features = false, features = ["gen-tonic-messages", "trace"] } prost = "0.14" -quick-xml = "0.41" +quick-xml = "0.42" regex = "1" # `form`: the OAuth device grant is `application/x-www-form-urlencoded` # on the wire, as RFC 6749 §4 requires. Nothing else in the CLI sends a diff --git a/crates/mergify-ci/src/junit_process/junit.rs b/crates/mergify-ci/src/junit_process/junit.rs index 3e9882a5..b34423c3 100644 --- a/crates/mergify-ci/src/junit_process/junit.rs +++ b/crates/mergify-ci/src/junit_process/junit.rs @@ -219,26 +219,22 @@ pub fn parse(xml: &[u8]) -> Result { // `GeneralRef` event below; entity resolution // happens in the `GeneralRef` arm. // - // `xml10_content()` (vs the plainer `decode()`) is - // important on Windows: when git checks fixtures - // out with `core.autocrlf` enabled the file's line - // endings are `\r\n`, and the XML 1.0 spec - // requires those to be normalized to `\n` before - // they reach element text. Without this, failure - // stacktraces ship `\r\n` and tests that diff on - // the assembled stacktrace fail only on Windows. - let s = e.xml10_content().map_err(|err| InvalidJunitXml { - details: format!("invalid UTF-8 in element text: {err}"), - })?; + // `xml10_content()` (vs the plain `Deref` content) is important on Windows: when git + // checks fixtures out with `core.autocrlf` enabled + // the file's line endings are `\r\n`, and the XML + // 1.0 spec requires those to be normalized to `\n` + // before they reach element text. Without this, + // failure stacktraces ship `\r\n` and tests that + // diff on the assembled stacktrace fail only on + // Windows. + let s = e.xml10_content(); state.append_failure_text(&s); } Ok(Event::CData(e)) => { - // CDATA bodies are by definition not entity-escaped, - // so plain UTF-8 decoding is correct here. - let text = std::str::from_utf8(e.as_ref()).map_err(|err| InvalidJunitXml { - details: format!("invalid UTF-8 in CDATA: {err}"), - })?; - state.append_failure_text(text); + // quick-xml validates UTF-8 up front, so the event + // already derefs to `str` — no decoding step needed. + state.append_failure_text(e.as_ref()); } Ok(Event::GeneralRef(e)) => { // quick-xml emits a separate `GeneralRef` event for @@ -258,10 +254,8 @@ pub fn parse(xml: &[u8]) -> Result { state.append_failure_text(ch.encode_utf8(&mut tmp)); } } else { - let name = e.decode().map_err(|err| InvalidJunitXml { - details: format!("invalid UTF-8 in entity reference: {err}"), - })?; - let resolved = match name.as_ref() { + let name = e.as_ref(); + let resolved = match name { "lt" => "<", "gt" => ">", "amp" => "&", @@ -294,14 +288,13 @@ pub fn parse(xml: &[u8]) -> Result { /// `testsuite` or `ns:testsuite` (XML namespaces); Python's /// `findall(".//{*}testsuite")` ignores the namespace prefix /// entirely, so we do the same. -fn local_name<'a>(name: QName<'a>) -> &'a [u8] { - // `QName::local_name` would return a wrapped type; we want - // raw bytes here. `into_inner()` (`QName: Deref` - // via `&'a [u8]`) gives the input-borrowed slice back so the - // returned `&[u8]` stays tied to `'a`, not the temporary - // function frame. - let raw: &'a [u8] = name.into_inner(); - raw.rsplit(|b| *b == b':').next().unwrap_or(raw) +fn local_name<'a>(name: QName<'a>) -> &'a str { + // `QName::local_name` would return a wrapped type; we want the + // raw string here. `into_inner()` gives the input-borrowed + // `&'a str` back so the return stays tied to `'a`, not the + // temporary function frame. + let raw: &'a str = name.into_inner(); + raw.rsplit(':').next().unwrap_or(raw) } /// Decode an attribute value to a `String`, resolving XML entity @@ -383,11 +376,11 @@ impl ParserState { let name = local_name(e.name()); if !self.saw_valid_root { match name { - b"testsuites" => { + "testsuites" => { self.saw_valid_root = true; return Ok(()); } - b"testsuite" => { + "testsuite" => { self.saw_valid_root = true; self.saw_any_testsuite = true; let suite_name = read_suite_name(e)?; @@ -404,13 +397,13 @@ impl ParserState { } match name { - b"testsuite" => { + "testsuite" => { self.saw_any_testsuite = true; let suite_name = read_suite_name(e)?; self.suite_names.push(suite_name.clone()); self.suite_stack.push(suite_name); } - b"testcase" => { + "testcase" => { if self.in_progress.is_some() { return Err(InvalidJunitXml { details: "nested not allowed".to_string(), @@ -424,11 +417,11 @@ impl ParserState { self.in_progress = Some(read_testcase(e, &suite_name)?); self.failure_captured = false; } - b"failure" | b"error" => { + "failure" | "error" => { if let Some(tc) = self.in_progress.as_mut() && !self.failure_captured { - tc.status = if name == b"failure" { + tc.status = if name == "failure" { TestStatus::Failed } else { TestStatus::Errored @@ -439,7 +432,7 @@ impl ParserState { self.failure_text_buf.clear(); } } - b"skipped" => { + "skipped" => { if let Some(tc) = self.in_progress.as_mut() { // `` wins over `` only when // the failure wasn't already recorded — Python @@ -465,7 +458,7 @@ impl ParserState { // mirror the work `on_start` does and just skip the // matching `on_end` since there's no body to track. match name { - b"testsuite" => { + "testsuite" => { self.saw_any_testsuite = true; if !self.saw_valid_root { self.saw_valid_root = true; @@ -477,7 +470,7 @@ impl ParserState { self.suite_names.push(read_suite_name(e)?); // Empty testsuite contributes nothing to `cases`. } - b"testcase" => { + "testcase" => { if !self.saw_valid_root { return Err(InvalidJunitXml { details: " outside ".to_string(), @@ -491,18 +484,18 @@ impl ParserState { let tc = read_testcase(e, &suite_name)?; self.output.push(tc); } - b"skipped" => { + "skipped" => { if let Some(tc) = self.in_progress.as_mut() && !self.failure_captured { tc.status = TestStatus::Skipped; } } - b"failure" | b"error" => { + "failure" | "error" => { if let Some(tc) = self.in_progress.as_mut() && !self.failure_captured { - tc.status = if name == b"failure" { + tc.status = if name == "failure" { TestStatus::Failed } else { TestStatus::Errored @@ -524,16 +517,16 @@ impl ParserState { fn on_end(&mut self, e: &quick_xml::events::BytesEnd<'_>) { let name = local_name(e.name()); match name { - b"testsuite" => { + "testsuite" => { self.suite_stack.pop(); } - b"testcase" => { + "testcase" => { if let Some(tc) = self.in_progress.take() { self.output.push(tc); } self.failure_captured = false; } - b"failure" | b"error" if self.in_failure => { + "failure" | "error" if self.in_failure => { // Flush the accumulated body text into the // testcase's failure record. Trimming once at the // close keeps the wire format identical to @@ -569,7 +562,7 @@ fn read_suite_name(e: &quick_xml::events::BytesStart<'_>) -> Result classname = Some(attr_value(&attr)?), - b"name" => name = Some(attr_value(&attr)?), - b"time" => { + "classname" => classname = Some(attr_value(&attr)?), + "name" => name = Some(attr_value(&attr)?), + "time" => { let raw = attr_value(&attr)?; if !raw.is_empty() { time = raw.parse::().ok(); } } - b"file" => file = Some(attr_value(&attr)?), - b"line" => line = Some(attr_value(&attr)?), + "file" => file = Some(attr_value(&attr)?), + "line" => line = Some(attr_value(&attr)?), _ => {} } } @@ -628,8 +621,8 @@ fn read_failure(e: &quick_xml::events::BytesStart<'_>) -> Result failure.kind = Some(attr_value(&attr)?), - b"message" => failure.message = Some(attr_value(&attr)?), + "type" => failure.kind = Some(attr_value(&attr)?), + "message" => failure.message = Some(attr_value(&attr)?), _ => {} } } From bb7ab68a8614da6a522ecc398922d377cb23b4b6 Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Thu, 17 Sep 2026 14:56:18 +0200 Subject: [PATCH 2/2] fix(plugin): fit the Codex subtitle in 30 characters The OpenAI submission portal rejects the plugin with "Subtitle must be 30 characters or fewer". Our shortDescription was 99, so the Info tab could not be filled at all. The subtitle is the one-line label under the plugin name in the directory, not a summary: "Run and debug your merge queue" is exactly 30 and keeps the verb the long description opens with. The longDescription carries everything the old string said about stacks and configuration. The limit is not documented anywhere in developers.openai.com/plugins; it only surfaces on upload. Co-Authored-By: Claude Opus 5 (1M context) Change-Id: Ib967c49ecb872dabe8357eb71898a9cdb3a806be --- .codex-plugin/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 65c9501d..b6bc5dfb 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -31,7 +31,7 @@ "requires_local_executor": true, "interface": { "displayName": "Mergify", - "shortDescription": "Run and debug your merge queue, stacked pull requests, and Mergify configuration from the terminal.", + "shortDescription": "Run and debug your merge queue", "longDescription": "Mergify serializes merges through a queue that runs CI on temporary merge commits, so a pull request that only breaks in combination with another one gets caught before it reaches your default branch. These skills drive the Mergify CLI so an agent can work that queue with you.\n\nAsk why a pull request left the queue and get the engine's own dequeue reason, the checks that failed, and a link straight to the job log. Read the activity log for one pull request or for the whole repository. Validate a .mergify.yml against the schema before you commit it, or simulate what your rules would do on a real pull request. Set up merge protections such as dependencies between pull requests or scheduled freezes for a release window. Turn a branch of commits into a stack of one pull request per commit and keep them rebased.\n\nSix skills ship in the bundle: mergify-merge-queue, mergify-events, mergify-config, mergify-merge-protections, mergify-ci, and mergify-stack.\n\nIt runs on the Mergify CLI, a single static binary with no runtime to install, so it needs a local execution environment: Codex, or ChatGPT desktop with local execution. You need a GitHub repository with Mergify enabled. Install the CLI with 'brew install mergifyio/tap/mergify-cli', sign in once with 'mergify auth login' or set MERGIFY_TOKEN, then ask for what you want in plain language.", "developerName": "Mergify", "category": "Developer Tools",