Skip to content

Publish to NuGet with Trusted Publishing and create the GitHub release - #1386

Draft
sacOO7 wants to merge 1 commit into
ci/release-workflow-prechecksfrom
ci/release-workflow-publish
Draft

sacOO7 wants to merge 1 commit into
ci/release-workflow-prechecksfrom
ci/release-workflow-publish

Conversation

@sacOO7

@sacOO7 sacOO7 commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR adds the two jobs that let release.yml actually release:

  • publish pushes Ably.PubSub.Core, waits until nuget.org serves it, then pushes Ably.PubSub.Device and Ably.PubSub.Server. It uses nuget.org Trusted Publishing, so no API key is stored anywhere, and it runs only after a reviewer of the nuget environment approves.
  • github-release creates an annotated tag and the GitHub release, with the CHANGELOG notes and the .unitypackage attached.

It also changes the release process in CONTRIBUTING.md to use the workflow, and keeps the manual process as a fallback.

It's stacked on #1385. See "Stacked on PR 1" at the end.

Before merging

Do these in the repo settings first. If a job names an environment that doesn't exist, GitHub creates it on first use with no protection rules. The nuget environment doesn't exist yet (checked on 2026-11-04).

While NUGET_USER is missing, the login would fail before any push. Don't rely on that: once the secret is added before the protection rules, a real run could publish without any review.

  • Create the environment nuget (Settings → Environments).
  • Required reviewers: at least two people. One approval is enough; the second person is for availability. Turn on "Prevent self-review".
  • Deployment branches and tags: "Selected branches and tags", with the branch main only and no tag patterns.
  • Turn off "Allow administrators to bypass configured protection rules".
  • Add the environment secret NUGET_USER: the nuget.org username of the person who created the "Ably PubSub" Trusted Publishing policy.
    • It isn't ably-realtime, and it isn't an email address. nuget.org looks the policy up by its creator, and the policy page shows only the owner.
    • That person must stay a member of the ably-realtime org, or the policy goes inactive.
  • Read the settings back:
    • gh api repos/ably/ably-pubsub-dotnet/environments/nuget shows a required_reviewers rule and custom_branch_policies: true;
    • gh api repos/ably/ably-pubsub-dotnet/environments/nuget/deployment-branch-policies lists only main;
    • gh secret list --env nuget -R ably/ably-pubsub-dotnet shows NUGET_USER.

What's in it

publish

It runs only on a real run on main, after the approval. It holds the only OIDC-capable token, so it checks out and runs no repository code: no checkout, no Cake.

  1. It downloads the output-package artifact that the same run packed and summarised. The pushed files are therefore exactly the ones the summary lists with SHA-256 values.
  2. NuGet/login (v1.2.0, pinned by SHA) exchanges the job's GitHub OIDC token for a short-lived nuget.org API key.
    • The job's only permission is id-token: write.
    • user comes from the NUGET_USER environment secret, which GitHub releases only after the approval.
  3. It pushes Ably.PubSub.Core with --skip-duplicate.
  4. It waits up to 30 minutes for Core to appear in the nuget.org flat container, the index that restore reads. Both doors pin Core to the exact version ([x.y.z]), so they mustn't go out before Core resolves.
  5. It logs in again, because the first key may be close to expiry after the wait. Then it pushes Device and Server, each with --skip-duplicate.
  6. It waits up to 15 minutes for the doors to appear. If they don't, it only warns.
  7. If any step fails, it writes recovery notes to the run summary.

github-release

This job holds a write token, so it doesn't check out or run repository code either. It works only from the run's artifacts and the gh CLI.

  • The tag. It creates an annotated tag <version> on GITHUB_SHA, tagged by github-actions[bot] and dated at release time. The 2.0.0 tag is lightweight, so it carries the merge commit's date instead.
    • If the tag already exists on the same commit, it's reused.
    • If it exists on another commit, the job fails.
    • It finds an existing tag with git ls-remote against the repository URL, so it needs no checkout.
  • The release notes come from the release-notes artifact that PR 1's plan job extracts with Release.Notes (cake-build/release.cake). They are the CHANGELOG section for the version, without its heading and its "Full Changelog" line, the same shape as the 2.0.0 release body. A dry run shows the same notes in its summary.
  • The release is created with gh release create --verify-tag, with ably.pubsub.<version>.unitypackage attached. A version with a - suffix is marked as a prerelease and isn't marked "Latest".
  • On a re-run, an existing release gets the asset only if it lacks it (there's no --clobber), and a leftover draft is published. That keeps re-runs working if immutable releases are turned on later.

Both jobs carry if: ${{ !inputs.dry_run && github.ref == 'refs/heads/main' }}. That adds to the main-only step in prechecks and to the environment's branch rule. Dry runs never reach either job.

Docs and the workflow header

  • CONTRIBUTING.md, "Release process" is rewritten around the workflow, in these sections:

    • Prepare the release PR: the existing steps (branch, version files, Unity plugin, CHANGELOG, commit, PR), plus a local Release.Preflight run before the PR is opened. The README PackageReference step is gone: the README hasn't pinned a version since Simplify the README for the 2.0 device and server packages #1384. The CHANGELOG step gives the heading format the workflow checks.
    • Publish: a dry run, check its summary, the real run, then the approval. This replaces the manual packing, pushing, tagging and release steps.
    • Approving a release: the reviewer checklist, including a look at CI on the commit, because the workflow doesn't gate on CI.
    • After the release: check nuget.org and the GitHub release, then post to the Ably changelog.
    • If something goes wrong: the recovery table.
    • Manual fallback: package.yml plus a short-lived API key scoped to Ably.PubSub.*. The release notes come from the run's release-notes artifact or from Release.Notes.
    • Withdrawing a version: unlist the doors first, then Core.

    Nothing from the old release steps is lost: each one is kept, or replaced by the workflow and kept in the manual fallback.

  • cake-build/README.md: the packaging paragraph no longer says that nothing is published. It now says that release.yml calls package.yml and publishes, and that a hand push is the fallback.

  • release.yml: the header points to "If something goes wrong" in CONTRIBUTING.md, and PR 1's "nothing was published" warning step is removed. Comments are brief, and only where the reason isn't obvious.

Recovery

If a run fails after the approval, use "Re-run failed jobs" on the same run. Every push uses --skip-duplicate, and the tag and release steps are idempotent, so whatever already succeeded is skipped. The pre-checks refuse a new dispatch for a version that is partly published. The table under "If something goes wrong" in CONTRIBUTING.md covers each failure point. That includes a Core push that fails nuget.org validation: the version number is then used up, so release the next patch.

Decisions taken, and open questions

Taken, using the plan's defaults:

  • Order: push, then tag, then release. Nothing public appears before the packages exist.
  • Release assets: only the .unitypackage, as for 2.0.0.

Still open. Only the Phase 0 settings above block the merge:

  1. Who created the "Ably PubSub" policy? Their nuget.org username goes in NUGET_USER.
  2. Who are the nuget environment's reviewers?
  3. Should the policy scope be narrowed to "Push only new package versions", now that the three IDs exist?
  4. Should the first release go straight to 2.0.1, or should we rehearse first? The options are a prerelease such as 2.0.1-rc.1, or a fork that publishes to int.nugettest.org.
  5. Should GitHub immutable releases be turned on for this repo? The release step works either way.
  6. Should a tag ruleset protect release tags? If so, it must let GitHub Actions bypass it, or the tag step fails.

Testing done

  • Lint: actionlint 1.7.12, with shellcheck 0.11.0, reports nothing for release.yml. Both workflow files parse, and all 14 run: blocks pass bash -n. The shell in the final file is 98 lines (93 in multi-line blocks, plus five one-line commands); it was 180 before the checks moved to Cake. Most of what remains is the push, wait and gh steps, which stay in YAML on purpose.

  • Release notes: Release.Notes, tested in PR 1, reproduces the published 2.0.0 release body exactly, apart from the CRLF line endings GitHub stored.

  • Wait loops, with a mocked curl:

    • Core found at once;
    • Core found after retries;
    • Core never found: the timeout fails the job with re-run instructions;
    • the doors' timeout only warns.
  • Tag step, run from an empty directory to prove it needs no checkout:

    • against the live remote, the existing lightweight 2.0.0 tag is reused at the same commit and fails at another commit;
    • against a local remote, an annotated tag resolves through its ^{} line;
    • for a new version, it calls git/tags and then git/refs with the expected arguments (mocked gh).
  • Release step, with a mocked gh, reading the notes from release-notes/release-notes.md:

    • a new release;
    • a new prerelease;
    • an existing release that has the asset: no change;
    • an existing release without the asset: upload only;
    • a leftover draft: upload, then publish.
  • Not testable before a real release:

    • the Trusted Publishing login;
    • the pushes;
    • the approval flow;
    • re-runs.

    A login problem fails before anything is pushed. The first real release, such as 2.0.1, is the end-to-end test. PR 1's dry runs already cover the pre-checks and packing.

  • To confirm before or at the first release:

    • on PR 1's first dry run, re-run only the plan job and check that it downloads output-package from the earlier attempt. "Re-run failed jobs" recovery relies on that;
    • the org's artifact retention must be at least 30 days, so a re-run within GitHub's 30-day window still finds output-package. The setting couldn't be read through the API (403);
    • whether "Re-run failed jobs" asks for the nuget approval again isn't documented. Plan for a reviewer being needed.
  • No .NET build, because no product or test code changed, and this PR doesn't change any Cake script.

Stacked on PR 1

This PR targets ci/release-workflow-prechecks (#1385), so its diff shows only these changes. Merge PR 1 first, then retarget this PR to main.

Part of #1377. That issue stays open until the first real release.

@coderabbitai

coderabbitai Bot commented Nov 4, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • 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

Add the publish job to release.yml. It runs only on a real run on main,
in the nuget environment, with id-token: write as its only permission.
It checks out and runs no repository code. NuGet/login exchanges the
OIDC token for a short-lived key. The job pushes Ably.PubSub.Core and
waits until nuget.org serves it, then logs in again and pushes
Ably.PubSub.Device and Ably.PubSub.Server. Every push uses
--skip-duplicate, so failed jobs can be re-run.

Add the github-release job. It creates an annotated, dated tag on the
released commit, and a GitHub release with the .unitypackage attached.
The notes come from the release-notes artifact that the plan job
extracts from CHANGELOG.md, so this job, which holds a write token,
runs no repository code either. Both steps are idempotent when re-run.

Rewrite the release process in CONTRIBUTING.md around the workflow:
preparing the release PR (with a local Release.Preflight run),
publishing, approving, after the release, what to do if something goes
wrong, the manual fallback and withdrawing a version. Update the
packaging paragraph in cake-build/README.md, and remove the "nothing
was published" warning that the previous change added.

This branch was successfully deployed

1 active deployment
staging/pull/1386/features — f2ed32b9 Deployed Nov 4, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant