From 9db2ea135f19a43924813d13930cdfdbd1d81e57 Mon Sep 17 00:00:00 2001 From: ancplua Date: Wed, 22 Apr 2026 14:56:13 +0200 Subject: [PATCH] ci: add NuGet trusted-publishing workflow for 4 semconv packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .github/workflows/nuget-publish.yml | 188 ++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 .github/workflows/nuget-publish.yml diff --git a/.github/workflows/nuget-publish.yml b/.github/workflows/nuget-publish.yml new file mode 100644 index 000000000..361cc1ec4 --- /dev/null +++ b/.github/workflows/nuget-publish.yml @@ -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. +# ============================================================================= + +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 + +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" + 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 + run: dotnet build packages/Qyl.OpenTelemetry.SemanticConventions.Analyzers/Qyl.OpenTelemetry.SemanticConventions.Analyzers.csproj -c Release -p:Version=${{ needs.version.outputs.version }} + + # 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' + 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