Visitar URL original
Promote Develop: the Fleet .NET SDK Line on Hosts and in CI by ptr727 · Pull Request #4126 · ptr727/ProjectTemplate · GitHub
Skip to content

Promote Develop: the Fleet .NET SDK Line on Hosts and in CI - #4126

Merged
ptr727 merged 6 commits into
mainfrom
develop
Nov 13, 2026
Merged

ptr727 merged 6 commits into
mainfrom
develop

Conversation

@ptr727

@ptr727 ptr727 commented Nov 13, 2026 •

Copy link
Copy Markdown
Owner

Summary

Promotes the .NET SDK line work and two changes from other hub sessions from develop to main.

Closes #4106
Closes #4107

🤖 Generated with Claude Code

ptr727 and others added 2 commits November 12, 2026 15:13
…#4116)

## Summary

On Windows, menu option 11, "Update agent setup" (`bootstrap.ps1
-AgentSetup`), upgraded the agent CLIs and then stopped. The skills, the
Claude Code safety kit and the Codex and opencode rules steps never ran.

### Root cause

- **The tool that failed was cswap, not herdr.** Nothing was wrong with
herdr: its updater printed "already up to date" on stderr and exited 0.
- **Why cswap failed:** `uv tool upgrade claude-swap` upgraded the
package in the tool's venv and refreshed the `claude-swap.exe` launcher.
It could not replace `cswap.exe`, because a running cswap held that
file, and Windows lets nothing replace a running executable. uv
therefore exited non-zero.
- **How that stopped the setup:** `install-agents.ps1` recorded `Failed:
cswap` and exited 1. The loader's `Invoke-Tool` then called `die`.
`bootstrap.sh`'s `agent_setup` has the same abort through `set -e`.
- **Why the summary missed the upgrade:** the failure path returns
before a version change is recorded. So `Changed:` left out a cswap that
had in fact moved to the new version.

## Changes

- **Both loaders tolerate a tool failure, and only that.**
- `install-agents` now exits 3 when a tool failed or was refused while
the others still ran. It exits 1 when the run itself refused or stopped,
for example a declined prompt, no terminal without `--yes`, or a usage
error.
- On exit 3, `bootstrap.ps1` (`Invoke-AgentSetup`) and `bootstrap.sh`
(`agent_setup`) still run the skills, kit and rules steps. Then they
print `Not current: agent tools (an upgrade failed, see above)` on
stdout and exit 1.
- Any other non-zero code stops the setup as before. A failure in the
skills, kit or rules step also keeps its current behavior.
- **Both install-agents scripts re-read the version after a failed
upgrade.** A partial upgrade now still appears under `Changed:`. One
shared helper, `record_change` / `Add-VersionChange`, replaces the
duplicated inline blocks in the self-updater and manager paths.
- **`install-agents.ps1` skips cswap, with a reason, while a running
process holds either cswap launcher.** This works like the herdr pane
case and runs under `-DryRun` too. Linux needs no equivalent, since a
running binary can be replaced there.
- **Docs and the `update-host` skill now describe the new outcome.**
- The steps in `docs/host-setup.md` "Agent Setup" and both cswap
explanations.
- The kept-tree paragraph in `host-setup/README.md`, which now names the
`install-tools` step.
- `update-host`'s "What the Setup Does" and "Report It", with the
generated copies regenerated by `build_dist.py`.

## Tests

- **`tests/test_bootstrap.py`**, for both loaders:
- An agent setup whose tools step exits 3 still runs every later step,
prints the not-current line, and exits 1.
  - One whose tools step exits any other code stops there.
  - The stubs now take a `FAIL_CODE`.
- **`tests/test_install_agents.py`:**
- A failed upgrade that moved the version is still listed under
`Changed:`. This is tested for both scripts.
- cswap is skipped while a copied-bash process stands in for a running
launcher, and a dry run previews that skip.
- A helper-level test of `Get-CswapLauncherHolder` against a stubbed
`Get-Process`, which runs natively on Windows.
  - The tool-failure assertions move from exit 1 to exit 3.

Run on this host:

- **Native Windows:** `test_bootstrap.py`: 190 ran, OK.
`test_install_agents.py`: OK, its Windows helpers included.
- **WSL Ubuntu:** the Linux classes of both files pass, apart from 5
failures that the unmodified `main` worktree shows identically under
WSL.
  - 4 are mise cases in `test_install_agents.py`.
- 1 is `test_every_managed_tool_is_executable`, because `/mnt/c` has no
exec bits.
- **`mcr.microsoft.com/powershell` container with Python 3.13 via uv:**
`TestInstallAgentsWindows`, 46 ran, OK.
- **Docker lint wrapper:** shellcheck, shfmt and PSScriptAnalyzer pass.
ruff, `build_dist.py --check` and the prose gate are clean too.

## Native verification on Windows 11

These ran from this branch's worktree at `3c1543f`, with `bootstrap.ps1
-AgentSetup -FromTree <worktree> -Yes`, then `-AgentStatus -FromTree
<worktree>`. Host-specific values are replaced with placeholders.

Setup, exit 0:

```text
cswap: at <version>, and a running cswap (process <pid>) holds its launcher, which uv cannot replace while it runs, close it and run this again to update it
  settings -> %USERPROFILE%\.claude\settings.json (autoMode.environment: $defaults added first, so Claude Code's built-in rules apply alongside this host's <n> entries)
  <host> | Windows 11 | hub 3c1543f | payload c0aad5827a70061e | agent-safety v1, fleet-bootstrap v1 | <time>
```

The `$defaults` line comes from the first run on this branch, which made
the change. Later runs read `already current`. After that run,
`autoMode.environment[0]` reads `$defaults`, and every other entry kept
its original order, which was compared against a capture taken before
the run.

Status, exit 0:

```text
This checkout: payload c0aad5827a70061e, hub 3c1543f
This machine:  <host> | Windows 11 | hub 3c1543f | payload c0aad5827a70061e | agent-safety v1, fleet-bootstrap v1 | <time>
autoMode.environment: <n> untagged entries, with $defaults
CURRENT: this machine matches this checkout.
  codex: CURRENT %USERPROFILE%\.codex\AGENTS.md (config directory present, no codex CLI on PATH)
  opencode: CURRENT %USERPROFILE%\.config\opencode\AGENTS.md
  Not current: fleet skills
  Agent tools: listed above, versions not compared with upstream
```

`Not current: fleet skills` is expected here and is not caused by this
change. A `-FromTree` run deliberately leaves the existing skills
registration on the bootstrap's kept tree, which still holds the last
`main` install, instead of moving it to a worktree. So the status reads
that copy as behind this checkout, and "Every other agent setup step
reports current" is not printed. The next `-AgentSetup` from `main`
after promotion refreshes the kept tree.

The tolerated-failure path did not fire natively, because no tool failed
on this host once cswap was skipped. The PowerShell handoff tests cover
it natively instead.

## Local review

Three `local-strict-review` passes ran at the `frontier` tier and were
recorded with `local_review.py`.

- **Round 1** raised 3 findings, all `introduced`, all fixed in
`3c1543f`:
  - The loaders treated a decline or refusal as an upgrade failure.
  - The cswap skip did not run under `-DryRun`.
  - `update-host` did not report a tool failure or skip.
- **Round 2**, over the full diff at `3c1543f`, raised none.
- **Round 3**, over the full diff at `096d4a8` after the Copilot fixes,
raised none.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…json in CI (#4110)

## Summary

Fixes #4106 and #4107. Both tie the .NET SDK line to the fleet's
`global.json`.

- **Host installers (#4106).**
- On Linux, `install-tools.sh` now installs the fleet's SDK line and the
newest line the feed carries by default. Before, it installed the newest
line alone. A host whose feed has moved to 11.0 still builds fleet .NET
repos.
- `--optional` still adds every other line. Each set installs in one apt
call, so a failed install fails the tool.
- A feed with no package for the fleet line still gets the newest line,
with a warning and a report note, and the run carries on so a stand-up
completes. The maintainer chose this over failing the tool. Which feed
gets added is unchanged.
- Each installer names the line itself: `DOTNET_FLEET_LINE` on Linux,
and `$DOTNET_MAJOR` on Windows, which gives
`Microsoft.DotNet.SDK.<major>` and the two majors below as its optional
set. A test fails while either differs from the
`catalog/snippets/configs/global.json` baseline, so a TFM move edits all
three together.
- The installers name the line rather than reading the baseline because
`host-setup/README.md` requires each script to run when fetched on its
own. The maintainer chose this over reading the file.
- The issue suggested naming the line in the host-tools manifest.
`docs/host-setup.md` keeps `dotnet` out of `spec/host-tools.json`
deliberately, so the baseline is the file the test holds them to.
- **Hub CI (#4107).**
- A new `.github/actions/dotnet-sdk` passes `global-json-file:
global.json` to `actions/setup-dotnet` when the caller's `global.json`
carries an `sdk.version`. Otherwise it passes `dotnet-version: 10.x`,
with a warning where jq could not read the file, since the dotnet host
accepts one holding a comment.
- The fallback keeps this change safe ahead of the #4103 rollout. Most
fleet repos don't carry `global.json` yet, and some carry one holding
only the MTP `test` block.
- setup-dotnet v6.1.0 maps `10.0.100` with `rollForward: latestFeature`
to the `10.0` channel. That was read from its source at the pinned SHA.
- Every hub workflow site reaches the action through `$/`:
`validate-task.yml`, `get-version-task.yml`,
`run-codegen-pull-request-task.yml`, and `build-release-task.yml`. The
last runs it ahead of the `dotnet-publish-default` and
`nuget-build-default` defaults, under each default's own condition, so
the defaults no longer set up an SDK. `get-version-task.yml` and
`run-codegen-pull-request-task.yml` gain scoped actionlint ignores like
their siblings.
- The two steps that ran setup-dotnet before checkout now run it after
checkout.
- The `publish-release.yml` snippet keeps `10.x`. Its job checks out
nothing and only runs `dotnet nuget push`, and a comment now says why.
- Tests hold the action's `10.x` fallback to the baseline's major, every
hub workflow and action other than the shared one free of a direct
setup-dotnet, and every shared SDK step after its job's checkout.
- **Docs.** The `dotnet-codestyle` Skill, its `project-config.md`
reference, and both installer READMEs describe the new sources, each
platform's default set, and where the old line comes from during a major
move. The generated skill trees are regenerated.

Part of #4106 and #4107. The closing keywords belong on the develop ->
main promotion.

## Verification

- `tests/test_install_tools.py`: both installer constants equal the
baseline's line. The Linux default and optional sets for each feed
shape, a failed default or optional install failing the tool under `set
-e` off, each missing-line warning under its own source, change
recording without duplicates, and the report row and notes. Each new
test was watched failing against the code it guards.
- `tests/test_release_guards.py`: the shared action's selection run
against a `sdk.version` `global.json`, a test-runner-only one, and none.
No hub workflow or default calls setup-dotnet directly, and the release
task runs the shared action ahead of each default under the same
condition.
- Full suite: 4070 tests, OK (11 skipped), with `develop` merged in. All
seven Docker linters pass. `repo_gate.py` `composite-actions`,
`sha-pin`, and `eol` pass.

## Open Findings From the Local Pass

None. The pass over `6cc50218` raised no finding, and each earlier
pass's findings are fixed in the commits after it.

## Declined Local Findings

- `host-setup/linux/install-tools.sh:863` (design, consolidation onto
`apt_candidate_version`): declined. `dotnet_sdk_fleet` reads the same
`dotnet_sdk_packages` list `dotnet_sdk_latest` and the optional set
read, so every dotnet decision rests on one view of the feed. `apt-cache
policy` is a second query that can disagree with it.

## Pre-existing findings, filed

- `host-setup/linux/install-tools.sh` `microsoft_feed` (`pre-existing`,
correctness): its `die` ends the whole run where packages.microsoft.com
has no config for the host. Filed as #4124.

## Pre-existing findings, not filed

- `tests/workflow_steps.py:96` (`pre-existing`, consolidation):
`job_texts`, moved here unchanged from
`tests/test_docker_task_workflow.py`, does the job `spec/audit.py`
`split_jobs` does through YAML. The overlap predates this change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* Build and validation workflows now select the .NET SDK version from
the calling repository’s configuration, with .NET 10 as the fallback
when no version is specified.
* Linux developer setup installs the fleet SDK line and the newest
available line by default, and can install other available lines when
requested.
* Windows developer setup now tracks the fleet-selected .NET SDK line
and reports newer SDKs when available.

* **Documentation**
* Updated guidance to explain SDK selection, installation options, and
behavior when a required SDK line is unavailable.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings November 13, 2026 00:58
@coderabbitai

coderabbitai Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

…ut (#4125)

## Summary

Addresses #4085, per the maintainer's answer there (option 1).
`scripts/carry.py` could not run `STANDUP.md` step 2 for a repository
whose registry entry had merged to `develop` but not yet reached `main`.
It refused any hub checkout not at `origin/main`, and the target's entry
was missing there.

- **`verify_hub` accepts `develop` for an unpromoted entry.** A hub
checkout at `origin/develop` is accepted when the target has no entry on
`origin/main`. It is refused for a target `main` already registers, so
every other carry stays on `main`. This matches
`repo-config/configure.sh` and `spec/audit.py` since #4082.
- **From `main`, a develop-only entry is named.** The run says to use a
`develop` checkout rather than reporting the repository unregistered.
- **Both branches are fetched by explicit refspec.** The hub menus clone
`main` with `--single-branch`, where a plain `git fetch origin main
develop` never creates `origin/develop`.
- **The menus stay main-only, as their own policy.** Their
`--ref`/`-Ref` refusal now names the unpromoted case and points at
running `scripts/carry.py` from a `develop` checkout. Their comments,
which stated the old carry.py rule, now state the menu's.
- **Docs agree.** `STANDUP.md` step 2, `GOVERNANCE.md` "Hub-Hosted
Tooling", `scripts/README.md` "`carry.py`", and `host-setup/README.md`.

`registry_matches` replaces the duplicate entry matching in
`resolve_repo`.

## Verification

- `tests/test_carry.py`: 85 tests pass. The new `CarryHubTests` build a
real two-branch hub and cover main, develop, the refusals both ways, a
checkout at neither, and a `--single-branch` main clone. Against the old
plain fetch, the single-branch case fails with `fatal: invalid object
name 'origin/develop'`, the defect it pins.
- Pre-commit gates clean: ruff format and check, mypy, prose gate, EOL.
`shellcheck host-setup/menu.sh` and a PowerShell parse of
`host-setup/menu.ps1` are clean.
- The menu change is a message string and comments only. `menu.ps1` was
parsed with `pwsh` on Linux, not run on native Windows.

## Labels

`comments`: the change corrects existing comments in both menus that the
new behavior made false.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The promotion affects shared CI SDK selection and Linux/Windows installer failure semantics, while native Windows behavior and an end-to-end hosted Actions run remain unverified and require maintainer review.

No open issues 🎉

What changed in this PR

This pull request promotes the fleet's .NET SDK alignment and resilient agent setup from develop to main. CI now selects SDK versions from each caller's global.json, host installers keep the fleet SDK line available, and agent setup continues after recoverable tool failures.

Changes:

  • Adds a shared .NET SDK action with global.json selection and a .NET 10 fallback.
  • Updates Linux and Windows host installers for fleet SDK alignment and optional SDK sets.
  • Makes agent upgrades report partial failures and skip locked Windows cswap launchers.
  • Expands tests and regenerates documentation and skill distributions.

A valid commented global.json can be treated as unreadable and replaced with the .NET 10 fallback, even when it selects another SDK line.

File Description
tests/​workflow_steps.py Shares workflow job extraction logic across test modules.
tests/​test_release_guards.py Tests SDK selection, workflow integration, and checkout ordering.
tests/​test_install_tools.py Tests baseline SDK alignment and Linux SDK installation behavior.
tests/​test_install_agents.py Tests partial upgrades, changed-version reporting, and locked cswap handling.
tests/​test_docker_task_workflow.py Reuses the shared workflow job parsing helper.
tests/​test_bootstrap.py Tests agent setup continuation and failure-code handling.
tests/​sdk_baseline.py Reads the fleet SDK line from the baseline configuration.
host-setup/​windows/​README.md Documents Windows SDK selection and locked-launcher behavior.
host-setup/​windows/​install-tools.ps1 Derives Windows .NET package sets from the fleet SDK major.
host-setup/​windows/​install-agents.ps1 Handles partial upgrades and skips locked cswap launchers.
host-setup/​README.md Documents kept-tree behavior after installer failures.
host-setup/​linux/​README.md Documents Linux default and optional .NET SDK sets.
host-setup/​linux/​install-tools.sh Installs and reports the fleet SDK line alongside the newest available line.
host-setup/​linux/​install-agents.sh Reports partial upgrade failures with a distinct recoverable status.
host-setup/​bootstrap.sh Continues agent setup after recoverable tool failures.
host-setup/​bootstrap.ps1 Adds equivalent Windows bootstrap continuation behavior.
docs/​reusable-workflows.md Explains the fixed SDK used by the package publishing snippet.
docs/​host-setup.md Documents resilient agent setup and locked-launcher handling.
catalog/​snippets/​workflows/​publish-release.yml Clarifies the release snippet's fixed SDK choice.
.github/​workflows/​validate-task.yml Routes validation SDK setup through the shared action.
.github/​workflows/​run-codegen-pull-request-task.yml Moves SDK setup after checkout and uses the shared action.
.github/​workflows/​get-version-task.yml Selects the SDK after checkout through the shared action.
.github/​workflows/​build-release-task.yml Sets up the caller-selected SDK before the hub's .NET defaults.
.github/​skills/​update-host/​SKILL.md Documents partial agent setup failures and reporting.
.github/​skills/​dotnet-codestyle/​SKILL.md Documents SDK selection across CI, hosts, and repositories.
.github/​skills/​dotnet-codestyle/​references/​project-config.md Updates SDK and baseline movement guidance.
.github/​actions/​nuget-build-default/​action.yml Removes duplicate SDK setup from the NuGet default action.
.github/​actions/​dotnet-sdk/​action.yml Selects and installs the caller's .NET SDK line.
.github/​actions/​dotnet-publish-default/​action.yml Removes duplicate SDK setup from the publish default action.
.github/​actionlint.yaml Allows local shared-action references in updated workflows.
.claude-plugin/​fleet-skills/​skills/​update-host/​SKILL.md Regenerates the distributed host-update skill.
.claude-plugin/​fleet-skills/​skills/​dotnet-codestyle/​SKILL.md Regenerates the distributed .NET coding skill.
.claude-plugin/​fleet-skills/​skills/​dotnet-codestyle/​references/​project-config.md Regenerates the distributed project configuration reference.
.claude-plugin/​fleet-skills/​.source-digests/​update-host Updates the generated host-update skill digest.
.claude-plugin/​fleet-skills/​.source-digests/​dotnet-codestyle Updates the generated .NET skill digest.
.agents/​skills/​update-host/​SKILL.md Updates the authored host-update skill.
.agents/​skills/​dotnet-codestyle/​SKILL.md Updates the authored .NET coding skill.
.agents/​skills/​dotnet-codestyle/​references/​project-config.md Updates the authored project configuration reference.

Review effort: Lite

@codecov

codecov Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.11765% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 76.69%. Comparing base (326bb1c) to head (fa06887).
⚠️ Report is 493 commits behind head on main.

Files with missing lines Patch % Lines
scripts/carry.py 94.11% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4126      +/-   ##
==========================================
+ Coverage   76.58%   76.69%   +0.10%     
==========================================
  Files          40       40              
  Lines       14136    14146      +10     
==========================================
+ Hits        10826    10849      +23     
+ Misses       3310     3297      -13     
Flag Coverage Δ
python-3.13 76.69% <94.11%> (+0.10%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ptr727

ptr727 commented Nov 13, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@ptr727

ptr727 commented Nov 13, 2026

Copy link
Copy Markdown
Owner Author

/review

@coderabbitai

coderabbitai Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Action not completed

Review rate limited.

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Approval is blocked by an active moderate issue where an inaccessible process path can abort the upgrade check, along with a nit requiring a misleading test comment to be corrected.

No open issues 🎉

2 previously missed issues

In code that hasn't changed since last review

Medium severity Handle access-denied errors when reading process paths

host-setup/​windows/​install-agents.ps1:766

[P1] Guard inaccessible process paths. Get-Process -ErrorAction SilentlyContinue does not suppress an access-denied exception raised while evaluating a protected process's .Path property. Because this script runs unelevated and enumerates every process before upgrading cswap, one protected process can abort the check instead of allowing the upgrade decision to continue. Read the path inside a guarded try/catch and treat unreadable paths as non-matches.

Low severity Correct the comment about checkout and shared-action ordering

tests/​test_release_guards.py:2422

This comment describes the opposite ordering from the assertion below. The shared action must run after checkout so it can read the caller's global.json; please update the comment to avoid making future workflow changes reverse the required order.

Review effort: Lite

## Summary

Answers a Copilot finding on the `develop -> main` promotion #4126. The
comment above the shared SDK step's ordering assertion in
`tests/test_release_guards.py` said "a step ahead of the checkout
installs the fallback", which the reviewer read as the opposite of the
order the assertion enforces. It now says the `dotnet-sdk` step must
follow the checkout, and that run before it, the step finds no
`global.json` and installs the fallback line.

The other finding on that round, on
`host-setup/windows/install-agents.ps1:766`, needs no change. It is
answered on #4126 with a constructed case.

## Verification

- `test_release_guards.DotnetSdkLineCase` passes.
- The local strict review over this diff raised no finding.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@ptr727

ptr727 commented Nov 13, 2026

Copy link
Copy Markdown
Owner Author

Answers to the 2 findings with no thread in the Copilot round on b9f70b37:

  1. host-setup/windows/install-agents.ps1:766, "Guard inaccessible process paths. Get-Process -ErrorAction SilentlyContinue does not suppress an access-denied exception raised while evaluating a protected process's .Path property." Disproven. Path on a process is an ETS ScriptProperty over MainModule.FileName, and PowerShell swallows an exception a property getter throws during member access, under Set-StrictMode -Version Latest as well as without it. A constructed case shows it: an object whose Path ScriptProperty throws Win32Exception(5) (access denied), read as $_.Path inside Where-Object with $ErrorActionPreference = 'Stop', prints strict=off no-throw count=0 and strict=Latest no-throw count=0. So a protected process reads as a null path, the -and short-circuits, and it is a non-match, which is the behavior the finding asks for. This was run under pwsh on Linux, since the behavior is the engine's member-access rule rather than a Windows API.
  2. tests/test_release_guards.py:2422, "This comment describes the opposite ordering from the assertion below." Fixed in 7c3b7ee (State the Shared SDK Step's Checkout Order Plainly in Its Test #4127). The comment now says the shared action must come after the checkout, and that an action step placed ahead of the checkout finds no global.json and installs the fallback line.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

A critical active finding shows that valid commented global.json files can trigger the wrong hard-coded SDK fallback, causing subsequent dotnet commands to use an incorrect SDK line.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

High severity Honor comments in global.json when selecting the SDK

.github/​actions/​dotnet-sdk/​action.yml:20

[P1] Preserve valid global.json comments when selecting the SDK line

This treats any jq parse failure as if the caller had no SDK selection, so a valid commented global.json selecting, for example, 11.0 installs the hard-coded 10.x fallback instead. The pinned actions/setup-dotnet implementation parses global.json as JSON5 and supports comments, so this causes the subsequent dotnet command to use the wrong SDK line. Distinguish a missing sdk.version from a parse failure, or use a parser with the same JSON5 behavior as the setup action.

Review effort: Lite

…4129)

## Summary

Answers a Copilot finding on the `develop -> main` promotion #4126. The
`dotnet-sdk` action decided between the caller's `global.json` and the
`10.x` fallback with `jq -e '.sdk.version'`. A `global.json` holding a
comment is valid to the dotnet host and to setup-dotnet, but jq cannot
parse it, so such a file selecting 11.0 got 10.x.

The selection step now reads `global.json` as both the dotnet host and
setup-dotnet do. Comments are the one extension both accept (the host's
`json_parser.cpp` parses with `kParseCommentsFlag` only), so it drops
comments outside strings, parses the rest as strict JSON, and passes
`global-json-file` where an `sdk.version` is present. A file with no
`sdk.version` still gets the fallback, and a file nothing can read falls
back with a warning.

## Verification

- `test_release_guards.DotnetSdkLineCase` runs the step's script, from a
file under bash, against a selecting file, a test-runner-only file, a
commented file of each kind, a BOM, a URL inside a string, a trailing
comma, and an unreadable file whose warning names the caller's line.
Each new case was watched failing against the code it guards.
- Full suite: 4078 tests, OK (11 skipped). All seven Docker linters and
`repo_gate.py` `composite-actions` pass.

## Open Findings From the Local Pass

None. Every finding listed for earlier pushes is fixed, the last in
`8b058f91` (a case with a UTF-8 BOM), and the pass over that head raised
none.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@ptr727

ptr727 commented Nov 13, 2026

Copy link
Copy Markdown
Owner Author

Answer to the 1 finding with no thread in the Copilot round on 7c3b7ee3:

  1. .github/actions/dotnet-sdk/action.yml:20, "Preserve valid global.json comments when selecting the SDK line ... a valid commented global.json selecting, for example, 11.0 installs the hard-coded 10.x fallback instead." Fixed in a45be93 (Read global.json as the dotnet Host Does When Selecting the SDK Line #4129). The selection step now reads global.json as both the dotnet host and setup-dotnet do. Comments are the one extension both accept: the host's json_parser.cpp parses with kParseCommentsFlag only, and setup-dotnet reads JSON5. The step drops comments outside strings and reads the rest with json.loads. A commented file selects its line, a file with no sdk.version still gets the fallback, and a file neither parser accepts falls back with a warning naming the caller's line. DotnetSdkLineCase covers each shape, and its new cases failed against the jq version.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A moderate active issue remains because malformed-but-valid configuration values can raise an AttributeError instead of using the documented fallback, causing the action to fail before setup-dotnet.

1 open issue

Review effort: Lite

Comment thread .github/actions/dotnet-sdk/action.yml Resolved
## Summary

Answers a Copilot thread on the `develop -> main` promotion #4126. The
`dotnet-sdk` action's selection step raised `AttributeError` on `{"sdk":
null}`. The dotnet host's `sdk_resolver.cpp` treats a missing or null
`sdk` member as no selection and resolves the default SDK, so that file
is valid to the host. The step now gives it the fallback line, like any
file with no `sdk.version`.

A root that is not an object still fails the step. The host's
`json_parser.cpp` rejects it with "Expected a JSON object", so a silent
fallback would only move the failure to the first `dotnet` command.

## Verification

- `test_release_guards.DotnetSdkLineCase` gains a `{"sdk": null}` case
asserting the fallback. It failed against the previous step.
- The local strict review over this diff raised no finding on its final
pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Human assessment is needed because the selection step can terminate on a documented global.json read failure instead of taking the fallback path.

No open issues 🎉

1 resolved since last review

Review effort: Lite

@ptr727

ptr727 commented Nov 13, 2026

Copy link
Copy Markdown
Owner Author

Answer to the verdict headline of the Copilot round on fa06887f, which counts 0 findings:

  1. "the selection step can terminate on a documented global.json read failure instead of taking the fallback path." No change needed. Every global.json the dotnet host accepts either selects its line or takes the fallback:

    • comments, a BOM, a non-UTF-8 byte, a missing or null sdk, and a missing sdk.version;
    • a parse error neither parser accepts, such as a trailing comma or trailing content, falls back with a warning naming the caller's line.

    The step stops only where the host itself would. A root that is not an object is rejected by the host's json_parser.cpp with "Expected a JSON object". A file the checkout cannot open at all would stop every dotnet command the same way. Falling back there would install a line the job's first dotnet command then refuses, so the failure would only move later and lose its cause. global.json is the calling repository's own trusted input, so per GOVERNANCE.md "Trust Boundaries and Hardening Effort" a loud failure on a shape the host rejects is the intended outcome.

@ptr727

ptr727 commented Nov 13, 2026

Copy link
Copy Markdown
Owner Author

Merge Gate item 2, promotion case, at head fa06887f: pr_review.py reads coverage=none here, and every change this promotion carries had full coverage on its own feature pull request into develop, read with pr_review.py status on each:

A Copilot round covers the final head, and its headline is answered. Every thread is resolved, every no-thread item is acknowledged, and all 13 checks pass. coverage=none is the only reason the digest does not read ready.

@ptr727
ptr727 merged commit dbe09c0 into main Nov 13, 2026
14 checks passed
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.

Read the .NET SDK Line From global.json in the Hub's setup-dotnet Steps Install the Fleet's .NET SDK Line by Default on Linux

2 participants