Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 188 additions & 0 deletions .github/workflows/nuget-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# =============================================================================
# NuGet Publish — OIDC Trusted Publishing
# -----------------------------------------------------------------------------
# Publishes 4 packages to nuget.org via short-lived OIDC tokens (no long-lived
# NUGET_API_KEY secret):
# 1. Qyl.SemanticConventions (core types)
# 2. Qyl.OpenTelemetry.SemanticConventions (OTel stable attrs)
# 3. Qyl.OpenTelemetry.SemanticConventions.Incubating (OTel experimental)
# 4. Qyl.OpenTelemetry.SemanticConventions.Analyzers (Roslyn + codefixes)
#
# Trusted-publishing policies (already configured on nuget.org, per package):
# Package Owner = ANcpLua
# Repository Owner = Alexander-Nachtmann
# Repository = qyl
# Workflow File = nuget-publish.yml
# Environment = nuget
#
# Policies are in NuGet's 7-day "pending full activation" state (standard for
# private repos). First successful publish per package activates it permanently.
#
# 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.
Comment on lines +21 to +28

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
# 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.

Copilot uses AI. Check for mistakes.
# =============================================================================

name: NuGet Publish

on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: 'Version to publish (without v prefix, e.g. 1.0.0)'
required: true
type: string
Comment on lines +37 to +42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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* ]]; then

Also 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.


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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP '^\s*-\s*uses:\s*[^@]+@(?![0-9a-f]{40}\b)\S+' .github/workflows/nuget-publish.yml

Repository: 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.

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"
Comment on lines +39 to +78

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
else
COMMITS=$(git rev-list --count HEAD)
SHA=$(git rev-parse --short HEAD)
echo "version=0.0.${COMMITS}-ci.g${SHA}" >> "$GITHUB_OUTPUT"
echo "is_release=false" >> "$GITHUB_OUTPUT"
fi

# 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
Comment on lines +101 to +107

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
- 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') != '' }}

Copilot uses AI. Check for mistakes.
run: dotnet build packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Qyl.OpenTelemetry.SemanticConventions.Analyzers.csproj -c Release -p:Version=${{ needs.version.outputs.version }}
Comment on lines +86 to +108

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.


# One matrix leg per package. Each leg:
# 1. Packs the csproj.
# 2. Requests a short-lived (1 h) API key via NuGet/login@v1 (OIDC).
# 3. Pushes the .nupkg.
#
# The `nuget` environment gates with the trusted-publishing policy. Each
# matrix leg runs its own OIDC exchange → separate short-lived key per
# package, blast radius isolated. fail-fast:false keeps partial bootstrap
# publishes working (see header).
publish:
needs: [version, build]
if: needs.version.outputs.is_release == 'true'
runs-on: ubuntu-latest
environment: nuget
permissions:
id-token: write
contents: read
strategy:
fail-fast: false
matrix:
package:
- id: Qyl.SemanticConventions
path: packages/Qyl.SemanticConventions/Qyl.SemanticConventions.csproj
- id: Qyl.OpenTelemetry.SemanticConventions
path: packages/Qyl.OpenTelemetry.SemanticConventions/Qyl.OpenTelemetry.SemanticConventions.csproj
- id: Qyl.OpenTelemetry.SemanticConventions.Incubating
path: packages/Qyl.OpenTelemetry.SemanticConventions.Incubating/Qyl.OpenTelemetry.SemanticConventions.Incubating.csproj
- id: Qyl.OpenTelemetry.SemanticConventions.Analyzers
path: packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Qyl.OpenTelemetry.SemanticConventions.Analyzers.csproj
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-dotnet@v5
with:
global-json-file: global.json

- name: Pack ${{ matrix.package.id }}
run: |
dotnet pack ${{ matrix.package.path }} \
-c Release \
-o artifacts \
-p:Version=${{ needs.version.outputs.version }}

- name: Authenticate to NuGet (trusted publishing)
id: nuget-login
uses: NuGet/login@v1
with:
user: ANcpLua

- name: Push ${{ matrix.package.id }}
run: |
dotnet nuget push "artifacts/${{ matrix.package.id }}.${{ needs.version.outputs.version }}.nupkg" \
--source https://api.nuget.org/v3/index.json \
--api-key "${{ steps.nuget-login.outputs.NUGET_API_KEY }}" \
--skip-duplicate

# 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'

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
if: needs.version.outputs.is_release == 'true'
if: always() && needs.version.outputs.is_release == 'true'

Copilot uses AI. Check for mistakes.
Comment on lines +167 to +172

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ needs.version.outputs.version }}
run: |
TAG="v${VERSION}"
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Release $TAG already exists, skipping"
else
gh release create "$TAG" --generate-notes --title "$TAG"
fi
Loading