`formatjson.ps1` - Sort manifest root keys by `schema.json` and child keys alphabetically
Description
- Reordered root properties in
schema.jsonso it matches the wanted order. - Created new functions in
manifest.ps1Sort-ScoopManifestProperties:- Sorts root properties according to
schema.jsonproperties. - Sorts one level deep child properties alphabetically if the parent property is of type
PSCustomObject. - Called by
formatjson.ps1.
- Sorts root properties according to
Sort-PSCustomObjectKeysRecursively:- Uses recursion to sort all child keys alphabetically, does not touch other values.
- Called by
Sort-ScoopManifestPropertiesfor manifest 1 level child properties that are of type[PSCustomObject].
- Made
formatjson.ps1use new functionSort-ScoopManifestProperties.
Motivation and Context
Closes #6491.
How Has This Been Tested?
# Path to formatjson.ps1 in branch "formatjson-sort-root-properties"
$FunctionPath = [string] 'D:\git\ScoopInstaller--Scoop\bin\formatjson.ps1'
# Path to main bucket in branch "master" pulled 2025-09-19T12:25:00+02:00
$Directory = [string] 'D:\git\ScoopInstaller--Main\bucket'
# Run format json, look at git afterwards
$ErrorActionPreference = 'Stop'
. $FunctionPath -Dir $Directory -App '*'
Checklist:
- [x] I have read the Contributing Guide.
- [x] I have ensured that I am targeting the
developbranch. - [ ] I have updated the documentation accordingly.
- Info about
formatjson.ps1was added with https://github.com/ScoopInstaller/GithubActions/pull/61: https://github.com/ScoopInstaller/GithubActions/wiki/Pull-Request-Checks
- Info about
- [ ] I have updated the tests accordingly.
- [x] I have added an entry in the CHANGELOG.
Summary by CodeRabbit
-
New Features
- Manifest formatter now deterministically orders root and first-level properties; added two manifest utilities to support recursive and root-level ordering.
- Schema expanded with new top-level fields and updated required fields (version, homepage, license).
-
Refactor
- More robust manifest parsing and safer formatting pipeline that writes back only when output is valid.
-
Tests
- Added test to ensure empty-path JSON parsing does not throw.
-
Documentation
- CHANGELOG updated.
@deevus @HUMORCE @rashil2000 @z-Fng: What do you guys think?
Walkthrough
Adds deterministic manifest root-property ordering and recursive PSCustomObject key sorting; updates parse_json to a Param signature and integrates sorting into bin/formatjson.ps1; annotates ConvertToPrettyJson with an output type; and updates schema.json top-level properties and required fields.
Changes
| Cohort / File(s) | Summary |
|---|---|
Formatting pipelinebin/formatjson.ps1 |
Reworks pipeline to parse JSON via parse_json -path, cast to PSCustomObject, apply Sort-ScoopManifestProperties (orders root and first-level properties), pretty-print via ConvertToPrettyJson, normalize tabs to four spaces, and write file only if non-empty; minor style tweaks (-not, explicit [string] cast). |
Manifest utilities & parsinglib/manifest.ps1 |
Converts parse_json to a Param-based signature with explicit path validation and file checks; adds Sort-PSCustomObjectKeysRecursively (recursive alphabetical sorting of PSCustomObject keys) and Sort-ScoopManifestProperties (orders root keys per schema.json, validates/filtering keys, and recursively sorts nested PSCustomObject values). |
JSON helper metadatalib/json.ps1 |
Adds [OutputType([string])] attribute to ConvertToPrettyJson declaration; no logic changes. |
Schema reordering and updatesschema.json |
Reorders and adds many top-level properties (e.g., version, description, homepage, license, notes, depends, suggest, url, hash, installer-related fields), updates references/definitions, and sets top-level required fields to version, homepage, and license. |
Teststest/Scoop-Manifest.Tests.ps1 |
Adds test asserting parse_json '' does not throw. |
ChangelogCHANGELOG.md |
Documents unreleased features: schema reorder, Sort-ScoopManifestProperties, Sort-PSCustomObjectKeysRecursively, and integration with formatjson.ps1. |
Sequence Diagram(s)
sequenceDiagram
autonumber
actor Dev as Developer
participant Fmt as bin/formatjson.ps1
participant Parse as lib/manifest.ps1::parse_json
participant SortRoot as lib/manifest.ps1::Sort-ScoopManifestProperties
participant SortRec as lib/manifest.ps1::Sort-PSCustomObjectKeysRecursively
participant Pretty as lib/json.ps1::ConvertToPrettyJson
participant FS as Filesystem
Dev->>Fmt: run formatjson with manifest path(s)
Fmt->>Parse: parse_json -path <file>
Parse-->>Fmt: PSCustomObject (manifest)
Fmt->>SortRoot: Sort-ScoopManifestProperties -JsonAsObject
SortRoot->>SortRec: recursively sort nested PSCustomObject keys
SortRec-->>SortRoot: nested objects sorted
SortRoot-->>Fmt: PSCustomObject (root ordered per schema)
Fmt->>Pretty: ConvertToPrettyJson -data
Pretty-->>Fmt: string (formatted JSON)
Fmt->>FS: Write-Content (only if non-empty)
FS-->>Dev: File updated if changed
note right of SortRoot: Root order is derived from `schema.json` and validated against available schema keys
Estimated code review effort
π― 4 (Complex) | β±οΈ ~45 minutes
Poem
I hopped through keys and gave them names,
I nudged the roots into tidy lanes.
With whisker-sorts and carrot cheer,
I made each manifest appear clear.
JSON crumbs and tidy dreams β neat seams. π₯
Pre-merge checks and finishing touches
β Failed checks (1 warning)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Out of Scope Changes Check | β οΈ Warning | The pull request includes additional modifications unrelated to the feature request, such as changing the parse_json function signature and behavior, adding an output type attribute in ConvertToPrettyJson, minor stylistic refinements in formatjson.ps1, and a new test for empty-path parsing, which fall outside the specified objectives of sorting manifest properties. |
Consider isolating or justifying the parse_json signature update, OutputType annotation, test addition, and stylistic changes in separate pull requests so that this PR remains focused solely on sorting manifest properties. |
β Passed checks (4 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | β Passed | Check skipped - CodeRabbitβs high-level summary is enabled. |
| Title Check | β Passed | The title succinctly names the primary script being modified (formatjson.ps1) and clearly describes the main feature added (sorting manifest root keys according to schema.json and child keys alphabetically), matching the core changes in the pull request. |
| Linked Issues Check | β Passed | This pull request fully implements the requirements from issue #6491 by establishing the desired property order in schema.json, adding Sort-ScoopManifestProperties and Sort-PSCustomObjectKeysRecursively functions in lib/manifest.ps1, and integrating the sorting logic into bin/formatjson.ps1 to enforce the canonical root-level and child-level ordering during formatting. |
| Docstring Coverage | β Passed | No functions found in the changes. Docstring coverage check skipped. |
β¨ Finishing touches
π§ͺ Generate unit tests
- [ ] Create PR with unit tests
- [ ] Post copyable unit tests in a comment
π Recent review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π₯ Commits
Reviewing files that changed from the base of the PR and between 931183897b0a07969dafbb150a94d5b7167167ae and 3fef8035d92b29ff9426e75b6fd48f475b62acb9.
π Files selected for processing (2)
CHANGELOG.md(1 hunks)lib/manifest.ps1(2 hunks)
π§ Files skipped from review as they are similar to previous changes (2)
- lib/manifest.ps1
- CHANGELOG.md
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.
Comment @coderabbitai help to get the list of available commands and usage tips.
@coderabbitai review
@coderabbitai review
β Actions performed
Review triggered.
Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.
I implemented some of the nitpicks from CodeRabbit.
- CHANGELOG: Fixed.
bin/formatjson.ps1 (2): Use WriteAllText for a single string payload
I tested this, this does not write the final newline. So I kept it to use WriteAllLines.
lib/json.ps1: Cache wanted order; avoid hidden dependency on parse_json
Scoop does this many other places, uses it's own functions. I can change this if maintainers want it, but that would not fit with what existing code already does.
Avoid [byte] cast; use int to prevent overflow and past -1 issues
It's highly unlikely that the Scoop manifest will ever get more than 255 unique root properties. But I changed it to uint16 just in case, which will do 65535.
I thought that when sorting keys like architecture.64bit/32bit , local order are used because the configuration file contains architecture.64bit/32bit. However, when sorting keys like architecture.64bit.url/hash, the configuration file lacks this entry, causing it to fall back to the global order.
As I mentioned earlier, this is the code implementation of my previous idea. Can this help us go further in a simpler way?
Just a thought.
function Get-RootPropertiesObject {
[OutputType([PSCustomObject])]
param()
if (-not $Script:RootPropertiesObject) {
$Script:RootPropertiesObject = [PSCustomObject](
(
parse_json -path (
'{0}\..\schema.json' -f $PSScriptRoot
)
).'properties'
)
}
return $Script:RootPropertiesObject
}
function Sort-Properties {
param(
[Parameter(Mandatory)]
$Properties,
[Parameter(Mandatory = $false)]
[PSCustomObject] $PropertiesObjectDefault = (Get-RootPropertiesObject),
[Parameter(Mandatory = $false)]
[PSCustomObject] $PropertiesObjectOverride = $null
)
if ($Properties -is [PSCustomObject]) {
$OrderDefault = $PropertiesObjectDefault.'PSObject'.'Properties'.'Name'
$OrderOverride = $PropertiesObjectOverride.'PSObject'.'Properties'.'Name'
[PSCustomObject] $PropertiesObject = if ($PropertiesObjectOverride) {
$PropertiesObjectOverride
} else {
$PropertiesObjectDefault
}
$PropertyNames = $Properties.'PSObject'.'Properties'.'Name'
$SortedProperties = [PSCustomObject]::new()
$SortedPropertyNames = @($PropertyNames.Where{ $_ -in $OrderOverride } | Sort-Object { $OrderOverride.IndexOf($_) }) +
@($PropertyNames.Where{ ($_ -notin $OrderOverride) -and ($_ -in $OrderDefault)} | Sort-Object { $OrderDefault.IndexOf($_) }) +
@($PropertyNames.Where{ ($_ -notin $OrderOverride) -and ($_ -notin $OrderDefault)})
$SortedPropertyNames | ForEach-Object {
$SortedProperty = Sort-Properties -Properties $Properties.$_ -PropertiesObjectDefault $PropertiesObjectDefault -PropertiesObjectOverride $PropertiesObject.$_.'properties'
if ($SortedProperty) {
Add-Member -InputObject $SortedProperties -NotePropertyName $_ -NotePropertyValue $SortedProperty | Out-Null
}
}
if ((-not $SortedProperties.PSObject.Properties) -or ($SortedProperties.PSObject.Properties.Count -eq 0)) {
$SortedProperties = $null
}
return $SortedProperties
}
if ($Properties -is [array]) {
$SortedProperties = @()
for($i = 0; $i -lt $Properties.Count; ++$i) {
$SortedProperties += @(Sort-Properties -Properties $Properties[$i] -PropertiesObjectDefault $PropertiesObjectDefault)
}
return ,$SortedProperties
}
return $Properties
}
function Get-SortedManifest {
[OutputType([PSCustomObject])]
param(
[Parameter(Mandatory)]
[ValidateScript({ $null -ne $_ -and $_ -ne [PSCustomObject]::new() })]
[PSCustomObject] $Manifest
)
Sort-Properties -Properties $Manifest -PropertiesObjectDefault (Get-RootPropertiesObject)
}
https://github.com/ScoopInstaller/Scoop/pull/6494#issuecomment-3329194281
I don't see how that is simpler. Also the first function won't, as PSCustomObject.properties won't output anything. Is it LLM generated?
I don't see how that is simpler.
Possibly more comprehensive.
PSCustomObject.properties won't output anything
It's actually 'properties' in schema.json.
Is it LLM generated?
Yes, after editing and modifying it, it runs fine on my local machine.
I don't see how that is simpler.
Possibly more comprehensive.
That's subjective. I think not using aliases, using explicit types, and using named parameters with functions are more readable, maintainable. But I'm down with whatever Scoop maintainers want, as long as we can get the functionality we want here. π
PSCustomObject.properties won't output anything
It's actually 'properties' in schema.json.
Isn't it <output_from_parse_json>.PSObject.Properties.Name you want?
Is it LLM generated?
Yes, after editing and modifying it, it runs fine on my local machine.
Ok.