Skip to content

Prove NuGet Trusted Publishing end-to-end and retire the long-lived NUGET_API_KEY #198

Description

@phmatray

Problem / motivation

FormCraft's release path was rewired to publish via NuGet Trusted Publishing (OIDC) in #173
(merged 2026-07-27). The workflow side is done and on dev today: .github/workflows/continuous.yml
declares id-token: write, runs NuGet/login pinned by digest (8d19675…, v1.2.0) gated on
startsWith(github.ref, 'refs/tags/v'), and feeds the short-lived key to Nuke as NUGET_API_KEY.
The NUGET_USER secret is set, and the nuget.org policy was registered the same day.

But that path has never once executed, and the long-lived key it was meant to retire is still there.

Measured on 2026-08-11:

Fact Value How it was measured
Last version tag v3.1.0, 2026-06-10 git log -1 --format=%ci v3.1.0
Trusted Publishing merged 2026-07-27T18:48Z (#173) gh pr view 173 --json mergedAt
Tag-triggered workflow runs since none gh run list filtered on event=push, headBranch starting v
Latest version on nuget.org 3.1.0 (both packages) api.nuget.org/v3-flatcontainer/…/index.json
NUGET_API_KEY repo secret still present, created 2026-02-23 gh secret list
NUGET_USER repo secret present, created 2026-07-27 gh secret list
Org-scope secrets n/aphmatray is a user account (API returns 422) gh api …/actions/organization-secrets

So the last tag predates the rewrite by six weeks. Every published version of FormCraft and
FormCraft.ForMudBlazor went out under the old long-lived key, and the OIDC path is wired but
unproven
. Two consequences, both bad:

  1. The next release is the test. If the policy fields, the workflow filename, or the Nuke
    Publish chain are wrong, we find out at the exact moment we want to ship a ★61 flagship
    package — and a half-published release on nuget.org cannot be undone (nuget.org delists, it
    never deletes).
  2. The credential the migration was supposed to eliminate is still live. Nothing references
    secrets.NUGET_API_KEY in the workflow any more, so it is inert, not armed — but an inert
    long-lived push credential is still a long-lived push credential, and deleting the GitHub secret
    does not revoke it on nuget.org.

There is also a latent trap in build/Build.cs:20-32: the [GitHubActions] attribute still declares
ImportSecrets = ["NUGET_API_KEY"] and omits IdToken from WritePermissions. It is harmless today
only because AutoGenerate = false. Anyone who flips that flag regenerates continuous.yml and
silently deletes the entire OIDC block.

Proposed solution

Finish the migration the way repo-audit/TRUSTED_PUBLISHING.md prescribes — policy first,
key deletion last
— and prove each step by observation rather than by reading the YAML:

  1. Re-verify the nuget.org policy and secret scope against the live repo (read-only gate).
  2. Close the AutoGenerate trap in build/Build.cs and add a regression test that fails if the
    OIDC wiring is ever removed from continuous.yml.
  3. Rehearse with a prerelease tag (v3.1.1-rc.1), which exercises the exact publish path, and
    confirm the package appears on the nuget.org index.
  4. Ship a real release through the same path.
  5. Only then delete the NUGET_API_KEY repo secret and revoke the key on nuget.org.

Alternatives considered

  • Wait for the next real release (no rehearsal). Burns no version number, but moves the risk onto
    a real user-facing release. Rejected — see Brainstorm.
  • Migrate to release-please like Koine. Koine drives its release from release-please.yml;
    FormCraft uses MinVer + hand-cut tags. Changing the version driver is a much larger, separate
    decision and is explicitly out of scope here — the Koine reference this issue borrows is the
    OIDC publish pattern, which FormCraft already matches.
  • A workflow_dispatch dry-run that logs in but does not push. Proves only half the path and
    risks a false negative. See Brainstorm.

Area

CI / release infrastructure — .github/workflows/continuous.yml, build/Build.cs (Nuke Publish /
PublishIfNeeded targets), and the nuget.org Trusted Publishing policy. No library code changes.

Related: #173

🧠 Brainstorm

Problem / context

The migration to Trusted Publishing is 90% done and 0% proven. repo-audit/TRUSTED_PUBLISHING.md
(the canonical, replayable runbook for this portfolio-wide effort) lays out six steps; FormCraft has
completed steps 1–4 (policy created, NUGET_USER set, workflow migrated, PR merged green) and has
completed neither step 5 ("a real release, then verify the publication — a merge proves
nothing"
) nor step 6 (delete the key).

That runbook is emphatic about the ordering, and for a reason it paid for: "Never before. Deleting
first leaves no way back if the policy is wrong."
It also carries the portfolio's most expensive
lesson — an earlier note concluded eleven repos "expose nothing" based on gh secret list, which
only returns repo secrets; an org-scoped NUGET_API_KEY existed and seven repos had an armed
push. The comment was then copied into workflow files, where it served as proof to everyone. That
does not apply to FormCraft (phmatray is a user account — the org-secrets endpoint returns 422, so
repo scope is the whole story here) but the discipline does: verify by API, not by comment.

What makes FormCraft's shape different from Koine's, and worth thinking about rather than
copy-pasting:

  • Koine packs and pushes in adjacent steps, seconds apart. FormCraft mints the OIDC key before
    ./build.cmd Continuous
    , which runs Test → Pack → Publish. The exchanged key lives ~1 hour, so
    in principle a slow build could outlive its own credential.
    Measured: the five most recent continuous runs took 60–77 s. That is ~50× headroom. Not a
    constraint — but it is now measured rather than assumed, and it is the reason not to restructure
    the workflow to move login adjacent to the push.
  • continuous.yml fires on push to main/dev, on pull_request, and on v* tags. The
    runbook's rule is that a tag-only workflow must fail loudly (no if: env.NUGET_USER != ''
    guard), because a guard on a tag produces a green CI with an unpublished version that nobody
    notices. FormCraft is the mixed case and gets this right already: the guard is on the trigger
    (startsWith(github.ref, 'refs/tags/v')), not on the secret. A fork's PR cannot obtain this
    repo's OIDC token, so gating on the tag is also what keeps fork PRs from failing. Do not
    "improve" this into a NUGET_USER guard.

Approaches

A. Prerelease rehearsal tag, then the real release, then revoke. (recommended)
Tag v3.1.1-rc.1, let continuous.yml run the whole path, and probe the nuget.org index for the
result. IsOnVersionTag() (build/Build.cs:379) matches on ^v\d+\.\d+\.\d+ with no $
anchor
, so a prerelease tag does trigger publish — the rehearsal exercises the identical code path,
not a simulation of it.
Cost, stated plainly: it publishes a real prerelease version to nuget.org, permanently. nuget.org
delists but never deletes. The blast radius is small — dotnet add package will not resolve a
prerelease without --prerelease — and --skip-duplicate (already set via EnableSkipDuplicate()
in build/Build.cs:226) makes a re-run idempotent.

B. Skip the rehearsal; let the next real release be the test.
Burns no version number. But it moves an untested, irreversible path onto a user-facing release of a
★61 package, and a partial failure (first package pushed, second not) is discovered only after the
first one is public. The whole point of step 5 in the runbook is that a merge proves nothing;
choosing B means the proof and the risk arrive together.

C. A workflow_dispatch dry-run that performs the OIDC exchange without pushing.
Cheap and publishes nothing. Two problems: it proves the login half but not the push half (which is
where the Nuke Requires/OnlyWhenStatic chain actually lives, and that chain has never run on a
tag), and the OIDC token's ref claim from a dev dispatch differs from a tag ref — so a rejection
would be ambiguous between "policy is wrong" and "this is not how the policy is scoped". A false
negative here costs more than the rc version A burns.

Recommendation

A. The cost is one prerelease version number; the return is that the first real release through
OIDC is not also the first release of OIDC. B defers a known risk onto the worst possible moment,
and C's cheapness comes from not testing the part most likely to break.

Fold in one piece of C's instinct as cheap insurance that costs nothing: a regression test on the
workflow file, so the wiring cannot be silently undone later. The repo already has this reflex —
FormCraft.UnitTests/Documentation/DocumentationSamplesTests exists precisely so documentation
"cannot silently drift away from the real public API again", and RenderPipelineParityTests keeps
the two render paths honest. A publish path that is exercised roughly four times a year is more
prone to silent drift than either, not less.

Assumptions

  • The nuget.org policy registered on 2026-07-27 names Package Owner phmatray, repository
    phmatray/FormCraft, workflow file continuous.yml, environment empty. Task 1 re-verifies this
    in the UI
    rather than trusting the runbook's record — nuget.org does not validate the workflow
    filename, so a typo there is accepted silently and only fails at the first tag.
  • v3.1.1-rc.1 is an acceptable version to burn. If a different rehearsal version is preferred, any
    v<major>.<minor>.<patch>-<label> works identically.
  • Deleting the secret and revoking the key (Task 5) are destructive and owner-only — they are
    written as an explicit human gate, not automated.
📋 Spec

Goal

The next FormCraft release publishes both packages to nuget.org through OIDC only, proven by
observing the run and the nuget.org index — after which the long-lived NUGET_API_KEY is deleted
from GitHub and revoked on nuget.org.

Scope

  1. Re-verify the nuget.org policy, secret scope, and workflow filename against the live repo.
  2. Close the AutoGenerate regression trap in build/Build.cs and guard the OIDC wiring with a test.
  3. Rehearse the publish path with a prerelease tag and verify the result on the nuget.org index.
  4. Ship a real release through the same path.
  5. Retire the long-lived key: delete the GitHub secret, then revoke it on nuget.org.

Non-goals

  • Migrating to release-please. FormCraft versions with MinVer from git tags
    (MinVerTagPrefix=v); there is no <Version> element anywhere. Changing the version driver is a
    separate decision.
  • Changing package ids, package ownership, or the phmatray-vs-atypical account split.
  • Touching .github/workflows/release.yml (git-cliff + GitHub Release). It fires on the same v*
    tag but does not push to nuget.org, and build/Build.cs:300-316 deliberately does not chain
    CreateGitHubRelease so the two cannot race for the same release.
  • Restructuring continuous.yml to move login adjacent to the push — the measured 60–77 s build
    makes that unnecessary.

The path being proven

flowchart TD
    T["git push origin v3.1.1-rc.1"] --> W["continuous.yml<br/>on: push tags v*"]
    W --> G{"startsWith(github.ref,<br/>'refs/tags/v')"}
    G -->|false: push to dev / PR| S["login skipped<br/>NUGET_API_KEY unset"]
    G -->|true| L["NuGet/login@8d19675 (v1.2.0)<br/>user: secrets.NUGET_USER"]
    L -->|OIDC token, id-token: write| N["nuget.org<br/>Trusted Publishing policy"]
    N -->|short-lived key ~1h| E["env NUGET_API_KEY"]
    E --> B["./build.cmd Continuous"]
    B --> C["Continuous → Test, Pack<br/>Triggers PublishIfNeeded"]
    C --> P{"OnlyWhenStatic<br/>IsOnVersionTag() && IsServerBuild"}
    P -->|false| X["no publish"]
    P -->|true| Q["Publish (DependsOn)<br/>Requires NuGetApiKey"]
    Q --> R["DotNetNuGetPush --skip-duplicate<br/>FormCraft + FormCraft.ForMudBlazor"]
    R --> V["verify: api.nuget.org<br/>flatcontainer index = 200"]
Loading

Key files

File Role Change
.github/workflows/continuous.yml the OIDC login + build invocation none — already correct; becomes the subject of a guard test
build/Build.cs:20-32 [GitHubActions] attribute remove stale ImportSecrets, add IdToken, document AutoGenerate = false
build/Build.cs:213-229 Publish target none — read to confirm behaviour
FormCraft.UnitTests/Ci/TrustedPublishingWorkflowTests.cs new guard test create

Behaviour and validation rules

  • IsOnVersionTag() is ^v\d+\.\d+\.\d+ with no $ anchor — prerelease tags publish. This is
    what makes the rehearsal a true rehearsal; it is also a live trap, since a tag like
    v1.0.0-donotship would publish. Documented, not changed.
  • Publish carries .Requires(() => NuGetApiKey). On a non-tag run the key is never minted, and
    PublishIfNeeded is skipped by OnlyWhenStatic — which is why dev pushes and PRs are green
    today. The rehearsal is the first time this evaluates with the requirement satisfied.
  • EnableSkipDuplicate() makes a re-run of the same version a no-op, so re-running a failed
    release job is safe.
  • A job-level permissions: block replaces the workflow-level one. continuous.yml declares
    permissions only at workflow level (contents: write, packages: write, id-token: write).
    Adding a job-level block without re-declaring all three would break the OIDC exchange or the
    artifact upload.

Edge cases

  • nuget.org does not validate the workflow filename in a policy — it is a free-text string. A
    wrong value is accepted at creation and fails only at the first tag. Task 1 checks it character
    for character against the file on dev.
  • Deleting the GitHub secret revokes nothing. The key remains valid on nuget.org until revoked
    there. Both halves are required.
  • Two packages, one push. Publish globs *.nupkg/*.snupkg from artifacts/. Verification
    must probe both ids — a policy misconfigured for only one id would publish one and fail the
    other.
  • Probe the nuget.org index, never the local ~/.nuget/packages/ cache: the cache can contain a
    package that exists nowhere (this is exactly how Phosphor was mistaken for green on 2026-07-26).

Assumptions

  • Package owner on nuget.org for both ids is phmatray (confirmed via the flatcontainer registration
    metadata in repo-audit/nuget_publish_audit.json).
  • NUGET_USER holds the nuget.org profile name matching the policy's Package Owner, not the
    repository owner. It is a secret by convention only, not a credential.

🛠️ Implementation plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Prove FormCraft's NuGet Trusted Publishing path end-to-end with a real published package, then delete and revoke the long-lived NUGET_API_KEY.

Architecture: Verify preconditions (read-only) → guard the wiring in code → rehearse with a prerelease tag → ship a real release → retire the key. The order is the runbook's and is not negotiable: the key is deleted last, only after a verified publication.

Tech Stack: GitHub Actions + NuGet/login v1.2.0 (OIDC), Nuke 10.1.0 (build/Build.cs), MinVer versioning from git tags, xUnit + Shouldly on Microsoft.Testing.Platform.

Global Constraints

  • Default branch is dev. PRs target dev (--base dev); main is the release branch.
  • Merge style: squash. PR title ci(release): <subject> (#<issue>) — this issue is type:chore but the work is CI/release, matching ci: publish to NuGet via Trusted Publishing, not a long-lived key #173's ci: prefix.
  • Commit identity: git -c user.email=phmatray@gmail.com -c user.name="Philippe Matray".
  • dotnet test --filter is inert — these test projects run on Microsoft.Testing.Platform, which prints MTP0001 and runs the whole suite anyway. Always run the full suite; never report a filtered result.
  • TreatWarningsAsErrors=true in Directory.Build.props is deliberate. Do not relax it to make a build pass.
  • SDK pinned to 10.0.302 (rollForward: latestFeature); multi-target net8.0;net10.0 — a build error can be TFM-specific.
  • Never delete or revoke the key before a verified publication (Task 5 gates on Task 4).
  • Tasks 1, 3, 4 and 5 contain owner-only steps (nuget.org UI, tag pushes, secret deletion). Do not automate them; stop and hand off.

Task 1: Verify the trusted-publishing preconditions against the live repo

Read-only gate. Everything downstream assumes these four facts; the runbook's own history shows what happens when they are assumed rather than measured.

Files: none modified. Output is a comment on this issue recording the measured state.

Interfaces: Produces the go/no-go for Task 3. If any check fails, fix it before tagging anything.

  • Step 1: Confirm the workflow file named by the policy exists on the default branch, character for character:
gh api "repos/phmatray/FormCraft/contents/.github/workflows/continuous.yml?ref=dev" --jq .name

Expected: continuous.yml. Anything else (404, a different name) means the policy names a file that does not exist and the first tag will fail.

  • Step 2: Confirm the secret inventory, on every scope that exists for a user account:
gh api repos/phmatray/FormCraft/actions/secrets   --jq '[.secrets[].name]'
gh api repos/phmatray/FormCraft/actions/variables --jq '[.variables[].name]'

Expected: secrets ["NUGET_API_KEY","NUGET_USER"], variables []. The organization-secrets endpoint returns HTTP 422 here because phmatray is a user, not an org — that 422 is the evidence that repo scope is complete. Record it; do not re-derive it from gh secret list alone.

  • Step 3: Confirm the OIDC wiring is present on dev and that no reference to the long-lived secret survives:
gh api "repos/phmatray/FormCraft/contents/.github/workflows/continuous.yml?ref=dev" --jq .content \
  | base64 -d | grep -nE "id-token: write|NuGet/login@|secrets.NUGET_USER|secrets.NUGET_API_KEY"

Expected: hits for id-token: write, NuGet/login@8d196754b4036150537f80ac539e15c2f1028841, and secrets.NUGET_USER; zero hits for secrets.NUGET_API_KEY. (Decode the base64 from raw JSON — gh api --jq @tsv corrupts base64 content.)

  • Step 4 (owner-only): On nuget.org → Account settings → Trusted Publishing, confirm the FormCraft policy reads exactly: Package owner phmatray · Repository owner phmatray · Repository FormCraft · Workflow file continuous.yml · Environment empty. nuget.org does not validate the workflow filename, so read it rather than assume it.

  • Step 5: Post the four results as a comment on this issue, then proceed. If any check fails, stop and fix it first — do not tag.


Task 2: Guard the OIDC wiring so it cannot be silently regenerated away

build/Build.cs:20-32 still declares ImportSecrets = ["NUGET_API_KEY"] and omits IdToken from WritePermissions. Harmless only because AutoGenerate = false; flipping that flag regenerates continuous.yml and deletes the OIDC block. This task removes the stale declaration and adds a test that fails if the wiring ever disappears.

Files:

  • Create: FormCraft.UnitTests/Ci/TrustedPublishingWorkflowTests.cs
  • Modify: build/Build.cs:20-32 (the [GitHubActions] attribute)

Interfaces: Produces no public API. GitHubActionsPermissions.IdToken is confirmed present in Nuke.Common 10.1.0.

  • Step 1: Write the failing test. Create FormCraft.UnitTests/Ci/TrustedPublishingWorkflowTests.cs:
using System.IO;

namespace FormCraft.UnitTests.Ci;

/// <summary>
/// Guards the NuGet Trusted Publishing wiring (#173). The publish path runs only on a version
/// tag — a handful of times a year — so a regression here is invisible until a release breaks.
/// These tests fail if the OIDC exchange is removed from the workflow, or if build/Build.cs
/// starts declaring the long-lived key again (which a future AutoGenerate = true would bake
/// straight back into continuous.yml).
/// </summary>
public class TrustedPublishingWorkflowTests
{
    private static string RepoRoot()
    {
        var dir = new DirectoryInfo(AppContext.BaseDirectory);
        while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "FormCraft.sln")))
            dir = dir.Parent;

        dir.ShouldNotBeNull("could not locate FormCraft.sln above the test output directory");
        return dir!.FullName;
    }

    private static string ReadWorkflow() =>
        File.ReadAllText(Path.Combine(RepoRoot(), ".github", "workflows", "continuous.yml"));

    private static string ReadBuildScript() =>
        File.ReadAllText(Path.Combine(RepoRoot(), "build", "Build.cs"));

    [Fact]
    public void Continuous_Workflow_Should_Request_The_OidcToken()
    {
        ReadWorkflow().ShouldContain("id-token: write");
    }

    [Fact]
    public void Continuous_Workflow_Should_Exchange_The_OidcToken_For_A_ShortLived_Key()
    {
        var workflow = ReadWorkflow();

        // Pinned by digest, never by a moving tag.
        workflow.ShouldContain("NuGet/login@8d196754b4036150537f80ac539e15c2f1028841");
        workflow.ShouldContain("secrets.NUGET_USER");
        workflow.ShouldContain("steps.nuget-login.outputs.NUGET_API_KEY");
    }

    [Fact]
    public void Continuous_Workflow_Should_Gate_The_Login_On_A_Version_Tag()
    {
        // Gating on the TRIGGER, not on NUGET_USER: a fork's PR cannot obtain this repo's OIDC
        // token, and a secret-based guard would turn a missing policy into a green build with an
        // unpublished version. Do not "simplify" this to `if: env.NUGET_USER != ''`.
        ReadWorkflow().ShouldContain("startsWith(github.ref, 'refs/tags/v')");
    }

    [Fact]
    public void No_LongLived_NuGetApiKey_Secret_Should_Be_Referenced_Anywhere()
    {
        ReadWorkflow().ShouldNotContain("secrets.NUGET_API_KEY");
        ReadBuildScript().ShouldNotContain("ImportSecrets");
    }
}
  • Step 2: Run the suite and verify it fails.
dotnet test FormCraft.UnitTests/FormCraft.UnitTests.csproj -c Release

Expected: No_LongLived_NuGetApiKey_Secret_Should_Be_Referenced_Anywhere FAILS — build/Build.cs:30 still reads ImportSecrets = ["NUGET_API_KEY"],. The other three should already pass; that is intentional, they are the regression net.

  • Step 3: Fix the Nuke attribute. In build/Build.cs, replace the [GitHubActions] attribute's stale lines so it reads:
// continuous.yml is HAND-MAINTAINED (AutoGenerate = false) because it carries the NuGet Trusted
// Publishing steps that Nuke cannot express: the OIDC exchange via NuGet/login before the build.
// Do NOT set AutoGenerate = true — regenerating this file would silently delete that block and
// the next release would fail. TrustedPublishingWorkflowTests guards the result.
[GitHubActions(
    "continuous",
    GitHubActionsImage.UbuntuLatest,
    AutoGenerate = false,
    OnPushBranches = ["main", "dev"],
    OnPushTags = ["v*"],
    OnPullRequestBranches = ["main", "dev"],
    InvokedTargets = [nameof(Continuous)],
    EnableGitHubToken = true,
    FetchDepth = 0,
    CacheKeyFiles = ["global.json", "**/*.csproj"],
    WritePermissions = [
        GitHubActionsPermissions.Contents,
        GitHubActionsPermissions.Packages,
        GitHubActionsPermissions.IdToken])]

The ImportSecrets = ["NUGET_API_KEY"], line is deleted: the key now arrives as a step output, not as an imported secret.

  • Step 4: Run the suite and verify it passes.
dotnet build -c Release && dotnet test FormCraft.UnitTests/FormCraft.UnitTests.csproj -c Release

Expected: build succeeds (TreatWarningsAsErrors=true, so any unused-using warning from the edit fails here) and all four tests PASS.

  • Step 5: Commit.
git -c user.email=phmatray@gmail.com -c user.name="Philippe Matray" \
  add FormCraft.UnitTests/Ci/TrustedPublishingWorkflowTests.cs build/Build.cs
git -c user.email=phmatray@gmail.com -c user.name="Philippe Matray" \
  commit -m "ci(release): guard trusted-publishing wiring and drop the stale NUGET_API_KEY import"
  • Step 6: Open the PR against dev and confirm CI is green before merging:
gh pr create --base dev --title "ci(release): guard trusted-publishing wiring (#<this-issue>)" --fill

Task 3: Rehearse the publish path with a prerelease tag

The first execution of a never-run publish path should not also be a user-facing release. IsOnVersionTag() matches ^v\d+\.\d+\.\d+ with no $ anchor, so a prerelease tag exercises the identical path.

Files: none. This task pushes a tag and observes.

Interfaces: Consumes Task 1's verified policy and Task 2's merged guard. Produces the go/no-go for Task 4.

  • Step 1 (owner-only): From an up-to-date dev, cut and push the rehearsal tag:
git -C <repo> checkout dev && git -C <repo> pull --ff-only
git -C <repo> tag v3.1.1-rc.1
git -C <repo> push origin v3.1.1-rc.1
  • Step 2: Watch the run. Both continuous.yml and release.yml fire on this tag; the publish lives in continuous.
gh run list --workflow=continuous.yml --limit 3
gh run watch <run-id>
  • Step 3: Read the run log and confirm the path actually executed — not merely that the run was green. A skipped publish also produces a green run, which is the exact failure mode this rehearsal exists to catch:
gh run view <run-id> --log | grep -nE "NuGet login|PublishIfNeeded conditions|IsOnVersionTag|Pushing|Publish|error"

Expected: the NuGet login (OIDC -> short-lived key) step ran (not skipped); PublishIfNeeded conditions: shows IsServerBuild: True and IsOnVersionTag: True; Publish executed and pushed. If PublishIfNeeded was skipped, the run is green and the version is unpublished — treat that as a failure.

  • Step 4: Verify against the nuget.org index — both ids. Probe the index, never the local ~/.nuget/packages/ cache, which can hold a package that exists nowhere:
for id in formcraft formcraft.formudblazor; do
  printf '%s ' "$id"
  curl -sS "https://api.nuget.org/v3-flatcontainer/$id/index.json" | grep -c '3.1.1-rc.1'
done
# control probe — must print 200:
curl -sS -o /dev/null -w '%{http_code}\n' https://api.nuget.org/v3-flatcontainer/newtonsoft.json/index.json

Expected: 1 for both ids. Indexing can lag a few minutes; re-probe before concluding failure.

  • Step 5: Record the outcome as a comment on this issue. If either package is missing, stop — fix the policy and re-tag (--skip-duplicate makes a re-run of the same version harmless). Do not proceed to Task 5 under any circumstances.

Task 4: Ship a real release through the OIDC path

Files: none.

Interfaces: Consumes Task 3's proven path. Produces the published release that gates Task 5.

  • Step 1 (owner-only): Cut the real tag from dev (MinVer derives the version from it; there is no version file to edit):
git -C <repo> tag v3.1.1
git -C <repo> push origin v3.1.1
  • Step 2: Watch continuous.yml and re-run the Step-3 log grep from Task 3, confirming the login step ran and Publish executed.

  • Step 3: Verify both packages on the nuget.org index:

for id in formcraft formcraft.formudblazor; do
  printf '%s ' "$id"
  curl -sS "https://api.nuget.org/v3-flatcontainer/$id/index.json" | grep -c '"3.1.1"'
done

Expected: 1 for both.

  • Step 4: Confirm release.yml created the GitHub release with the git-cliff changelog, and that it did not collide with Nuke's CreateGitHubRelease (which is deliberately not chained):
gh release view v3.1.1 --json name,createdAt,body --jq '.name'

Task 5: Retire the long-lived key — GitHub secret and nuget.org

Destructive and owner-only. Only after Task 4 verified a real publication. Deleting first leaves no way back if the policy is wrong.

Files: none.

Interfaces: Consumes Task 4's verified release.

  • Step 1: Re-confirm the gate before touching anything — Task 4 Step 3 printed 1 for both package ids. If you cannot point at that output, go back.

  • Step 2 (owner-only): Delete the GitHub repo secret:

gh secret delete NUGET_API_KEY -R phmatray/FormCraft
gh api repos/phmatray/FormCraft/actions/secrets --jq '[.secrets[].name]'

Expected: ["NUGET_USER"].

  • Step 3 (owner-only): Revoke the key on nuget.org. Deleting the GitHub secret revokes nothing — the key stays valid until removed at Account settings → API keys. Delete the key that was used for FormCraft publishing.

  • Step 4: Record the completion in repo-audit/TRUSTED_PUBLISHING.md, whose step 6 tracks exactly this residue across the portfolio, and note it on this issue. Then close.


Task: publish policy for the third package, FormCraft.ForFluentUI

Folded in from the merge of #261 (issue #260) — an instance of this issue's cause, not a separate one.

Pack now produces a third package. Publish globs artifacts/*.nupkg, so it will try to push
FormCraft.ForFluentUI on the next release, and a brand-new package id has no Trusted Publishing
policy. dotnet nuget push returns 403, and --skip-duplicate does not cover that.

The failure lands in the worst position this issue already documents: release-please has created the
tag and the GitHub Release before the publish job runs, so the release exists with packages
missing. .github/workflows/release-please.yml now says so in its own comment block.

  • Create the nuget.org Trusted Publishing policy for FormCraft.ForFluentUI, naming this repository and release-please.yml, before the first release that includes it — and confirm the package id can be claimed by the publishing account on its first push.

Metadata

Metadata

Assignees

No one assigned

    Labels

    priority:highShould fix soonstatus:blockedBlocked by external dependencystatus:triagedClassified and ready for analysis/worktype:choreMaintenance, dependencies, tooling

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions