From b2842a9c11c2ccc8fca86d2e925499364837afb8 Mon Sep 17 00:00:00 2001 From: Foowy <49217685+Foowy@users.noreply.github.com> Date: Sun, 16 Aug 2026 15:51:02 +0000 Subject: [PATCH] fix(workflows): match brandedoutcast/publish-nuget's gating 1:1, verify remote tag state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to #1406. Thanks to revam for flagging that force_push_tag: true works but does so blindly, with no comparison against what the remote tag currently points to — since this repo's checkout intentionally doesn't fetch tag history, there was no way to detect a release tag pointing at an unexpected commit before overwriting it. Two fixes, both additive to the existing rickstaa/action-create-tag step rather than replacing it: - A "Check remote tag state" step runs git ls-remote before the tag step (a lightweight network call, not a history fetch) and only lets the tag step run when the remote either doesn't have the tag yet or already points at the exact commit being published. If it points anywhere else, the job fails loudly instead of the tag step force-overwriting it. - Restore 1:1 parity with the archived brandedoutcast/publish-nuget action, which #1401 didn't actually achieve: that action queried NuGet.org's own version index up front and skipped pack/push/tag entirely for an already-published version, so an unchanged-version day was a silent no-op. The composite action always ran pack/push/tag unconditionally and relied only on --skip-duplicate to no-op the package push, while the tag step still ran every time regardless — that decoupling is the actual root cause of the Shoko.Abstractions tag conflict across #1401/#1405/#1406. Added a "Check if version is already published" step that queries NuGet.org's flat-container index and gates Pack/Push/Tag on the version being genuinely new. Verified the git ls-remote logic against three simulated cases on a local checkout replicating CI's shallow, tags-excluded actions/checkout@v7 default (new tag, idempotent same-commit rerun, tag pointing at a different commit), and the NuGet.org version-check logic against real data (existing package+version, nonexistent version, nonexistent package/404). --- .github/actions/publish-nuget/action.yml | 55 +++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/.github/actions/publish-nuget/action.yml b/.github/actions/publish-nuget/action.yml index c32b99464..5bc4fae19 100644 --- a/.github/actions/publish-nuget/action.yml +++ b/.github/actions/publish-nuget/action.yml @@ -25,26 +25,79 @@ runs: shell: bash run: | version=$(dotnet msbuild "${{ inputs.project-file-path }}" -nologo -getProperty:Version) + packageId=$(dotnet msbuild "${{ inputs.project-file-path }}" -nologo -getProperty:PackageId) echo "version=$version" >> "$GITHUB_OUTPUT" + echo "package-id=$packageId" >> "$GITHUB_OUTPUT" echo "tag=${TAG_FORMAT/\*/$version}" >> "$GITHUB_OUTPUT" env: TAG_FORMAT: ${{ inputs.tag-format }} + # Matches brandedoutcast/publish-nuget's original gating: skip publishing entirely + # if this version is already on NuGet.org. + - name: Check if version is already published + id: check + shell: bash + run: | + set -euo pipefail + package_id_lower=$(echo "${{ steps.version.outputs.package-id }}" | tr '[:upper:]' '[:lower:]') + status=$(curl -s -o /tmp/nuget-versions.json -w '%{http_code}' \ + "https://api.nuget.org/v3-flatcontainer/${package_id_lower}/index.json") + + if [ "$status" = "404" ]; then + echo "already-published=false" >> "$GITHUB_OUTPUT" # package has no published versions yet + elif [ "$status" = "200" ]; then + if jq -e --arg v "${{ steps.version.outputs.version }}" '.versions | index($v)' /tmp/nuget-versions.json > /dev/null; then + echo "already-published=true" >> "$GITHUB_OUTPUT" + else + echo "already-published=false" >> "$GITHUB_OUTPUT" + fi + else + echo "::error::Unexpected HTTP $status querying NuGet.org for ${{ steps.version.outputs.package-id }}" + exit 1 + fi + - name: Pack + if: steps.check.outputs.already-published != 'true' shell: bash run: dotnet pack -c Release "${{ inputs.project-file-path }}" -o ./nupkg - name: Push + if: steps.check.outputs.already-published != 'true' shell: bash run: dotnet nuget push "./nupkg/*.nupkg" --api-key "${{ inputs.nuget-key }}" --source https://api.nuget.org/v3/index.json --skip-duplicate - name: Clean up package output + if: steps.check.outputs.already-published != 'true' shell: bash run: rm -rf ./nupkg + # Queries the remote tag instead of the local checkout, which has no tag history. + # For an annotated tag, ls-remote also returns its peeled ^{} commit ref — tail -1 + # picks that commit SHA over the tag object's own SHA. + - name: Check remote tag state + id: tagcheck + if: steps.check.outputs.already-published != 'true' + shell: bash + run: | + set -euo pipefail + tag="${{ steps.version.outputs.tag }}" + remote_sha=$(git ls-remote origin "refs/tags/${tag}" "refs/tags/${tag}^{}" | tail -1 | cut -f1) + + if [ -n "$remote_sha" ]; then + if [ "$remote_sha" = "${{ github.sha }}" ]; then + echo "Tag '${tag}' already points at this commit — nothing to do." + echo "should-tag=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "::error::Tag '${tag}' already exists on the remote and points at a different commit ($remote_sha) than this build (${{ github.sha }}). Refusing to overwrite it." + exit 1 + fi + + echo "should-tag=true" >> "$GITHUB_OUTPUT" + - name: Tag release + if: steps.check.outputs.already-published != 'true' && steps.tagcheck.outputs.should-tag == 'true' uses: rickstaa/action-create-tag@v1 with: tag: ${{ steps.version.outputs.tag }} message: "${{ inputs.project-file-path }} v${{ steps.version.outputs.version }}" - force_push_tag: true