ci: NuGet trusted-publishing workflow for 4 semconv packages - #158
Conversation
Publishes Qyl.SemanticConventions, Qyl.OpenTelemetry.SemanticConventions, Qyl.OpenTelemetry.SemanticConventions.Incubating, and Qyl.OpenTelemetry.SemanticConventions.Analyzers to nuget.org via OIDC (NuGet/login@v1, environment `nuget`, no long-lived API key). Matrix publish isolates failures per package. fail-fast:false keeps partial publishes working during the 3-PR bootstrap window (workflow, semconv feature, analyzer feature) — once the two feature PRs merge, all 4 legs succeed on every tag push. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
📝 WalkthroughWalkthroughA new GitHub Actions workflow file is added at 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Pull request overview
Adds a new GitHub Actions workflow to publish the upcoming semconv-related NuGet packages via NuGet.org OIDC trusted publishing (no long-lived API key secret), and to create a GitHub Release for the version.
Changes:
- Introduces
.github/workflows/nuget-publish.ymltriggered byv*tags or manual dispatch with a version. - Builds the targeted package projects on Ubuntu/Windows/macOS before publishing.
- Publishes via
NuGet/login@v1(OIDC) and then creates a GitHub Release for the tag.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| - name: Build Qyl.SemanticConventions | ||
| run: dotnet build packages/Qyl.SemanticConventions/Qyl.SemanticConventions.csproj -c Release -p:Version=${{ needs.version.outputs.version }} | ||
| - name: Build Qyl.OpenTelemetry.SemanticConventions | ||
| run: dotnet build packages/Qyl.OpenTelemetry.SemanticConventions/Qyl.OpenTelemetry.SemanticConventions.csproj -c Release -p:Version=${{ needs.version.outputs.version }} | ||
| - name: Build Qyl.OpenTelemetry.SemanticConventions.Incubating | ||
| run: dotnet build packages/Qyl.OpenTelemetry.SemanticConventions.Incubating/Qyl.OpenTelemetry.SemanticConventions.Incubating.csproj -c Release -p:Version=${{ needs.version.outputs.version }} | ||
| - name: Build Qyl.OpenTelemetry.SemanticConventions.Analyzers |
There was a problem hiding this comment.
The header says partial publishes are expected during the bootstrap window when only a subset of the 4 csprojs exist on a given ref, but this build job hard-builds all four projects unconditionally. If any of the package csprojs are missing (or intentionally not present yet), the build job will fail and block the publish matrix entirely. Consider making the build job conditional per project (e.g., check file existence and skip) or restructuring so the per-package publish matrix does the build/pack for just the packages that exist.
| - name: Build Qyl.SemanticConventions | |
| run: dotnet build packages/Qyl.SemanticConventions/Qyl.SemanticConventions.csproj -c Release -p:Version=${{ needs.version.outputs.version }} | |
| - name: Build Qyl.OpenTelemetry.SemanticConventions | |
| run: dotnet build packages/Qyl.OpenTelemetry.SemanticConventions/Qyl.OpenTelemetry.SemanticConventions.csproj -c Release -p:Version=${{ needs.version.outputs.version }} | |
| - name: Build Qyl.OpenTelemetry.SemanticConventions.Incubating | |
| run: dotnet build packages/Qyl.OpenTelemetry.SemanticConventions.Incubating/Qyl.OpenTelemetry.SemanticConventions.Incubating.csproj -c Release -p:Version=${{ needs.version.outputs.version }} | |
| - name: Build Qyl.OpenTelemetry.SemanticConventions.Analyzers | |
| - name: Build Qyl.SemanticConventions | |
| if: ${{ hashFiles('packages/Qyl.SemanticConventions/Qyl.SemanticConventions.csproj') != '' }} | |
| run: dotnet build packages/Qyl.SemanticConventions/Qyl.SemanticConventions.csproj -c Release -p:Version=${{ needs.version.outputs.version }} | |
| - name: Build Qyl.OpenTelemetry.SemanticConventions | |
| if: ${{ hashFiles('packages/Qyl.OpenTelemetry.SemanticConventions/Qyl.OpenTelemetry.SemanticConventions.csproj') != '' }} | |
| run: dotnet build packages/Qyl.OpenTelemetry.SemanticConventions/Qyl.OpenTelemetry.SemanticConventions.csproj -c Release -p:Version=${{ needs.version.outputs.version }} | |
| - name: Build Qyl.OpenTelemetry.SemanticConventions.Incubating | |
| if: ${{ hashFiles('packages/Qyl.OpenTelemetry.SemanticConventions.Incubating/Qyl.OpenTelemetry.SemanticConventions.Incubating.csproj') != '' }} | |
| run: dotnet build packages/Qyl.OpenTelemetry.SemanticConventions.Incubating/Qyl.OpenTelemetry.SemanticConventions.Incubating.csproj -c Release -p:Version=${{ needs.version.outputs.version }} | |
| - name: Build Qyl.OpenTelemetry.SemanticConventions.Analyzers | |
| if: ${{ hashFiles('packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Qyl.OpenTelemetry.SemanticConventions.Analyzers.csproj') != '' }} |
| # waits for all matrix combinations. | ||
| release: | ||
| needs: [version, publish] | ||
| if: needs.version.outputs.is_release == 'true' |
There was a problem hiding this comment.
The comments say this release job runs “after every publish leg finishes (success or partial)”, but needs: [version, publish] means it only runs if the overall publish job succeeds. With a matrix, a single failing leg makes publish fail, so this job will be skipped and you won’t get a GitHub release on partial bootstrap publishes. If you want the release job to run after the matrix completes regardless of failures, use an if: always() && needs.version.outputs.is_release == 'true' condition (and optionally gate creation based on needs.publish.result).
| if: needs.version.outputs.is_release == 'true' | |
| if: always() && needs.version.outputs.is_release == 'true' |
| version: | ||
| description: 'Version to publish (without v prefix, e.g. 1.0.0)' | ||
| required: true | ||
| type: string | ||
|
|
||
| concurrency: | ||
| group: nuget-publish-${{ github.ref }} | ||
| cancel-in-progress: false | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| env: | ||
| DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 | ||
| DOTNET_NOLOGO: true | ||
| DOTNET_CLI_TELEMETRY_OPTOUT: 1 | ||
| CI: true | ||
|
|
||
| jobs: | ||
| version: | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| version: ${{ steps.ver.outputs.version }} | ||
| is_release: ${{ steps.ver.outputs.is_release }} | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| with: | ||
| fetch-depth: 0 | ||
| - id: ver | ||
| env: | ||
| EVENT_NAME: ${{ github.event_name }} | ||
| DISPATCH_VERSION: ${{ github.event.inputs.version }} | ||
| REF_NAME: ${{ github.ref_name }} | ||
| run: | | ||
| if [ "$EVENT_NAME" = "workflow_dispatch" ]; then | ||
| echo "version=${DISPATCH_VERSION}" >> "$GITHUB_OUTPUT" | ||
| echo "is_release=true" >> "$GITHUB_OUTPUT" | ||
| elif [[ "$GITHUB_REF" == refs/tags/v* ]]; then | ||
| echo "version=${REF_NAME#v}" >> "$GITHUB_OUTPUT" | ||
| echo "is_release=true" >> "$GITHUB_OUTPUT" |
There was a problem hiding this comment.
The workflow_dispatch input expects a version “without v prefix”, but the version step uses the input verbatim. If a user supplies v1.0.0 by mistake, dotnet pack -p:Version=... will receive an invalid NuGet version and the release job will try to create tag vv1.0.0. Consider normalizing/validating the input (e.g., strip an optional leading v and fail fast if the remaining string isn’t a valid NuGet version).
| # Cross-branch reality (2026-04): | ||
| # Packages 1–3 live on branch `claude/focused-gauss-3c1f8d` (semconv PR). | ||
| # Package 4 lives on branch `claude/goofy-cohen-8f4c45` (analyzer PR). | ||
| # Until BOTH PRs merge to main, a tag push or workflow_dispatch on a feature | ||
| # branch will only publish the subset of packages present in that ref. The | ||
| # publish matrix is fail-fast:false by design — partial success is the | ||
| # expected outcome during this bootstrap window. Once both PRs merge, every | ||
| # tag push produces all 4 nupkgs. |
There was a problem hiding this comment.
The header comment hard-codes temporary branch names and a point-in-time note (“Cross-branch reality (2026-04) …”). This will become stale immediately after merge and can mislead future maintainers. Consider replacing it with a durable reference to the prerequisite PRs/packages (or removing the branch-specific details entirely).
| # Cross-branch reality (2026-04): | |
| # Packages 1–3 live on branch `claude/focused-gauss-3c1f8d` (semconv PR). | |
| # Package 4 lives on branch `claude/goofy-cohen-8f4c45` (analyzer PR). | |
| # Until BOTH PRs merge to main, a tag push or workflow_dispatch on a feature | |
| # branch will only publish the subset of packages present in that ref. The | |
| # publish matrix is fail-fast:false by design — partial success is the | |
| # expected outcome during this bootstrap window. Once both PRs merge, every | |
| # tag push produces all 4 nupkgs. | |
| # This workflow publishes whichever package projects are present in the | |
| # checked-out ref. If a tag or manual run targets a ref that does not yet | |
| # contain all four package projects, only the available subset can be packed | |
| # and published. The publish matrix is configured with fail-fast: false so | |
| # that each package publish attempt proceeds independently. Refs that contain | |
| # all four package projects will publish all four nupkgs. |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/nuget-publish.yml:
- Around line 37-42: Normalize and validate the manual version input from
workflow_dispatch by stripping an optional leading "v" from inputs.version and
verifying the remaining string is a valid semver before using it to construct
tags/releases; update the workflow steps that read inputs.version (the
workflow_dispatch input block and the steps referenced around the regions
corresponding to lines 73-75 and 183-187) to first set a sanitized variable
(e.g., "version" or "normalized_version") by removing a leading 'v' if present
and then fail early with a clear message if the sanitized value is not a valid
semantic version, and use that sanitized variable everywhere you currently use
inputs.version.
- Around line 167-172: The release job is skipped if any matrix publish leg
fails because "needs: publish" requires all legs to succeed; create an explicit
fan-in aggregator job (e.g., "publish-fan-in") that has needs: publish and runs
regardless of leg outcomes (use if: always() or run with a step that uses
job.status), compute a boolean output like
publish-fan-in.outputs.partial_or_full_success (set via a step that checks
needs.publish[*].result or job.status to detect any_success or all_failed), and
then change the release job to depend on publish-fan-in (replace needs: publish
with needs: [version, publish-fan-in]) and use the aggregator output in the
release if condition instead of relying on needs.publish; keep the existing
version check (needs.version.outputs.is_release) combined with the aggregator
output so release runs for intended partial publishes while preserving
fail-fast: false on the matrix.
- Line 64: Replace every mutable action reference in the workflow (e.g.,
actions/checkout@v6 and actions/setup-dotnet@v5) with their corresponding
immutable 40-character commit SHAs; locate each "uses:" entry that currently
uses a tag and swap the tag (e.g., `@v6`, `@v5`) for the correct full commit SHA for
that action, ensuring consistency for all occurrences of actions/checkout and
actions/setup-dotnet in the file so the workflow is pinned to fixed commits.
- Around line 86-108: The build job currently unconditionally runs dotnet build
for four projects
(packages/Qyl.SemanticConventions/Qyl.SemanticConventions.csproj,
packages/Qyl.OpenTelemetry.SemanticConventions/Qyl.OpenTelemetry.SemanticConventions.csproj,
packages/Qyl.OpenTelemetry.SemanticConventions.Incubating/Qyl.OpenTelemetry.SemanticConventions.Incubating.csproj,
packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Qyl.OpenTelemetry.SemanticConventions.Analyzers.csproj)
which causes failure when one or more paths are missing; modify the workflow so
the build step either reuses the existing package matrix (matrix-driven job) or
wraps each dotnet build invocation in a file-existence check (e.g., test for the
.csproj path before running dotnet build), ensuring the build job succeeds and
allows the publish matrix job to run.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: da15c51a-bb48-4a95-9be8-2abffce05532
📒 Files selected for processing (1)
.github/workflows/nuget-publish.yml
📜 Review details
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: Dependency Audit
- GitHub Check: Frontend (React)
- GitHub Check: Backend (.NET)
- GitHub Check: Analyze (javascript-typescript)
- GitHub Check: Analyze (csharp)
🧰 Additional context used
📓 Path-based instructions (1)
.github/**
⚙️ CodeRabbit configuration file
GitHub Actions workflows. Review for: action version pinning (use SHA not tags), proper secret handling (no secrets in logs), unnecessary workflow triggers, and job dependency correctness. Flag missing concurrency groups on push-triggered workflows.
Files:
.github/workflows/nuget-publish.yml
| workflow_dispatch: | ||
| inputs: | ||
| version: | ||
| description: 'Version to publish (without v prefix, e.g. 1.0.0)' | ||
| required: true | ||
| type: string |
There was a problem hiding this comment.
Normalize workflow_dispatch versions before using them.
The manual path forwards inputs.version verbatim, while .github/workflows/release.yml:14-18 tells operators to enter versions like v1.0.0. If someone follows that convention here, this workflow emits version=v1.0.0 and then creates a vv1.0.0 GitHub release tag. Strip an optional leading v and validate the remainder before setting the output.
Suggested fix
- id: ver
env:
EVENT_NAME: ${{ github.event_name }}
DISPATCH_VERSION: ${{ github.event.inputs.version }}
REF_NAME: ${{ github.ref_name }}
run: |
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
- echo "version=${DISPATCH_VERSION}" >> "$GITHUB_OUTPUT"
+ VERSION="${DISPATCH_VERSION#v}"
+ test -n "$VERSION"
+ echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "is_release=true" >> "$GITHUB_OUTPUT"
elif [[ "$GITHUB_REF" == refs/tags/v* ]]; thenAlso applies to: 73-75, 183-187
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/nuget-publish.yml around lines 37 - 42, Normalize and
validate the manual version input from workflow_dispatch by stripping an
optional leading "v" from inputs.version and verifying the remaining string is a
valid semver before using it to construct tags/releases; update the workflow
steps that read inputs.version (the workflow_dispatch input block and the steps
referenced around the regions corresponding to lines 73-75 and 183-187) to first
set a sanitized variable (e.g., "version" or "normalized_version") by removing a
leading 'v' if present and then fail early with a clear message if the sanitized
value is not a valid semantic version, and use that sanitized variable
everywhere you currently use inputs.version.
| version: ${{ steps.ver.outputs.version }} | ||
| is_release: ${{ steps.ver.outputs.is_release }} | ||
| steps: | ||
| - uses: actions/checkout@v6 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -nP '^\s*-\s*uses:\s*[^@]+@(?![0-9a-f]{40}\b)\S+' .github/workflows/nuget-publish.ymlRepository: Alexander-Nachtmann/qyl
Length of output: 299
Pin all GitHub Actions to immutable commit SHAs.
Every uses: statement in this workflow references a mutable tag (@v5, @v6) instead of a full commit SHA. This exposes the workflow to silent behavioral changes from upstream action updates without visibility in pull requests. Replace all mutable tags with their corresponding 40-character commit SHAs:
- Line 64:
actions/checkout@v6 - Line 97:
actions/checkout@v6 - Line 98:
actions/setup-dotnet@v5 - Line 140:
actions/checkout@v6 - Line 143:
actions/setup-dotnet@v5 - Line 177:
actions/checkout@v6
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/nuget-publish.yml at line 64, Replace every mutable action
reference in the workflow (e.g., actions/checkout@v6 and
actions/setup-dotnet@v5) with their corresponding immutable 40-character commit
SHAs; locate each "uses:" entry that currently uses a tag and swap the tag
(e.g., `@v6`, `@v5`) for the correct full commit SHA for that action, ensuring
consistency for all occurrences of actions/checkout and actions/setup-dotnet in
the file so the workflow is pinned to fixed commits.
| # Compile on all three OSes to catch platform-specific issues before we push. | ||
| # Scoped to the 4 publishable csprojs — qyl.slnx contains unrelated projects | ||
| # (collector, dashboard, loom) with WIP CI state that isn't our concern here. | ||
| build: | ||
| needs: version | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| os: [ubuntu-latest, windows-latest, macos-latest] | ||
| runs-on: ${{ matrix.os }} | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - uses: actions/setup-dotnet@v5 | ||
| with: | ||
| global-json-file: global.json | ||
| - name: Build Qyl.SemanticConventions | ||
| run: dotnet build packages/Qyl.SemanticConventions/Qyl.SemanticConventions.csproj -c Release -p:Version=${{ needs.version.outputs.version }} | ||
| - name: Build Qyl.OpenTelemetry.SemanticConventions | ||
| run: dotnet build packages/Qyl.OpenTelemetry.SemanticConventions/Qyl.OpenTelemetry.SemanticConventions.csproj -c Release -p:Version=${{ needs.version.outputs.version }} | ||
| - name: Build Qyl.OpenTelemetry.SemanticConventions.Incubating | ||
| run: dotnet build packages/Qyl.OpenTelemetry.SemanticConventions.Incubating/Qyl.OpenTelemetry.SemanticConventions.Incubating.csproj -c Release -p:Version=${{ needs.version.outputs.version }} | ||
| - name: Build Qyl.OpenTelemetry.SemanticConventions.Analyzers | ||
| run: dotnet build packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Qyl.OpenTelemetry.SemanticConventions.Analyzers.csproj -c Release -p:Version=${{ needs.version.outputs.version }} |
There was a problem hiding this comment.
The bootstrap strategy is blocked by the unconditional four-project build.
Lines 101-108 build all four .csproj files before publish starts. On the branch states described in Lines 21-28, at least one of those paths is absent, so build fails and no package reaches the matrix publish step. Reuse the package matrix for build, or guard each build with a file-existence check.
As per coding guidelines, .github/**: GitHub Actions workflows. Review for: job dependency correctness.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/nuget-publish.yml around lines 86 - 108, The build job
currently unconditionally runs dotnet build for four projects
(packages/Qyl.SemanticConventions/Qyl.SemanticConventions.csproj,
packages/Qyl.OpenTelemetry.SemanticConventions/Qyl.OpenTelemetry.SemanticConventions.csproj,
packages/Qyl.OpenTelemetry.SemanticConventions.Incubating/Qyl.OpenTelemetry.SemanticConventions.Incubating.csproj,
packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Qyl.OpenTelemetry.SemanticConventions.Analyzers.csproj)
which causes failure when one or more paths are missing; modify the workflow so
the build step either reuses the existing package matrix (matrix-driven job) or
wraps each dotnet build invocation in a file-existence check (e.g., test for the
.csproj path before running dotnet build), ensuring the build job succeeds and
allows the publish matrix job to run.
| # Separate job so matrix legs don't race on `gh release create`. Runs only | ||
| # after every publish leg finishes (success or partial) — `needs: publish` | ||
| # waits for all matrix combinations. | ||
| release: | ||
| needs: [version, publish] | ||
| if: needs.version.outputs.is_release == 'true' |
There was a problem hiding this comment.
Partial publish currently suppresses release creation.
fail-fast: false only keeps the matrix running; it does not make publish succeed. If any publish leg fails, needs: publish causes release to be skipped, so the workflow cannot deliver the “partial success during bootstrap” behavior documented above. If partial publish is intentional, move release behind an explicit fan-in that uses always() plus a computed success signal.
As per coding guidelines, .github/**: GitHub Actions workflows. Review for: job dependency correctness.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/nuget-publish.yml around lines 167 - 172, The release job
is skipped if any matrix publish leg fails because "needs: publish" requires all
legs to succeed; create an explicit fan-in aggregator job (e.g.,
"publish-fan-in") that has needs: publish and runs regardless of leg outcomes
(use if: always() or run with a step that uses job.status), compute a boolean
output like publish-fan-in.outputs.partial_or_full_success (set via a step that
checks needs.publish[*].result or job.status to detect any_success or
all_failed), and then change the release job to depend on publish-fan-in
(replace needs: publish with needs: [version, publish-fan-in]) and use the
aggregator output in the release if condition instead of relying on
needs.publish; keep the existing version check
(needs.version.outputs.is_release) combined with the aggregator output so
release runs for intended partial publishes while preserving fail-fast: false on
the matrix.
Summary
Adds
.github/workflows/nuget-publish.yml— OIDC trusted-publishing workflow that publishes all 4 semconv packages to nuget.org via short-lived tokens (no long-livedNUGET_API_KEYsecret).Packages published (once this PR and PRs #156 + #157 are all merged):
Qyl.SemanticConventions(core types) — from PR feat(semconv): donation-ready NuGet packages + runtime cutover #156Qyl.OpenTelemetry.SemanticConventions(OTel stable) — from PR feat(semconv): donation-ready NuGet packages + runtime cutover #156Qyl.OpenTelemetry.SemanticConventions.Incubating(OTel experimental) — from PR feat(semconv): donation-ready NuGet packages + runtime cutover #156Qyl.OpenTelemetry.SemanticConventions.Analyzers(Roslyn + codefixes) — from PR feat(analyzers): 245 per-entry deprecated semconv diagnostics + codefixes #157Triggers:
pushof anyv*tagworkflow_dispatchwith a version string188 lines, 1 file.
Scope
Target achieved: 100% of the workflow itself.
id-token: write,contents: read)concurrencyguard prevents overlapping tag pushesfail-fast: false(documented reason: during bootstrap, tag pushes on a feature branch only contain a subset)Missing to 100%: nothing on this branch. External dependencies (out of scope):
Risk
15% — very certain goal is met, small residual risk on first-publish activation path.
dotnet nuget pushwithNuGet.ProtocolOIDC supportfail-fast: falselimits blast radius.What's left to do or delete to reach 100%
v1.0.0on main once PRs feat(semconv): donation-ready NuGet packages + runtime cutover #156 and feat(analyzers): 245 per-entry deprecated semconv diagnostics + codefixes #157 have merged; first run will publish + activate the 4 nuget.org policies.workflow_callvariant so the workflow can be invoked from a release-drafter pipeline.Test plan
.csprojfiles are present on mainworkflow_dispatchwith a pre-release version to smoke-test the matrixv*tag; confirm all 4 packages appear on nuget.org and the pending policies activate🤖 Generated with Claude Code