diff --git a/CHANGELOG.md b/CHANGELOG.md index 96d750d..005d540 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [0.1.5] — 2026-05-23 + +### Fixed + +- **`GhCliReleaseSource` returns null when `--prerelease` / a `Channel` is in play.** The list path asked `gh release list --json tagName,name,url,publishedAt,isDraft,isPrerelease` — but `gh release list` exposes a narrower field set than `gh release view`, and `url` is view-only. gh exited non-zero ("Unknown JSON field: \"url\""), `GhProcess` threw, the source's catch-all swallowed it, and consumers saw "Could not determine the latest release." Surfaced by pl-app running `update --prerelease` against a private repo whose only releases were prereleases. Fix: drop `name` and `url` from the list `--json` value — neither was read from the list result anyway (only `tagName`, `publishedAt`, `isDraft`, `isPrerelease` drive filter/sort). The full detail (incl. `url`, `assets`) is still fetched per-tag via `gh release view`. New regression test in `GhCliReleaseSourceTests` asserts the list args stay within `release list`'s supported fields. + +### Why this slipped through v0.1.4 + +- The existing tests use a fake gh runner with canned JSON, so they never exercised the real gh CLI's field-validation. `gh release list` was only reached when `Channel` was set or `IncludePrereleases` was `true` at the source — both uncommon configs before `--prerelease` landed. + +--- + ## [0.1.4] — 2026-05-23 ### Added @@ -85,6 +97,7 @@ Initial commit. Never published to nuget.org — superseded by 0.1.1 before the - Full XML documentation on the public surface, `TreatWarningsAsErrors=true`, `AnalysisLevel=latest`. - SourceLink, deterministic builds, published symbol packages. +[0.1.5]: https://github.com/StuartMeeks/NextIteration.SpectreConsole.SelfUpdate/releases/tag/v0.1.5 [0.1.4]: https://github.com/StuartMeeks/NextIteration.SpectreConsole.SelfUpdate/releases/tag/v0.1.4 [0.1.3]: https://github.com/StuartMeeks/NextIteration.SpectreConsole.SelfUpdate/releases/tag/v0.1.3 [0.1.2]: https://github.com/StuartMeeks/NextIteration.SpectreConsole.SelfUpdate/releases/tag/v0.1.2 diff --git a/src/NextIteration.SpectreConsole.SelfUpdate/NextIteration.SpectreConsole.SelfUpdate.csproj b/src/NextIteration.SpectreConsole.SelfUpdate/NextIteration.SpectreConsole.SelfUpdate.csproj index 3ce9b83..31d900f 100644 --- a/src/NextIteration.SpectreConsole.SelfUpdate/NextIteration.SpectreConsole.SelfUpdate.csproj +++ b/src/NextIteration.SpectreConsole.SelfUpdate/NextIteration.SpectreConsole.SelfUpdate.csproj @@ -11,7 +11,7 @@ NextIteration.SpectreConsole.SelfUpdate - 0.1.4 + 0.1.5 Stuart Meeks Self-update for Spectre.Console CLIs: pluggable update sources (GitHub Releases over HTTP, GitHub Releases via gh CLI for private repos, generic HTTPS manifest, custom), SHA-256 verification, atomic file swap, and a drop-in `update` command. true diff --git a/src/NextIteration.SpectreConsole.SelfUpdate/Sources/GhCliReleaseSource.cs b/src/NextIteration.SpectreConsole.SelfUpdate/Sources/GhCliReleaseSource.cs index 500f768..4b2f0f9 100644 --- a/src/NextIteration.SpectreConsole.SelfUpdate/Sources/GhCliReleaseSource.cs +++ b/src/NextIteration.SpectreConsole.SelfUpdate/Sources/GhCliReleaseSource.cs @@ -98,11 +98,16 @@ internal GhCliReleaseSource( return dto is null ? null : Convert(dto, channel); } + // `gh release list --json` exposes a narrower field set than + // `gh release view --json` — notably `url` is view-only and + // asking for it on list exits the gh process with a non-zero + // status. Only request fields used for filtering / sort here; + // the full detail (incl. url, assets) is fetched per-tag below. var listJson = await _runner( new[] { "release", "list", - "--json", "tagName,name,url,publishedAt,isDraft,isPrerelease", + "--json", "tagName,publishedAt,isDraft,isPrerelease", "--limit", "30", "--repo", _repository, }, diff --git a/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Sources/GhCliReleaseSourceTests.cs b/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Sources/GhCliReleaseSourceTests.cs index 4e4c42a..2947e5a 100644 --- a/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Sources/GhCliReleaseSourceTests.cs +++ b/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Sources/GhCliReleaseSourceTests.cs @@ -83,6 +83,33 @@ public async Task GetLatestAsync_when_prereleases_enabled_lists_then_views_match Assert.Contains("v2.0.0-beta.1", runner.Invocations[1]); } + [Fact] + public async Task GetLatestAsync_list_call_does_not_request_view_only_json_fields() + { + // Regression: `gh release list --json` rejects `url` (and other + // fields that exist only on `release view`). Asking for them + // returns a non-zero exit code that the source's catch-all + // surfaces as "no result". Keep the list `--json` value restricted + // to fields supported by `release list`. + var runner = new RecordingRunner { NextStdout = "[]" }; + var source = new GhCliReleaseSource(Repo, includePrereleases: true, runner.RunAsync); + + await source.GetLatestAsync(channel: null, includePrereleasesOverride: null, CancellationToken.None); + + var listInvocation = runner.Invocations.Single(); + Assert.Equal("list", listInvocation[1]); + + var jsonIdx = -1; + for (var i = 0; i < listInvocation.Count; i++) + { + if (listInvocation[i] == "--json") { jsonIdx = i; break; } + } + Assert.True(jsonIdx >= 0 && jsonIdx + 1 < listInvocation.Count); + var fields = listInvocation[jsonIdx + 1].Split(','); + Assert.DoesNotContain("url", fields); + Assert.DoesNotContain("assets", fields); + } + [Fact] public async Task GetLatestAsync_when_channel_does_not_match_returns_null() {