From d62384cc2f635a49198256ea1fdbc8eaf63eb521 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:14:26 +0000 Subject: [PATCH 1/9] install: stop re-saving an unchanged lockfile for wildcard deps on versionless workspace members Loading bun.lock rewrites a dependency whose resolution is a workspace to a workspace-tagged dependency, but parsing package.json kept a wildcard range on a versionless workspace member as an npm dependency (parse only mirrored the resolver's rule for members with a version field). The two dependency lists never compared equal, so every install counted one update and re-saved a byte-identical lockfile. Apply the resolver's versionless-member rule at parse time: a wildcard range on a versionless workspace member parses as a workspace dependency, the same as ranges that satisfy a versioned member. --- src/install/lockfile/Package.rs | 82 ++++++++++++++----------- test/cli/install/bun-workspaces.test.ts | 63 +++++++++++++++++++ 2 files changed, 108 insertions(+), 37 deletions(-) diff --git a/src/install/lockfile/Package.rs b/src/install/lockfile/Package.rs index da74ede1d3b9..3f6945bb3262 100644 --- a/src/install/lockfile/Package.rs +++ b/src/install/lockfile/Package.rs @@ -1750,43 +1750,51 @@ impl Package { .append::(if relative.is_empty() { b"." } else { relative }); } dependency::version::Tag::Npm => { - if let Some(workspace_version) = workspace_version { - let satisfies = - dependency_version - .npm() - .version - .satisfies(workspace_version, buf, buf); - if pm.options.link_workspace_packages && satisfies { - // `String::sliced` takes `&'a self`; bind the unwrapped - // value so the borrow outlives the parse call. - let wp = workspace_path.unwrap(); - let path = wp.sliced(buf); - if let Some(mut dep) = dependency::parse_with_tag( - external_alias.value, - Some(external_alias.hash), - path.slice, - dependency::version::Tag::Workspace, - &path, - Some(&mut *log), - Some(&mut *pm), - ) { - // Whole-struct move so `Drop` frees the old npm - // chain; keep the existing `literal`. - dep.literal = dependency_version.literal; - dependency_version = dep; - } - } else { - // It doesn't satisfy, but a workspace shares the same name. Override the workspace with the other dependency - for dep in &mut package_dependencies[0..dependencies_count as usize] { - if dep.name_hash == name_hash && dep.behavior.is_workspace() { - *dep = Dependency { - behavior: group.behavior, - name: external_alias.value, - name_hash: external_alias.hash, - version: dependency_version, - }; - return Ok(None); - } + // A versionless workspace member satisfies a wildcard range + // (the resolver links it; see the resolve-from-workspace rule + // in `get_or_put_resolved_package`). Apply the same rule here + // so re-parsing package.json yields the workspace-tagged + // dependency a loaded lockfile holds, instead of a mismatch + // that re-saves an unchanged lockfile on every install. + let satisfies = match workspace_version { + Some(workspace_version) => dependency_version + .npm() + .version + .satisfies(workspace_version, buf, buf), + None => { + workspace_path.is_some() && dependency_version.npm().version.is_star() + } + }; + if pm.options.link_workspace_packages && satisfies { + // `String::sliced` takes `&'a self`; bind the unwrapped + // value so the borrow outlives the parse call. + let wp = workspace_path.unwrap(); + let path = wp.sliced(buf); + if let Some(mut dep) = dependency::parse_with_tag( + external_alias.value, + Some(external_alias.hash), + path.slice, + dependency::version::Tag::Workspace, + &path, + Some(&mut *log), + Some(&mut *pm), + ) { + // Whole-struct move so `Drop` frees the old npm + // chain; keep the existing `literal`. + dep.literal = dependency_version.literal; + dependency_version = dep; + } + } else if workspace_version.is_some() { + // It doesn't satisfy, but a workspace shares the same name. Override the workspace with the other dependency + for dep in &mut package_dependencies[0..dependencies_count as usize] { + if dep.name_hash == name_hash && dep.behavior.is_workspace() { + *dep = Dependency { + behavior: group.behavior, + name: external_alias.value, + name_hash: external_alias.hash, + version: dependency_version, + }; + return Ok(None); } } } diff --git a/test/cli/install/bun-workspaces.test.ts b/test/cli/install/bun-workspaces.test.ts index 8892276eaf73..760aa8338001 100644 --- a/test/cli/install/bun-workspaces.test.ts +++ b/test/cli/install/bun-workspaces.test.ts @@ -193,6 +193,69 @@ test("dependency on workspace without version in package.json", async () => { } }); +test.concurrent( + "repeat install does not re-save the lockfile for a wildcard dependency on a versionless workspace member", + async () => { + using ctx = await setupTest(); + const { packageDir, packageJson, env } = ctx; + await Promise.all([ + write( + packageJson, + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "no-deps": "*", + }, + }), + ), + // no version field on purpose + write(join(packageDir, "packages", "no-deps", "package.json"), JSON.stringify({ name: "no-deps" })), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { + "no-deps": "*", + }, + }), + ), + ]); + + var { stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + var err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + + const lockBefore = await file(join(packageDir, "bun.lock")).text(); + // the wildcard dependencies resolve to the workspace member, not the registry + expect(lockBefore).toContain("no-deps@workspace:packages/no-deps"); + + ({ stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + })); + + err = await stderr.text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + + expect(await file(join(packageDir, "bun.lock")).text()).toBe(lockBefore); + }, +); + test.concurrent("allowing negative workspace patterns", async () => { using ctx = await setupTest(); const { packageDir, env } = ctx; From 1a3217e353fe8ece131249f6c14e7fa2f86ed852 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:16:55 +0000 Subject: [PATCH 2/9] [autofix.ci] apply automated fixes --- src/install/lockfile/Package.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/install/lockfile/Package.rs b/src/install/lockfile/Package.rs index 3f6945bb3262..2e68d49b60bc 100644 --- a/src/install/lockfile/Package.rs +++ b/src/install/lockfile/Package.rs @@ -1757,13 +1757,13 @@ impl Package { // dependency a loaded lockfile holds, instead of a mismatch // that re-saves an unchanged lockfile on every install. let satisfies = match workspace_version { - Some(workspace_version) => dependency_version - .npm() - .version - .satisfies(workspace_version, buf, buf), - None => { - workspace_path.is_some() && dependency_version.npm().version.is_star() + Some(workspace_version) => { + dependency_version + .npm() + .version + .satisfies(workspace_version, buf, buf) } + None => workspace_path.is_some() && dependency_version.npm().version.is_star(), }; if pm.options.link_workspace_packages && satisfies { // `String::sliced` takes `&'a self`; bind the unwrapped From 2df23376842f4bad2b2a6dffe581ae43b96795c7 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:18:30 +0000 Subject: [PATCH 3/9] Trim rationale comment to the rule reference --- src/install/lockfile/Package.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/install/lockfile/Package.rs b/src/install/lockfile/Package.rs index 2e68d49b60bc..1448e2f70004 100644 --- a/src/install/lockfile/Package.rs +++ b/src/install/lockfile/Package.rs @@ -1750,12 +1750,8 @@ impl Package { .append::(if relative.is_empty() { b"." } else { relative }); } dependency::version::Tag::Npm => { - // A versionless workspace member satisfies a wildcard range - // (the resolver links it; see the resolve-from-workspace rule - // in `get_or_put_resolved_package`). Apply the same rule here - // so re-parsing package.json yields the workspace-tagged - // dependency a loaded lockfile holds, instead of a mismatch - // that re-saves an unchanged lockfile on every install. + // A versionless member satisfies only a wildcard range, the + // same rule resolution applies in `get_or_put_resolved_package`. let satisfies = match workspace_version { Some(workspace_version) => { dependency_version From 9aded966a7b78fc9f0b9abd53fd11f12b3a2c166 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:44:44 +0000 Subject: [PATCH 4/9] Keep the parsed dependency tag when loading bun.lock instead of rewriting parse The parse-side rewrite regressed package-lock.json migration: a migrated lockfile keeps npm-tagged dependencies, so rewriting the fresh parse to workspace-tagged made the differ discard every affected workspace member and re-resolve its dependencies, losing npm's pinned versions. Fix the mismatch from the loader side instead. bun.lock loading rewrote any dependency whose resolution is a workspace package to a workspace-tagged dependency, including shapes the package.json parser never produces (a wildcard range on a versionless member). Gate that rewrite on the parser's rule: an npm range is rewritten only for a member version the range satisfies. Deps the parser keeps npm-tagged now load npm-tagged, so the loaded and re-parsed root compare equal and the lockfile stops re-saving. --- src/install/lockfile/Package.rs | 72 +++++++++---------- src/install/lockfile/bun.lock.rs | 35 +++++++++ .../__snapshots__/bun-workspaces.test.ts.snap | 25 +++++-- 3 files changed, 89 insertions(+), 43 deletions(-) diff --git a/src/install/lockfile/Package.rs b/src/install/lockfile/Package.rs index 1448e2f70004..da74ede1d3b9 100644 --- a/src/install/lockfile/Package.rs +++ b/src/install/lockfile/Package.rs @@ -1750,47 +1750,43 @@ impl Package { .append::(if relative.is_empty() { b"." } else { relative }); } dependency::version::Tag::Npm => { - // A versionless member satisfies only a wildcard range, the - // same rule resolution applies in `get_or_put_resolved_package`. - let satisfies = match workspace_version { - Some(workspace_version) => { + if let Some(workspace_version) = workspace_version { + let satisfies = dependency_version .npm() .version - .satisfies(workspace_version, buf, buf) - } - None => workspace_path.is_some() && dependency_version.npm().version.is_star(), - }; - if pm.options.link_workspace_packages && satisfies { - // `String::sliced` takes `&'a self`; bind the unwrapped - // value so the borrow outlives the parse call. - let wp = workspace_path.unwrap(); - let path = wp.sliced(buf); - if let Some(mut dep) = dependency::parse_with_tag( - external_alias.value, - Some(external_alias.hash), - path.slice, - dependency::version::Tag::Workspace, - &path, - Some(&mut *log), - Some(&mut *pm), - ) { - // Whole-struct move so `Drop` frees the old npm - // chain; keep the existing `literal`. - dep.literal = dependency_version.literal; - dependency_version = dep; - } - } else if workspace_version.is_some() { - // It doesn't satisfy, but a workspace shares the same name. Override the workspace with the other dependency - for dep in &mut package_dependencies[0..dependencies_count as usize] { - if dep.name_hash == name_hash && dep.behavior.is_workspace() { - *dep = Dependency { - behavior: group.behavior, - name: external_alias.value, - name_hash: external_alias.hash, - version: dependency_version, - }; - return Ok(None); + .satisfies(workspace_version, buf, buf); + if pm.options.link_workspace_packages && satisfies { + // `String::sliced` takes `&'a self`; bind the unwrapped + // value so the borrow outlives the parse call. + let wp = workspace_path.unwrap(); + let path = wp.sliced(buf); + if let Some(mut dep) = dependency::parse_with_tag( + external_alias.value, + Some(external_alias.hash), + path.slice, + dependency::version::Tag::Workspace, + &path, + Some(&mut *log), + Some(&mut *pm), + ) { + // Whole-struct move so `Drop` frees the old npm + // chain; keep the existing `literal`. + dep.literal = dependency_version.literal; + dependency_version = dep; + } + } else { + // It doesn't satisfy, but a workspace shares the same name. Override the workspace with the other dependency + for dep in &mut package_dependencies[0..dependencies_count as usize] { + if dep.name_hash == name_hash && dep.behavior.is_workspace() { + *dep = Dependency { + behavior: group.behavior, + name: external_alias.value, + name_hash: external_alias.hash, + version: dependency_version, + }; + return Ok(None); + } } } } diff --git a/src/install/lockfile/bun.lock.rs b/src/install/lockfile/bun.lock.rs index be1254346f20..ee48b4a5a951 100644 --- a/src/install/lockfile/bun.lock.rs +++ b/src/install/lockfile/bun.lock.rs @@ -1604,6 +1604,10 @@ pub(crate) fn parse_into_binary_lockfile( ) -> Result<(), ParseError> { lockfile.init_empty(); + let link_workspace_packages = manager + .as_deref() + .is_none_or(|m| m.options.link_workspace_packages); + let Some(lockfile_version_expr) = root.get(b"lockfileVersion") else { log.add_error(Some(source), root.loc, b"Missing lockfile version"); return Err(ParseError::InvalidLockfileVersion); @@ -2774,6 +2778,7 @@ pub(crate) fn parse_into_binary_lockfile( // tree path (see `resolve_peer_dep_version_based`). let package_index = &lockfile.package_index; let overrides = &lockfile.overrides; + let workspace_versions = &lockfile.workspace_versions; // Disjoint-field split of `lockfile.buffers` so each loop body can hold // `&mut dependencies[i]` and `&mut resolutions[i]` together with a shared @@ -2833,6 +2838,9 @@ pub(crate) fn parse_into_binary_lockfile( resolutions, lockfile_version, pkg_resolutions, + string_buf, + workspace_versions, + link_workspace_packages, ); } } @@ -2917,6 +2925,9 @@ pub(crate) fn parse_into_binary_lockfile( resolutions, lockfile_version, pkg_resolutions, + string_buf, + workspace_versions, + link_workspace_packages, ); } } @@ -2989,6 +3000,9 @@ pub(crate) fn parse_into_binary_lockfile( resolutions, lockfile_version, pkg_resolutions, + string_buf, + workspace_versions, + link_workspace_packages, ); } } @@ -3133,12 +3147,33 @@ fn map_dep_to_pkg( resolutions: &mut [PackageID], text_lockfile_version: Version, pkg_resolutions: &[Resolution], + string_buf: &[u8], + workspace_versions: &VersionHashMap, + link_workspace_packages: bool, ) { resolutions[dep_id as usize] = pkg_id; if text_lockfile_version != Version::V0 { let res = &pkg_resolutions[pkg_id as usize]; if res.tag == ResolutionTag::Workspace { + // Rewrite the dependency only into the shape `Package::parse_dependency` + // would produce for it: an npm range becomes a workspace dependency + // only for a member version the range satisfies. A dependency the + // parser keeps npm-tagged (a wildcard on a versionless member) must + // keep its parsed tag here too, or every install diffs the loaded + // lockfile against the fresh parse and re-saves it unchanged. + if dep.version.tag == DependencyVersionTag::Npm { + let npm = dep.version.npm(); + let parse_would_rewrite = link_workspace_packages + && workspace_versions + .get(&StringBuilder::string_hash(npm.name.slice(string_buf))) + .is_some_and(|version| { + npm.version.satisfies(*version, string_buf, string_buf) + }); + if !parse_would_rewrite { + return; + } + } // Whole-struct assign so `DependencyVersion::Drop` frees any prior // npm chain. SAFETY: `res.tag == Workspace` checked above. let literal = dep.version.literal; diff --git a/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap b/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap index ac3d396feaab..28d8fc8eb2cd 100644 --- a/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap @@ -50,7 +50,10 @@ exports[`dependency on workspace without version in package.json: version: * 1`] { "name": "no-deps", "literal": "*", - "workspace": "packages/mono", + "npm": { + "name": "no-deps", + "version": ">=0.0.0" + }, "package_id": 2, "behavior": { "prod": true @@ -173,7 +176,10 @@ exports[`dependency on workspace without version in package.json: version: *.*.* { "name": "no-deps", "literal": "*.*.*", - "workspace": "packages/mono", + "npm": { + "name": "no-deps", + "version": ">=0.0.0" + }, "package_id": 2, "behavior": { "prod": true @@ -296,7 +302,10 @@ exports[`dependency on workspace without version in package.json: version: =* 1` { "name": "no-deps", "literal": "=*", - "workspace": "packages/mono", + "npm": { + "name": "no-deps", + "version": ">=0.0.0" + }, "package_id": 2, "behavior": { "prod": true @@ -542,7 +551,10 @@ exports[`dependency on workspace without version in package.json: version: *.1.* { "name": "no-deps", "literal": "*.1.*", - "workspace": "packages/mono", + "npm": { + "name": "no-deps", + "version": ">=0.0.0" + }, "package_id": 2, "behavior": { "prod": true @@ -665,7 +677,10 @@ exports[`dependency on workspace without version in package.json: version: *-pre { "name": "no-deps", "literal": "*-pre", - "workspace": "packages/mono", + "npm": { + "name": "no-deps", + "version": ">=0.0.0" + }, "package_id": 2, "behavior": { "prod": true From 73ddfe992c507c436c0cc2137c7ecbc8cf9dcb9c Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:46:17 +0000 Subject: [PATCH 5/9] Shorten loader rewrite comment --- src/install/lockfile/bun.lock.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/install/lockfile/bun.lock.rs b/src/install/lockfile/bun.lock.rs index ee48b4a5a951..6e5df579daee 100644 --- a/src/install/lockfile/bun.lock.rs +++ b/src/install/lockfile/bun.lock.rs @@ -3156,12 +3156,10 @@ fn map_dep_to_pkg( if text_lockfile_version != Version::V0 { let res = &pkg_resolutions[pkg_id as usize]; if res.tag == ResolutionTag::Workspace { - // Rewrite the dependency only into the shape `Package::parse_dependency` - // would produce for it: an npm range becomes a workspace dependency - // only for a member version the range satisfies. A dependency the - // parser keeps npm-tagged (a wildcard on a versionless member) must - // keep its parsed tag here too, or every install diffs the loaded - // lockfile against the fresh parse and re-saves it unchanged. + // Only rewrite into the shape `Package::parse_dependency` would + // produce (a member version the range satisfies); otherwise the + // loaded root never matches a fresh parse and every install + // re-saves the unchanged lockfile. if dep.version.tag == DependencyVersionTag::Npm { let npm = dep.version.npm(); let parse_would_rewrite = link_workspace_packages From 17e9ce56f2b4948ca18f6b94e33dab4e61f517a5 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:48:48 +0000 Subject: [PATCH 6/9] test: ignore stdout and drain stderr with exit concurrently --- test/cli/install/bun-workspaces.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/cli/install/bun-workspaces.test.ts b/test/cli/install/bun-workspaces.test.ts index 760aa8338001..f8b5f9a7a8d2 100644 --- a/test/cli/install/bun-workspaces.test.ts +++ b/test/cli/install/bun-workspaces.test.ts @@ -225,15 +225,15 @@ test.concurrent( var { stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: "pipe", + stdout: "ignore", stderr: "pipe", env, }); - var err = await stderr.text(); + var [err, exitCode] = await Promise.all([stderr.text(), exited]); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); - expect(await exited).toBe(0); + expect(exitCode).toBe(0); const lockBefore = await file(join(packageDir, "bun.lock")).text(); // the wildcard dependencies resolve to the workspace member, not the registry @@ -242,15 +242,15 @@ test.concurrent( ({ stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, - stdout: "pipe", + stdout: "ignore", stderr: "pipe", env, })); - err = await stderr.text(); + [err, exitCode] = await Promise.all([stderr.text(), exited]); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("error:"); - expect(await exited).toBe(0); + expect(exitCode).toBe(0); expect(await file(join(packageDir, "bun.lock")).text()).toBe(lockBefore); }, From 632c744919284cbfce6fa0be9ff73b45294f56c5 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 9 Aug 2026 12:22:22 +0000 Subject: [PATCH 7/9] Keep rewriting loaded workspace-resolved deps when linking is disabled With linkWorkspacePackages turned off after a lockfile linked a member, keeping the parsed npm tag made the loaded root match the fresh parse, so the stale workspace resolution was silently kept forever. Rewriting in that state restores the pre-change behavior: the diff fires and the dependency re-resolves under the current setting. --- src/install/lockfile/bun.lock.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/install/lockfile/bun.lock.rs b/src/install/lockfile/bun.lock.rs index 6e5df579daee..7556b1511e36 100644 --- a/src/install/lockfile/bun.lock.rs +++ b/src/install/lockfile/bun.lock.rs @@ -3159,15 +3159,15 @@ fn map_dep_to_pkg( // Only rewrite into the shape `Package::parse_dependency` would // produce (a member version the range satisfies); otherwise the // loaded root never matches a fresh parse and every install - // re-saves the unchanged lockfile. - if dep.version.tag == DependencyVersionTag::Npm { + // re-saves the unchanged lockfile. With linking disabled, always + // rewrite so a stale workspace resolution diffs and re-resolves. + if dep.version.tag == DependencyVersionTag::Npm && link_workspace_packages { let npm = dep.version.npm(); - let parse_would_rewrite = link_workspace_packages - && workspace_versions - .get(&StringBuilder::string_hash(npm.name.slice(string_buf))) - .is_some_and(|version| { - npm.version.satisfies(*version, string_buf, string_buf) - }); + let parse_would_rewrite = workspace_versions + .get(&StringBuilder::string_hash(npm.name.slice(string_buf))) + .is_some_and(|version| { + npm.version.satisfies(*version, string_buf, string_buf) + }); if !parse_would_rewrite { return; } From f20bf8144a65af4df1be7f6f232d3a4564204af4 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 9 Aug 2026 12:24:48 +0000 Subject: [PATCH 8/9] [autofix.ci] apply automated fixes --- src/install/lockfile/bun.lock.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/install/lockfile/bun.lock.rs b/src/install/lockfile/bun.lock.rs index 7556b1511e36..958017671074 100644 --- a/src/install/lockfile/bun.lock.rs +++ b/src/install/lockfile/bun.lock.rs @@ -3165,9 +3165,7 @@ fn map_dep_to_pkg( let npm = dep.version.npm(); let parse_would_rewrite = workspace_versions .get(&StringBuilder::string_hash(npm.name.slice(string_buf))) - .is_some_and(|version| { - npm.version.satisfies(*version, string_buf, string_buf) - }); + .is_some_and(|version| npm.version.satisfies(*version, string_buf, string_buf)); if !parse_would_rewrite { return; } From a368e5659b4d200a8ca930dc89db586712d702b3 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 9 Aug 2026 13:32:41 +0000 Subject: [PATCH 9/9] ci: retrigger