Skip to content

Commit 5a6b94d

Browse files
JohnCampionJrclaude
andcommitted
Add front-door delegation to the Node tool + document composition
- With autoRunNode, `version` in a repo with no .NET projects now delegates entirely to the Node tool, so net-as-`changeset` transparently handles Node-only repos (the interop + autoRunNode post-pass already covers mixed). - Loop guard via an inherited env marker so a nodeChangesetCommand that resolves back to net cannot recurse forever. - README: "Using alongside @changesets (Node)" with the composition principle, a CLI/repo chart, the front-door config, and per-shell `changeset-net` setup instructions (zsh/bash/pwsh). - Delegation parity test + changeset. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4e09683 commit 5a6b94d

5 files changed

Lines changed: 122 additions & 17 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"SolarWinds.Changesets": Minor
3+
---
4+
5+
Make net-changesets a front door to the Node `@changesets` tool. With `autoRunNode` enabled, `version` in a repository with no .NET projects (for example a Node-only repo where `changeset` resolved to net) now delegates entirely to the Node tool instead of just reporting nothing to do — so a single `changeset version` works whether the repo is .NET, Node, or both. A loop guard (an inherited environment marker) prevents infinite recursion if `nodeChangesetCommand` is misconfigured to point back at net. The README now documents how the two tools compose.

README.md

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,62 @@ intentional gaps:
4747
## Using alongside @changesets (Node)
4848

4949
net-changesets installs the `changeset` command — the same name as the Node `@changesets` CLI. When both are
50-
installed globally, whichever comes first on your `PATH` wins, and a .NET tool can expose only one command, so
51-
there is no built-in second name. To always reach net-changesets regardless of `PATH`, add its `changeset-net`
52-
shell function:
50+
installed globally, whichever comes first on your `PATH` wins. A .NET tool can expose only one command, so
51+
there is no built-in second name.
5352

54-
```bash
55-
# zsh (~/.zshrc) — bash and pwsh are also supported
53+
**The key principle: net-changesets can drive the Node tool, but the Node tool cannot drive net-changesets.**
54+
So in a polyglot repo, let net-changesets own `changeset` and act as the front door — it versions your .NET
55+
projects and then runs the Node tool for the rest. Enable that in `.changeset/config.json`:
56+
57+
```json
58+
{
59+
"dotnet": { "interop": true, "autoRunNode": true }
60+
}
61+
```
62+
63+
With `autoRunNode`, `changeset version` versions the .NET side and then invokes the Node tool
64+
(`nodeChangesetCommand`, default `npx changeset`); in a repo with no .NET projects it delegates to the Node
65+
tool entirely. net never deletes a Node changeset it didn't process.
66+
67+
### How it composes
68+
69+
| Repository | `changeset` runs… | What happens |
70+
| --- | --- | --- |
71+
| **.NET only** | net | Versions your .NET projects. ✅ |
72+
| **.NET only** | Node | Node finds no packages — run `changeset-net` instead. |
73+
| **Node only** | net | With `autoRunNode`, net delegates to the Node tool ✅; otherwise it points you to the Node tool and leaves changesets untouched. |
74+
| **Node only** | Node | Versions your Node packages. ✅ |
75+
| **Both** | net | Versions .NET, then runs the Node tool (`interop` + `autoRunNode`) ✅; without interop it versions .NET and keeps Node-only changesets for the Node tool. |
76+
| **Both** | Node | Versions Node packages only — run `changeset-net version` for the .NET side. |
77+
78+
### Reaching net directly: `changeset-net`
79+
80+
When the Node tool owns `changeset` (it's first on your `PATH`), add net-changesets' `changeset-net` shell
81+
function so you can always invoke net regardless of `PATH`. Add the line for your shell to its startup file,
82+
then restart the shell (or re-source the file):
83+
84+
**zsh** — add to `~/.zshrc`:
85+
86+
```zsh
5687
eval "$(changeset shell-init zsh)"
5788
```
5889

59-
`changeset shell-init <shell>` prints the snippet; run `changeset shell-init` for setup instructions. In a
60-
polyglot repo the smoothest setup is to let net-changesets own `changeset` (it leaves Node changesets untouched
61-
and guides you to the Node tool when there are no .NET projects), using `changeset-net` only when the Node tool
62-
owns `changeset`.
90+
**bash** — add to `~/.bashrc` (on macOS, `$HOME/.bash_profile`):
91+
92+
```bash
93+
eval "$(changeset shell-init bash)"
94+
```
95+
96+
**PowerShell** — add to your `$PROFILE` (open it with `notepad $PROFILE`, creating it if prompted):
97+
98+
```powershell
99+
Invoke-Expression (& changeset shell-init pwsh | Out-String)
100+
```
101+
102+
The line is evaluated by whichever `changeset` runs first on your `PATH`. If that's the Node tool, replace
103+
`changeset` in the line with the full path to net's binary (typically `~/.dotnet/tools/changeset`, or
104+
`%USERPROFILE%\.dotnet\tools\changeset` on Windows). Running `changeset shell-init` with no shell prints these
105+
instructions with the resolved path already filled in.
63106

64107
## Documentation
65108

src/SolarWinds.Changesets/Commands/Version/VersionChangesetCommand.cs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,17 @@ public override async Task<int> ExecuteCommandAsync(CommandContext context, Vers
8989

9090
CsProject[] csProjects = _csProjFileHelper.GetCsProjects(ChangesetConfig);
9191

92-
// No .NET projects: net-changesets has nothing to version. Guide the user rather than silently doing
93-
// nothing - this is the case where the shared `changeset` command resolved to net inside a Node (or
94-
// misconfigured) repository. The changesets are left untouched.
92+
// No .NET projects: net-changesets has nothing to version. This is the case where the shared
93+
// `changeset` command resolved to net inside a Node (or misconfigured) repository. As a front door,
94+
// delegate to the Node tool when autoRunNode is set; otherwise guide the user and leave changesets be.
9595
if (csProjects.Length == 0)
9696
{
97+
if (ChangesetConfig.AutoRunNode && NotAlreadyDelegated && !snapshot && !inPreMode && !exiting)
98+
{
99+
_console.WriteLine("No .NET projects found; delegating to the Node changeset tool.");
100+
return await RunNodeVersionAsync();
101+
}
102+
97103
WarnNoDotnetProjects();
98104

99105
if (exiting)
@@ -177,14 +183,21 @@ await _changesetsRepository.CleanupProcessedChangesetsAsync(
177183

178184
_console.WriteLine(message);
179185

180-
if (ChangesetConfig.Interop && ChangesetConfig.AutoRunNode && !snapshot && !inPreMode && !exiting)
186+
if (ChangesetConfig.Interop && ChangesetConfig.AutoRunNode && NotAlreadyDelegated && !snapshot && !inPreMode && !exiting)
181187
{
182188
return await RunNodeVersionAsync();
183189
}
184190

185191
return 0;
186192
}
187193

194+
// Set on the child environment when net delegates to (or auto-runs) the Node tool, so that a
195+
// nodeChangesetCommand mistakenly pointing back at net is caught instead of recursing forever.
196+
private const string DelegationGuardVariable = "NET_CHANGESETS_DELEGATED";
197+
198+
private static bool NotAlreadyDelegated =>
199+
Environment.GetEnvironmentVariable(DelegationGuardVariable) is not "1";
200+
188201
private async Task ApplySnapshotVersionsAsync(List<ModuleChangelog> changelogs, VersionChangesetCommandSettings settings)
189202
{
190203
string? template = settings.SnapshotPrereleaseTemplate ?? ChangesetConfig.Snapshot.PrereleaseTemplate;
@@ -304,6 +317,9 @@ private void DeleteOwnedChangesets(ChangesetFile[] changesets, CsProject[] csPro
304317

305318
private async Task<int> RunNodeVersionAsync()
306319
{
320+
// Inherited by the Node process; breaks the loop if nodeChangesetCommand resolves back to net.
321+
Environment.SetEnvironmentVariable(DelegationGuardVariable, "1");
322+
307323
_console.WriteLine($"Running '{ChangesetConfig.NodeChangesetCommand} version' for the Node packages...");
308324

309325
ProcessOutput result = await _nodeChangesetService.RunAsync(

src/SolarWinds.Changesets/Shared/DotnetConfig.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,17 @@ internal sealed class DotnetConfig
4545
public string ChangesetExtension { get; init; } = ".net.mkd";
4646

4747
/// <summary>
48-
/// When <see cref="Interop"/> is on, run the Node <c>@changesets</c> tool after net-changesets' own pass,
49-
/// so a single command drives the whole release.
48+
/// Make net-changesets a front door to the Node <c>@changesets</c> tool: it runs the Node tool as part of
49+
/// its own commands, so a single <c>changeset</c> invocation drives the whole release.
5050
/// </summary>
5151
/// <remarks>
52-
/// Default is <c>false</c>. When <c>true</c>, <c>version</c> shells out to <see cref="NodeChangesetCommand"/>
53-
/// after processing the .NET side, which also guarantees net-changesets runs before the Node tool.
52+
/// Default is <c>false</c>. When <c>true</c>: with <see cref="Interop"/> on and .NET projects present,
53+
/// <c>version</c> shells out to <see cref="NodeChangesetCommand"/> after processing the .NET side (so net
54+
/// runs before the Node tool); when there are no .NET projects (for example a Node-only repository where
55+
/// <c>changeset</c> resolved to net), <c>version</c> delegates entirely to the Node tool. Do not set
56+
/// <see cref="NodeChangesetCommand"/> to a bare <c>changeset</c> that resolves back to net - use
57+
/// <c>npx changeset</c> (the default) or another command that targets the Node tool; net guards against an
58+
/// accidental loop but a distinct command is clearer.
5459
/// </remarks>
5560
public bool AutoRunNode { get; init; }
5661

tests/SolarWinds.Changesets.Tests/E2E/InteropTests.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,42 @@ public void Version_WithAutoRunNode_DrivesBothEcosystemsInOneCommand()
162162
ChangesetExists("shared.md").Should().BeFalse();
163163
}
164164

165+
[Test]
166+
[Category("Parity")]
167+
public void Version_NodeOnlyRepo_WithAutoRunNode_DelegatesToNode()
168+
{
169+
if (!NodeChangeset.IsAvailable)
170+
{
171+
Assert.Ignore("Node and/or the @changesets CLI are not available.");
172+
}
173+
174+
// A Node-only repo (no .NET project) where `changeset` resolved to net. As a front door, net delegates.
175+
Directory.CreateDirectory(Path.Combine(_repo, "packages", "web"));
176+
File.WriteAllText(Path.Combine(_repo, "package.json"), """{ "name": "root", "private": true, "workspaces": ["packages/*"] }""");
177+
File.WriteAllText(Path.Combine(_repo, "package-lock.json"), "{}");
178+
File.WriteAllText(Path.Combine(_repo, "packages", "web", "package.json"), """{ "name": "@demo/web", "version": "1.0.0" }""");
179+
180+
WriteConfig($$"""
181+
{
182+
"baseBranch": "main",
183+
"updateInternalDependencies": "patch",
184+
"dotnet": {
185+
"autoRunNode": true,
186+
"nodeChangesetCommand": "node {{NodeChangeset.CliBinPath!.Replace("\\", "\\\\", StringComparison.Ordinal)}}"
187+
}
188+
}
189+
""");
190+
WriteChangeset("web.md", "\"@demo/web\": minor");
191+
192+
CliResult version = ChangesetCli.Run(_repo, "version");
193+
version.ExitCode.Should().Be(0, version.Output);
194+
195+
// net found no .NET projects and delegated to the Node tool, which versioned the package and consumed it.
196+
version.Output.Should().Contain("delegating to the Node changeset tool");
197+
NodePackageVersion("web").Should().Be("1.1.0");
198+
ChangesetExists("web.md").Should().BeFalse();
199+
}
200+
165201
private string? NodePackageVersion(string packageDirectory)
166202
{
167203
string json = File.ReadAllText(Path.Combine(_repo, "packages", packageDirectory, "package.json"));

0 commit comments

Comments
 (0)