Skip to content

Commit a9f226d

Browse files
Merge pull request #5 from TimeWarpEngineering/Cramer/2025-12-22/dev
fix: pass NuGet API key from OIDC action to push command
2 parents a17cb5b + 205a3b9 commit a9f226d

5 files changed

Lines changed: 68 additions & 9 deletions

File tree

.github/workflows/ci-cd.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,18 @@ jobs:
4343

4444
- name: NuGet login (OIDC Trusted Publishing)
4545
if: github.event_name == 'release'
46+
id: nuget-login
4647
uses: nuget/login@v1
4748
with:
4849
user: TimeWarp.Enterprises
4950

5051
- name: Run CI Pipeline
51-
run: dotnet run --project tools/dev-cli/timewarp-terminal-dev-cli.csproj -- ci
52+
run: |
53+
if [ "${{ github.event_name }}" == "release" ]; then
54+
dotnet run --project tools/dev-cli/timewarp-terminal-dev-cli.csproj -- ci --api-key "${{ steps.nuget-login.outputs.NUGET_API_KEY }}"
55+
else
56+
dotnet run --project tools/dev-cli/timewarp-terminal-dev-cli.csproj -- ci
57+
fi
5258
5359
- name: Upload Artifacts
5460
if: always()
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Fix NuGet Trusted Publishing API Key Passing
2+
3+
## Description
4+
5+
The CI/CD release workflow fails at the "Push to NuGet" step with a 401 Unauthorized error. Initial diagnosis incorrectly assumed NuGet Trusted Publishing wasn't configured - it was. The real issue was that the `nuget/login@v1` action outputs the API key but doesn't automatically make it available to `dotnet nuget push`. The key must be explicitly passed.
6+
7+
## Checklist
8+
9+
- [x] Investigate CI failure - found 401 error on push
10+
- [x] Verify NuGet Trusted Publishing is configured (it was already correct)
11+
- [x] Discover that `nuget/login@v1` only sets output, not environment variable
12+
- [x] Confirm blog post claiming "no API key needed" is misleading (author's own repo passes it explicitly)
13+
- [x] Add `--api-key` option to `ci-command.cs`
14+
- [x] Update `PushPackagesAsync` to use the API key
15+
- [x] Update `ci-cd.yml` to pass the API key from `nuget-login` step output
16+
- [x] Verify build succeeds
17+
- [ ] Create PR and verify release workflow works
18+
19+
## Notes
20+
21+
### Root Cause
22+
The `nuget/login@v1` action only does `core.setOutput('NUGET_API_KEY', apiKey)` - it does NOT:
23+
- Set an environment variable
24+
- Write to a NuGet config file
25+
- Make the key automatically available to subsequent `dotnet` commands
26+
27+
### The Misleading Blog Post
28+
Gerald Versluis (Microsoft employee) wrote https://blog.verslu.is/nuget/trusted-publishing-easy-setup/ which claims:
29+
> "You don't need to explicitly pass the API key anymore. It's automatically used by the subsequent dotnet commands."
30+
31+
But his actual workflow at https://github.com/jfversluis/maui-version/blob/main/.github/workflows/release.yml **does** pass the key explicitly:
32+
```yaml
33+
--api-key "${{ steps.nuget-login.outputs.NUGET_API_KEY }}"
34+
```
35+
36+
### Fix Applied
37+
1. Added `--api-key` option to `ci` command
38+
2. Updated workflow to pass the key on release events:
39+
```yaml
40+
dotnet run --project tools/dev-cli/timewarp-terminal-dev-cli.csproj -- ci --api-key "${{ steps.nuget-login.outputs.NUGET_API_KEY }}"
41+
```
42+
43+
## Results
44+
45+
Fixed the dev-cli and workflow to properly pass the NuGet API key from the OIDC login step to the push command.

source/timewarp-terminal/timewarp-terminal.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<!-- Independent versioning - not tied to Nuru version -->
5-
<Version>1.0.0-beta.3</Version>
5+
<Version>1.0.0-beta.2</Version>
66
<AssemblyName>TimeWarp.Terminal</AssemblyName>
77
<RootNamespace>TimeWarp.Terminal</RootNamespace>
88
<Description>Terminal abstractions and widgets for console applications - IConsole, ITerminal, panels, tables, rules, and ANSI color support</Description>

tools/dev-cli/commands/ci-command.cs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ internal sealed class CiCommand : ICommand<Unit>
1919
[Option("mode", "m", Description = "CI mode: pr, merge, or release (auto-detected from GITHUB_EVENT_NAME if not specified)")]
2020
public string? Mode { get; set; }
2121

22+
[Option("api-key", "k", Description = "NuGet API key for publishing (required for release mode)")]
23+
public string? ApiKey { get; set; }
24+
2225
internal sealed class Handler : ICommandHandler<CiCommand, Unit>
2326
{
2427
private readonly ITerminal Terminal;
@@ -42,7 +45,12 @@ public async ValueTask<Unit> Handle(CiCommand command, CancellationToken ct)
4245

4346
if (mode == CiMode.Release)
4447
{
45-
await RunReleaseWorkflowAsync(ct);
48+
if (string.IsNullOrEmpty(command.ApiKey))
49+
{
50+
throw new InvalidOperationException("Release mode requires --api-key option for NuGet publishing");
51+
}
52+
53+
await RunReleaseWorkflowAsync(command.ApiKey, ct);
4654
}
4755
else
4856
{
@@ -121,7 +129,7 @@ private async Task RunPrWorkflowAsync(CancellationToken ct)
121129
Terminal.WriteLine("===============================================================================");
122130
}
123131

124-
private async Task RunReleaseWorkflowAsync(CancellationToken ct)
132+
private async Task RunReleaseWorkflowAsync(string apiKey, CancellationToken ct)
125133
{
126134
Terminal.WriteLine("Pipeline: clean -> build -> check-version -> pack -> push");
127135
Terminal.WriteLine("");
@@ -165,7 +173,7 @@ private async Task RunReleaseWorkflowAsync(CancellationToken ct)
165173
Terminal.WriteLine("===============================================================================");
166174
Terminal.WriteLine(" Step 5/5: Push to NuGet");
167175
Terminal.WriteLine("===============================================================================");
168-
await PushPackagesAsync(repoRoot);
176+
await PushPackagesAsync(repoRoot, apiKey);
169177

170178
Terminal.WriteLine("");
171179
Terminal.WriteLine("===============================================================================");
@@ -204,7 +212,7 @@ private async Task PackProjectsAsync(string repoRoot)
204212
Terminal.WriteLine($"\nPackages created in: {artifactsDir}");
205213
}
206214

207-
private async Task PushPackagesAsync(string repoRoot)
215+
private async Task PushPackagesAsync(string repoRoot, string apiKey)
208216
{
209217
string artifactsDir = Path.Combine(repoRoot, "artifacts", "packages");
210218

@@ -235,11 +243,10 @@ private async Task PushPackagesAsync(string repoRoot)
235243

236244
Terminal.WriteLine($"Pushing {package}.{version}.nupkg...");
237245

238-
// Using Trusted Publishing - no API key needed
239-
// The NuGet/login@v1 action in GitHub Actions handles OIDC authentication
240246
int exitCode = await Shell.Builder("dotnet")
241247
.WithArguments(
242248
"nuget", "push", nupkgPath,
249+
"--api-key", apiKey,
243250
"--source", "https://api.nuget.org/v3/index.json",
244251
"--skip-duplicate")
245252
.WithWorkingDirectory(repoRoot)

tools/dev-cli/generated/TimeWarp.Nuru.Analyzers/TimeWarp.Nuru.NuruAttributedRouteGenerator/GeneratedAttributedRoutes.g.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ internal static class GeneratedAttributedRoutes
2929
internal static readonly global::TimeWarp.Nuru.CompiledRoute __Route_CiCommand = new global::TimeWarp.Nuru.CompiledRouteBuilder()
3030
.WithLiteral("ci")
3131
.WithOption("mode", shortForm: "m", parameterName: "mode", expectsValue: true, parameterType: "string", parameterIsOptional: true, description: "CI mode: pr, merge, or release (auto-detected from GITHUB_EVENT_NAME if not specified)", isOptionalFlag: true)
32+
.WithOption("api-key", shortForm: "k", parameterName: "apiKey", expectsValue: true, parameterType: "string", parameterIsOptional: true, description: "NuGet API key for publishing (required for release mode)", isOptionalFlag: true)
3233
.WithMessageType(global::TimeWarp.Nuru.MessageType.Command)
3334
.Build();
34-
internal const string __Pattern_CiCommand = "ci --mode,-m {mode?}";
35+
internal const string __Pattern_CiCommand = "ci --mode,-m {mode?} --api-key,-k {apiKey?}";
3536

3637
internal static readonly global::TimeWarp.Nuru.CompiledRoute __Route_CleanCommand = new global::TimeWarp.Nuru.CompiledRouteBuilder()
3738
.WithLiteral("clean")

0 commit comments

Comments
 (0)