Skip to content

fix(scoop-export|scoop-list): Make exported JSON key order deterministic - #6697

Open
shauneccles wants to merge 2 commits into
ScoopInstaller:developfrom
shauneccles:fix/deterministic-export-key-order
Open

fix(scoop-export|scoop-list): Make exported JSON key order deterministic#6697
shauneccles wants to merge 2 commits into
ScoopInstaller:developfrom
shauneccles:fix/deterministic-export-key-order

Conversation

@shauneccles

Copy link
Copy Markdown

Description

Two one-line changes, @{}[ordered]@{}, so that scoop export produces byte-identical output when nothing has changed.

[PSCustomObject]$hashtable and ConvertToPrettyJson both follow the hashtable's enumeration order. For a hashtable literal PowerShell preserves the written order, so [PSCustomObject]@{ a = 1; b = 2 } is stable — but a hashtable built by assigning keys one at a time enumerates in bucket order, which depends on String.GetHashCode(), and .NET randomizes string hashing per process. Both export sites use the assignment pattern, so the key order was stable within a process and different in the next one.

  • libexec/scoop-export.ps1:8$export is built by assigning .config / .buckets / .apps, so the three top-level keys were emitted in a random order.
  • libexec/scoop-list.ps1:29$item is built by assigning .Name / .Version / .Source / .Updated / .Info before the [PSCustomObject] cast at line 63, so every app object in the file was independently reordered. This is what dominates the diff.

[ordered]@{} gives insertion order, which keeps the existing readable Name, Version, Source, Updated, Info layout — no sorting pass needed, and no behaviour change for consumers, since scoop import reads by property name ($item.Name, $item.Source, …).

Motivation and Context

Relates to #5862

scoop export's main use is checking the file into version control, and non-deterministic key order defeats that. On a machine committing a daily scoop export, 62 of 80 commits were pure property reordering with no content change at all — typically over 100 changed lines each, from only ~18 installed apps. Real version bumps were buried in the noise.

Worth noting #5862 is currently labelled enhancement, but the output is non-deterministic rather than merely unsorted, which is arguably a bug. Both facets reported there — the top-level order in the original report and the per-app property order in the follow-up comment — have the same root cause and are both fixed here.

How Has This Been Tested?

PowerShell 7.6.1 on Windows 11, against a real install with 18 apps and 1 bucket.

1. Determinism, running bin/scoop.ps1 export in 4 separate processes and hashing the output — once on unmodified develop, once with this change:

distinct outputs / 4 first app's property order
develop 4 Updated, Version, Name, Info, Source
this PR 1 Name, Version, Source, Updated, Info

2. Minimal repro of the underlying mechanism (5 separate processes):

1..5 | ForEach-Object { pwsh -NoProfile -Command '$e=@{}; $e.buckets=1; $e.apps=2; $e.config=3; ($e.Keys) -join ", "' }
# apps, config, buckets / config, apps, buckets / buckets, apps, config / buckets, apps, config / config, buckets, apps

With [ordered]@{} the same loop returns insertion order every time. This is also why exporting twice in one session looks stable — the reordering only shows up across processes.

3. scoop export -c — the riskiest part of the change, since it does $export.config.PSObject.Properties.Remove(...). Verified on both variants that the output is still valid JSON, that config is present, and that last_update / root_path / global_path / cache_path / alias are still stripped. Property access and .Remove() work the same on an OrderedDictionary.

4. scoop list — unchanged output (scoop-list.ps1 backs the display path too): 22 lines, exit 0, identical on both variants.

5. LintingInvoke-ScriptAnalyzer with the repo's PSScriptAnalyzerSettings.psd1: 0 findings on both changed files and 0 across libexec as a whole, matching Scoop-00Linting.Tests.ps1.

There is currently no test coverage for scoop export or scoop-list.ps1 in test/, so nothing existing needed updating. A regression test would have to spawn a child process to defeat per-process hash seeding, which felt disproportionate for a two-line change — happy to add one if you'd prefer.

Checklist:

  • I have read the Contributing Guide.
  • I have ensured that I am targeting the develop branch.
  • I have updated the documentation accordingly.
  • I have updated the tests accordingly.
  • I have added an entry in the CHANGELOG.

shauneccles and others added 2 commits July 27, 2026 09:35
`[PSCustomObject]$hashtable` and `ConvertToPrettyJson` both follow the
hashtable's enumeration order. For a hashtable literal PowerShell keeps the
written order, but a hashtable built by assigning keys one at a time enumerates
in bucket order, which depends on `String.GetHashCode()` -- and .NET randomizes
string hashing per process. Both export sites use the assignment pattern, so the
key order was stable within a process and different in the next one.

  - scoop-export.ps1: `$export` is built by assigning .config/.buckets/.apps,
    so the top-level key order varied.
  - scoop-list.ps1: `$item` is built by assigning .Name/.Version/.Source/
    .Updated/.Info before the `[PSCustomObject]` cast, so every app object in
    the file was independently reordered. This dominated the diff.

Using `[ordered]@{}` is a drop-in for the assignment pattern and yields
insertion order, which preserves the existing readable Name/Version/Source/
Updated/Info layout without needing a sorting pass.

Relates to ScoopInstaller#5862

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6d1d3306-79d4-4737-88b3-25fe5f1cd942

📥 Commits

Reviewing files that changed from the base of the PR and between 78f7221 and a819595.

📒 Files selected for processing (1)
  • CHANGELOG.md

Summary by CodeRabbit

  • Bug Fixes
    • Exported JSON now preserves deterministic key order, making output more readable and predictable.
    • App listings now use a stable field order when rendering each app’s details, without altering which apps appear.
    • Updated the changelog entry under Unreleased → Bug Fixes to reflect these output-order improvements.

Walkthrough

Changes

Output ordering

Layer / File(s) Summary
Preserve output field order
libexec/scoop-export.ps1, libexec/scoop-list.ps1, CHANGELOG.md
The export JSON payload and per-app listing records now use ordered hashtables, preserving insertion order in generated output; the changelog records this fix.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: making scoop export/list JSON key order deterministic.
Description check ✅ Passed The description is directly related to the changes and accurately explains the ordered-hashtable fix and its motivation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant