From 717bec28763d12d68d0ab5b364caaff98536c220 Mon Sep 17 00:00:00 2001 From: Renato Golia Date: Tue, 15 Sep 2026 23:12:56 +0200 Subject: [PATCH 01/19] Use shared .NET build action --- .github/workflows/ci.yml | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 277c049..aa5d39b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,25 +20,10 @@ jobs: name: Build and Test runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Setup .NET - uses: actions/setup-dotnet@v6 + - name: Build and test + uses: Kralizek/github-actions/actions/dotnet-build@v0.2 with: - global-json-file: global.json - - - name: Restore - run: dotnet restore - - - name: Format Check - run: dotnet format --verify-no-changes --no-restore - - - name: Build - run: dotnet build --configuration $CONFIGURATION --no-restore --warnaserror - - - name: Test - run: dotnet test --configuration $CONFIGURATION --no-build --logger GitHubActions + global_json_file: global.json - name: Pack run: dotnet pack --configuration $CONFIGURATION --no-build --output ./artifacts/packages From 117620449ca2ff29ba71b4b6dc4770afe2aa32eb Mon Sep 17 00:00:00 2001 From: Renato Golia Date: Tue, 15 Sep 2026 23:13:15 +0200 Subject: [PATCH 02/19] Create releases from the publish workflow --- .github/workflows/release.yml | 186 +++++++++++++++++++++++++++++----- 1 file changed, 163 insertions(+), 23 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8e974d8..ef43d31 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,9 +2,38 @@ name: Release on: workflow_dispatch: + inputs: + bump: + description: Version bump + required: true + default: minor + type: choice + options: + - major + - minor + - patch + release_channel: + description: Release channel + required: true + default: stable + type: choice + options: + - stable + - alpha + - beta + - rc + dry_run: + description: Validate the selected release without publishing packages or creating a release + required: false + default: false + type: boolean release: types: [published] +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + permissions: contents: write packages: write @@ -17,35 +46,98 @@ env: jobs: release: - name: Build and Publish + name: Validate, pack, and publish runs-on: ubuntu-latest steps: + - name: Validate manual release source + if: github.event_name == 'workflow_dispatch' && !inputs.dry_run && github.ref != 'refs/heads/master' + run: | + echo "Manual releases must be dispatched from master." + exit 1 + - name: Checkout uses: actions/checkout@v7 with: fetch-depth: 0 - - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + - name: Determine minimum version + if: github.event_name == 'workflow_dispatch' + id: minimum-version + shell: bash + run: | + set -euo pipefail - - name: Restore - run: dotnet restore + minimum_major_minor=$(sed -n 's:.*\([^<]*\).*:\1:p' src/Kralizek.Extensions.Configuration.AWSSecretsManager/Kralizek.Extensions.Configuration.AWSSecretsManager.csproj | head -n 1) - - name: Format Check - run: dotnet format --verify-no-changes --no-restore + if [[ ! "$minimum_major_minor" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then + echo "Could not determine a valid MinVerMinimumMajorMinor from the package project." + exit 1 + fi - - name: Build - run: dotnet build --configuration $CONFIGURATION --no-restore --warnaserror + echo "version=${minimum_major_minor}.0" >> "$GITHUB_OUTPUT" - - name: Test - run: dotnet test --configuration $CONFIGURATION --no-build --logger GitHubActions + - name: Calculate release version + if: github.event_name == 'workflow_dispatch' + id: calculated-version + uses: Kralizek/github-actions/actions/calculate-next-release@v0.2 + with: + bump: ${{ inputs.bump }} + channel: ${{ inputs.release_channel != 'stable' && inputs.release_channel || '' }} + minimum-version: ${{ steps.minimum-version.outputs.version }} + + - name: Validate published release + if: github.event_name == 'release' + id: published-release + uses: Kralizek/github-actions/actions/validate-release@v0.2 + with: + tag: ${{ github.event.release.tag_name }} + prerelease: ${{ github.event.release.prerelease }} + + - name: Build and test + uses: Kralizek/github-actions/actions/dotnet-build@v0.2 + env: + MinVerVersionOverride: ${{ github.event_name == 'workflow_dispatch' && steps.calculated-version.outputs.version || '' }} + with: + checkout: false + global_json_file: global.json - name: Pack + env: + MinVerVersionOverride: ${{ github.event_name == 'workflow_dispatch' && steps.calculated-version.outputs.version || '' }} run: dotnet pack --configuration $CONFIGURATION --no-build --output ./artifacts/packages + - name: Read package version + id: package-version + shell: bash + run: | + set -euo pipefail + + package=$(ls ./artifacts/packages/Kralizek.Extensions.Configuration.AWSSecretsManager.*.nupkg | head -n 1) + nuspec=$(unzip -p "$package" '*.nuspec') + version=$(printf '%s\n' "$nuspec" | sed -n 's:.*\(.*\).*:\1:p' | head -n 1) + + if [ -z "$version" ]; then + echo "Could not determine the packed package version." + exit 1 + fi + + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "tag=v$version" >> "$GITHUB_OUTPUT" + + - name: Validate package version + env: + PACKAGE_VERSION: ${{ steps.package-version.outputs.version }} + EXPECTED_VERSION: ${{ github.event_name == 'release' && steps.published-release.outputs.version || steps.calculated-version.outputs.version }} + shell: bash + run: | + set -euo pipefail + + if [ "$PACKAGE_VERSION" != "$EXPECTED_VERSION" ]; then + echo "Packed package version $PACKAGE_VERSION does not match expected version $EXPECTED_VERSION." + exit 1 + fi + - name: Upload package artifact uses: actions/upload-artifact@v7 with: @@ -55,8 +147,52 @@ jobs: ./artifacts/packages/*.snupkg if-no-files-found: error + - name: Dry-run release summary + if: github.event_name == 'workflow_dispatch' && inputs.dry_run + run: | + echo "Dry run: no packages or GitHub release will be published." + echo "Version bump: ${{ inputs.bump }}" + echo "Release channel: ${{ inputs.release_channel }}" + echo "Package version: ${{ steps.package-version.outputs.version }}" + + - name: Create GitHub release + if: github.event_name == 'workflow_dispatch' && !inputs.dry_run + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.calculated-version.outputs.tag }} + PREVIOUS_TAG: ${{ steps.calculated-version.outputs['previous-tag'] }} + RELEASE_CHANNEL: ${{ inputs.release_channel }} + shell: bash + run: | + set -euo pipefail + + args=( + "$TAG" + --repo "${{ github.repository }}" + --target "$GITHUB_SHA" + --title "$TAG" + --generate-notes + ) + + if [[ -n "$PREVIOUS_TAG" ]]; then + args+=(--notes-start-tag "$PREVIOUS_TAG") + fi + + if [[ "$RELEASE_CHANNEL" != "stable" ]]; then + args+=(--prerelease) + fi + + gh release create "${args[@]}" + + - name: Validate created release tag + if: github.event_name == 'workflow_dispatch' && !inputs.dry_run + uses: Kralizek/github-actions/actions/validate-release@v0.2 + with: + tag: ${{ steps.calculated-version.outputs.tag }} + prerelease: ${{ inputs.release_channel != 'stable' }} + - name: Configure GitHub Packages source - if: github.event_name == 'release' + if: github.event_name == 'release' || !inputs.dry_run run: >- dotnet nuget add source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" @@ -66,7 +202,7 @@ jobs: --store-password-in-clear-text - name: Publish to GitHub Packages - if: github.event_name == 'release' + if: github.event_name == 'release' || !inputs.dry_run run: >- dotnet nuget push "./artifacts/packages/*.nupkg" @@ -75,14 +211,14 @@ jobs: --skip-duplicate - name: NuGet Login - if: github.event_name == 'release' + if: github.event_name == 'release' || !inputs.dry_run id: nuget-login uses: nuget/login@v1 with: user: Kralizek - name: Publish to NuGet.org - if: github.event_name == 'release' + if: github.event_name == 'release' || !inputs.dry_run run: >- dotnet nuget push "./artifacts/packages/*.nupkg" @@ -91,9 +227,13 @@ jobs: --skip-duplicate - name: Add packages to release - if: github.event_name == 'release' - uses: softprops/action-gh-release@v3 - with: - files: | - ./artifacts/packages/*.nupkg - ./artifacts/packages/*.snupkg + if: github.event_name == 'release' || !inputs.dry_run + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || steps.calculated-version.outputs.tag }} + run: >- + gh release upload "$RELEASE_TAG" + ./artifacts/packages/*.nupkg + ./artifacts/packages/*.snupkg + --repo "${{ github.repository }}" + --clobber From b85ef5e491c344c3109fb3d6a280ce6bb16dbb07 Mon Sep 17 00:00:00 2001 From: Renato Golia Date: Tue, 15 Sep 2026 23:34:27 +0200 Subject: [PATCH 03/19] Make manual releases retry-safe --- .github/workflows/release.yml | 39 ++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ef43d31..6d8c65d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -155,7 +155,7 @@ jobs: echo "Release channel: ${{ inputs.release_channel }}" echo "Package version: ${{ steps.package-version.outputs.version }}" - - name: Create GitHub release + - name: Create or reuse draft GitHub release if: github.event_name == 'workflow_dispatch' && !inputs.dry_run env: GH_TOKEN: ${{ github.token }} @@ -166,12 +166,24 @@ jobs: run: | set -euo pipefail + if gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then + is_draft=$(gh release view "$TAG" --repo "${{ github.repository }}" --json isDraft --jq .isDraft) + if [[ "$is_draft" != "true" ]]; then + echo "Release $TAG already exists and is not a draft." >&2 + exit 1 + fi + + echo "Reusing draft release $TAG." + exit 0 + fi + args=( "$TAG" --repo "${{ github.repository }}" --target "$GITHUB_SHA" --title "$TAG" --generate-notes + --draft ) if [[ -n "$PREVIOUS_TAG" ]]; then @@ -184,13 +196,6 @@ jobs: gh release create "${args[@]}" - - name: Validate created release tag - if: github.event_name == 'workflow_dispatch' && !inputs.dry_run - uses: Kralizek/github-actions/actions/validate-release@v0.2 - with: - tag: ${{ steps.calculated-version.outputs.tag }} - prerelease: ${{ inputs.release_channel != 'stable' }} - - name: Configure GitHub Packages source if: github.event_name == 'release' || !inputs.dry_run run: >- @@ -237,3 +242,21 @@ jobs: ./artifacts/packages/*.snupkg --repo "${{ github.repository }}" --clobber + + - name: Publish GitHub release + if: github.event_name == 'workflow_dispatch' && !inputs.dry_run + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.calculated-version.outputs.tag }} + shell: bash + run: | + set -euo pipefail + gh release edit "$TAG" --repo "${{ github.repository }}" --draft=false + git fetch origin "refs/tags/$TAG:refs/tags/$TAG" + + - name: Validate created release tag + if: github.event_name == 'workflow_dispatch' && !inputs.dry_run + uses: Kralizek/github-actions/actions/validate-release@v0.2 + with: + tag: ${{ steps.calculated-version.outputs.tag }} + prerelease: ${{ inputs.release_channel != 'stable' }} From 2bf002ca85a95f531b87e0469670c747891bc74a Mon Sep 17 00:00:00 2001 From: Renato Golia Date: Wed, 16 Sep 2026 00:11:16 +0200 Subject: [PATCH 04/19] Resume existing draft release before version calculation --- .github/workflows/release.yml | 96 ++++++++++++++++++++++++++++------- 1 file changed, 79 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6d8c65d..75ae611 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,8 +61,49 @@ jobs: with: fetch-depth: 0 + - name: Detect resumable draft release + if: github.event_name == 'workflow_dispatch' && !inputs.dry_run + id: resumable-release + env: + GH_TOKEN: ${{ github.token }} + RELEASE_CHANNEL: ${{ inputs.release_channel }} + shell: bash + run: | + set -euo pipefail + + releases=$(gh api --paginate "repos/${{ github.repository }}/releases?per_page=100") + + if [[ "$RELEASE_CHANNEL" == "stable" ]]; then + pattern='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' + else + pattern="^v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)-${RELEASE_CHANNEL}\\.[0-9]+$" + fi + + mapfile -t matching_tags < <( + jq -r \ + --arg sha "$GITHUB_SHA" \ + --arg pattern "$pattern" \ + '.[] | select(.draft == true and .target_commitish == $sha) | .tag_name | select(test($pattern))' \ + <<< "$releases" + ) + + if (( ${#matching_tags[@]} > 1 )); then + echo "Multiple matching draft releases target $GITHUB_SHA: ${matching_tags[*]}" >&2 + exit 1 + fi + + if (( ${#matching_tags[@]} == 1 )); then + tag="${matching_tags[0]}" + echo "Resuming draft release $tag." + echo "resume=true" >> "$GITHUB_OUTPUT" + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "version=${tag#v}" >> "$GITHUB_OUTPUT" + else + echo "resume=false" >> "$GITHUB_OUTPUT" + fi + - name: Determine minimum version - if: github.event_name == 'workflow_dispatch' + if: github.event_name == 'workflow_dispatch' && steps.resumable-release.outputs.resume != 'true' id: minimum-version shell: bash run: | @@ -78,7 +119,7 @@ jobs: echo "version=${minimum_major_minor}.0" >> "$GITHUB_OUTPUT" - name: Calculate release version - if: github.event_name == 'workflow_dispatch' + if: github.event_name == 'workflow_dispatch' && steps.resumable-release.outputs.resume != 'true' id: calculated-version uses: Kralizek/github-actions/actions/calculate-next-release@v0.2 with: @@ -86,6 +127,32 @@ jobs: channel: ${{ inputs.release_channel != 'stable' && inputs.release_channel || '' }} minimum-version: ${{ steps.minimum-version.outputs.version }} + - name: Resolve manual release version + if: github.event_name == 'workflow_dispatch' + id: manual-release + env: + RESUME: ${{ steps.resumable-release.outputs.resume }} + RESUMED_VERSION: ${{ steps.resumable-release.outputs.version }} + RESUMED_TAG: ${{ steps.resumable-release.outputs.tag }} + CALCULATED_VERSION: ${{ steps.calculated-version.outputs.version }} + CALCULATED_TAG: ${{ steps.calculated-version.outputs.tag }} + CALCULATED_PREVIOUS_TAG: ${{ steps.calculated-version.outputs['previous-tag'] }} + shell: bash + run: | + set -euo pipefail + + if [[ "$RESUME" == "true" ]]; then + echo "resume=true" >> "$GITHUB_OUTPUT" + echo "version=$RESUMED_VERSION" >> "$GITHUB_OUTPUT" + echo "tag=$RESUMED_TAG" >> "$GITHUB_OUTPUT" + echo "previous-tag=" >> "$GITHUB_OUTPUT" + else + echo "resume=false" >> "$GITHUB_OUTPUT" + echo "version=$CALCULATED_VERSION" >> "$GITHUB_OUTPUT" + echo "tag=$CALCULATED_TAG" >> "$GITHUB_OUTPUT" + echo "previous-tag=$CALCULATED_PREVIOUS_TAG" >> "$GITHUB_OUTPUT" + fi + - name: Validate published release if: github.event_name == 'release' id: published-release @@ -97,14 +164,14 @@ jobs: - name: Build and test uses: Kralizek/github-actions/actions/dotnet-build@v0.2 env: - MinVerVersionOverride: ${{ github.event_name == 'workflow_dispatch' && steps.calculated-version.outputs.version || '' }} + MinVerVersionOverride: ${{ github.event_name == 'workflow_dispatch' && steps.manual-release.outputs.version || '' }} with: checkout: false global_json_file: global.json - name: Pack env: - MinVerVersionOverride: ${{ github.event_name == 'workflow_dispatch' && steps.calculated-version.outputs.version || '' }} + MinVerVersionOverride: ${{ github.event_name == 'workflow_dispatch' && steps.manual-release.outputs.version || '' }} run: dotnet pack --configuration $CONFIGURATION --no-build --output ./artifacts/packages - name: Read package version @@ -128,7 +195,7 @@ jobs: - name: Validate package version env: PACKAGE_VERSION: ${{ steps.package-version.outputs.version }} - EXPECTED_VERSION: ${{ github.event_name == 'release' && steps.published-release.outputs.version || steps.calculated-version.outputs.version }} + EXPECTED_VERSION: ${{ github.event_name == 'release' && steps.published-release.outputs.version || steps.manual-release.outputs.version }} shell: bash run: | set -euo pipefail @@ -159,20 +226,15 @@ jobs: if: github.event_name == 'workflow_dispatch' && !inputs.dry_run env: GH_TOKEN: ${{ github.token }} - TAG: ${{ steps.calculated-version.outputs.tag }} - PREVIOUS_TAG: ${{ steps.calculated-version.outputs['previous-tag'] }} + TAG: ${{ steps.manual-release.outputs.tag }} + PREVIOUS_TAG: ${{ steps.manual-release.outputs['previous-tag'] }} RELEASE_CHANNEL: ${{ inputs.release_channel }} + RESUME: ${{ steps.manual-release.outputs.resume }} shell: bash run: | set -euo pipefail - if gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then - is_draft=$(gh release view "$TAG" --repo "${{ github.repository }}" --json isDraft --jq .isDraft) - if [[ "$is_draft" != "true" ]]; then - echo "Release $TAG already exists and is not a draft." >&2 - exit 1 - fi - + if [[ "$RESUME" == "true" ]]; then echo "Reusing draft release $TAG." exit 0 fi @@ -235,7 +297,7 @@ jobs: if: github.event_name == 'release' || !inputs.dry_run env: GH_TOKEN: ${{ github.token }} - RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || steps.calculated-version.outputs.tag }} + RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || steps.manual-release.outputs.tag }} run: >- gh release upload "$RELEASE_TAG" ./artifacts/packages/*.nupkg @@ -247,7 +309,7 @@ jobs: if: github.event_name == 'workflow_dispatch' && !inputs.dry_run env: GH_TOKEN: ${{ github.token }} - TAG: ${{ steps.calculated-version.outputs.tag }} + TAG: ${{ steps.manual-release.outputs.tag }} shell: bash run: | set -euo pipefail @@ -258,5 +320,5 @@ jobs: if: github.event_name == 'workflow_dispatch' && !inputs.dry_run uses: Kralizek/github-actions/actions/validate-release@v0.2 with: - tag: ${{ steps.calculated-version.outputs.tag }} + tag: ${{ steps.manual-release.outputs.tag }} prerelease: ${{ inputs.release_channel != 'stable' }} From 006c26da247ba78159b410289e80cf2827500ac8 Mon Sep 17 00:00:00 2001 From: Renato Golia Date: Wed, 16 Sep 2026 00:21:17 +0200 Subject: [PATCH 05/19] Avoid duplicate publication for workflow-created releases --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 75ae611..c6cbded 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,6 +47,7 @@ env: jobs: release: name: Validate, pack, and publish + if: github.event_name != 'release' || github.event.release.author.login != 'github-actions[bot]' runs-on: ubuntu-latest steps: From e046f825ec92dbb3c40ca3542edec5cd11ac7c3e Mon Sep 17 00:00:00 2001 From: Renato Golia Date: Wed, 16 Sep 2026 00:24:13 +0200 Subject: [PATCH 06/19] Extract resumable release detection script --- scripts/detect-resumable-release.sh | 39 +++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 scripts/detect-resumable-release.sh diff --git a/scripts/detect-resumable-release.sh b/scripts/detect-resumable-release.sh new file mode 100644 index 0000000..30c2e83 --- /dev/null +++ b/scripts/detect-resumable-release.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${GH_TOKEN:?GH_TOKEN must be set}" +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY must be set}" +: "${GITHUB_SHA:?GITHUB_SHA must be set}" +: "${RELEASE_CHANNEL:?RELEASE_CHANNEL must be set}" +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT must be set}" + +releases=$(gh api --paginate "repos/${GITHUB_REPOSITORY}/releases?per_page=100") + +if [[ "$RELEASE_CHANNEL" == "stable" ]]; then + pattern='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' +else + pattern="^v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)-${RELEASE_CHANNEL}\\.[0-9]+$" +fi + +mapfile -t matching_tags < <( + jq -r \ + --arg sha "$GITHUB_SHA" \ + --arg pattern "$pattern" \ + '.[] | select(.draft == true and .target_commitish == $sha) | .tag_name | select(test($pattern))' \ + <<< "$releases" +) + +if (( ${#matching_tags[@]} > 1 )); then + echo "Multiple matching draft releases target $GITHUB_SHA: ${matching_tags[*]}" >&2 + exit 1 +fi + +if (( ${#matching_tags[@]} == 1 )); then + tag="${matching_tags[0]}" + echo "Resuming draft release $tag." + echo "resume=true" >> "$GITHUB_OUTPUT" + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "version=${tag#v}" >> "$GITHUB_OUTPUT" +else + echo "resume=false" >> "$GITHUB_OUTPUT" +fi From a6b4e418ee31c337a9bb22b18f819c069c6ce708 Mon Sep 17 00:00:00 2001 From: Renato Golia Date: Wed, 16 Sep 2026 00:24:53 +0200 Subject: [PATCH 07/19] Move draft release detection into script --- .github/workflows/release.yml | 35 +---------------------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c6cbded..05ef23d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,40 +68,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} RELEASE_CHANNEL: ${{ inputs.release_channel }} - shell: bash - run: | - set -euo pipefail - - releases=$(gh api --paginate "repos/${{ github.repository }}/releases?per_page=100") - - if [[ "$RELEASE_CHANNEL" == "stable" ]]; then - pattern='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' - else - pattern="^v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)-${RELEASE_CHANNEL}\\.[0-9]+$" - fi - - mapfile -t matching_tags < <( - jq -r \ - --arg sha "$GITHUB_SHA" \ - --arg pattern "$pattern" \ - '.[] | select(.draft == true and .target_commitish == $sha) | .tag_name | select(test($pattern))' \ - <<< "$releases" - ) - - if (( ${#matching_tags[@]} > 1 )); then - echo "Multiple matching draft releases target $GITHUB_SHA: ${matching_tags[*]}" >&2 - exit 1 - fi - - if (( ${#matching_tags[@]} == 1 )); then - tag="${matching_tags[0]}" - echo "Resuming draft release $tag." - echo "resume=true" >> "$GITHUB_OUTPUT" - echo "tag=$tag" >> "$GITHUB_OUTPUT" - echo "version=${tag#v}" >> "$GITHUB_OUTPUT" - else - echo "resume=false" >> "$GITHUB_OUTPUT" - fi + run: bash scripts/detect-resumable-release.sh - name: Determine minimum version if: github.event_name == 'workflow_dispatch' && steps.resumable-release.outputs.resume != 'true' From be57ef0212079b88725205fb1fb330e8ec36a0e4 Mon Sep 17 00:00:00 2001 From: Renato Golia Date: Wed, 16 Sep 2026 00:25:53 +0200 Subject: [PATCH 08/19] Move release script under workflows --- .../scripts/detect-resumable-release.sh | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/scripts/detect-resumable-release.sh diff --git a/.github/workflows/scripts/detect-resumable-release.sh b/.github/workflows/scripts/detect-resumable-release.sh new file mode 100644 index 0000000..30c2e83 --- /dev/null +++ b/.github/workflows/scripts/detect-resumable-release.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${GH_TOKEN:?GH_TOKEN must be set}" +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY must be set}" +: "${GITHUB_SHA:?GITHUB_SHA must be set}" +: "${RELEASE_CHANNEL:?RELEASE_CHANNEL must be set}" +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT must be set}" + +releases=$(gh api --paginate "repos/${GITHUB_REPOSITORY}/releases?per_page=100") + +if [[ "$RELEASE_CHANNEL" == "stable" ]]; then + pattern='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' +else + pattern="^v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)-${RELEASE_CHANNEL}\\.[0-9]+$" +fi + +mapfile -t matching_tags < <( + jq -r \ + --arg sha "$GITHUB_SHA" \ + --arg pattern "$pattern" \ + '.[] | select(.draft == true and .target_commitish == $sha) | .tag_name | select(test($pattern))' \ + <<< "$releases" +) + +if (( ${#matching_tags[@]} > 1 )); then + echo "Multiple matching draft releases target $GITHUB_SHA: ${matching_tags[*]}" >&2 + exit 1 +fi + +if (( ${#matching_tags[@]} == 1 )); then + tag="${matching_tags[0]}" + echo "Resuming draft release $tag." + echo "resume=true" >> "$GITHUB_OUTPUT" + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "version=${tag#v}" >> "$GITHUB_OUTPUT" +else + echo "resume=false" >> "$GITHUB_OUTPUT" +fi From 9d0fe7e2c36f470d338e67042db53d0add5ff50a Mon Sep 17 00:00:00 2001 From: Renato Golia Date: Wed, 16 Sep 2026 00:26:21 +0200 Subject: [PATCH 09/19] Use workflow-local release script --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 05ef23d..cd2d164 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,7 +68,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} RELEASE_CHANNEL: ${{ inputs.release_channel }} - run: bash scripts/detect-resumable-release.sh + run: bash .github/workflows/scripts/detect-resumable-release.sh - name: Determine minimum version if: github.event_name == 'workflow_dispatch' && steps.resumable-release.outputs.resume != 'true' From bbb92cd5de351055c349cc9b07f09ffd5e6f2fd5 Mon Sep 17 00:00:00 2001 From: Renato Golia Date: Wed, 16 Sep 2026 00:26:26 +0200 Subject: [PATCH 10/19] Remove old release script location --- scripts/detect-resumable-release.sh | 39 ----------------------------- 1 file changed, 39 deletions(-) delete mode 100644 scripts/detect-resumable-release.sh diff --git a/scripts/detect-resumable-release.sh b/scripts/detect-resumable-release.sh deleted file mode 100644 index 30c2e83..0000000 --- a/scripts/detect-resumable-release.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -: "${GH_TOKEN:?GH_TOKEN must be set}" -: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY must be set}" -: "${GITHUB_SHA:?GITHUB_SHA must be set}" -: "${RELEASE_CHANNEL:?RELEASE_CHANNEL must be set}" -: "${GITHUB_OUTPUT:?GITHUB_OUTPUT must be set}" - -releases=$(gh api --paginate "repos/${GITHUB_REPOSITORY}/releases?per_page=100") - -if [[ "$RELEASE_CHANNEL" == "stable" ]]; then - pattern='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' -else - pattern="^v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)-${RELEASE_CHANNEL}\\.[0-9]+$" -fi - -mapfile -t matching_tags < <( - jq -r \ - --arg sha "$GITHUB_SHA" \ - --arg pattern "$pattern" \ - '.[] | select(.draft == true and .target_commitish == $sha) | .tag_name | select(test($pattern))' \ - <<< "$releases" -) - -if (( ${#matching_tags[@]} > 1 )); then - echo "Multiple matching draft releases target $GITHUB_SHA: ${matching_tags[*]}" >&2 - exit 1 -fi - -if (( ${#matching_tags[@]} == 1 )); then - tag="${matching_tags[0]}" - echo "Resuming draft release $tag." - echo "resume=true" >> "$GITHUB_OUTPUT" - echo "tag=$tag" >> "$GITHUB_OUTPUT" - echo "version=${tag#v}" >> "$GITHUB_OUTPUT" -else - echo "resume=false" >> "$GITHUB_OUTPUT" -fi From 0788bdce86d4bdfcbd52ca4e01239d9a6df7691f Mon Sep 17 00:00:00 2001 From: Renato Golia Date: Wed, 16 Sep 2026 00:28:47 +0200 Subject: [PATCH 11/19] Extract draft release creation script --- .../scripts/create-or-reuse-draft-release.sh | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/scripts/create-or-reuse-draft-release.sh diff --git a/.github/workflows/scripts/create-or-reuse-draft-release.sh b/.github/workflows/scripts/create-or-reuse-draft-release.sh new file mode 100644 index 0000000..da9445d --- /dev/null +++ b/.github/workflows/scripts/create-or-reuse-draft-release.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${GH_TOKEN:?GH_TOKEN must be set}" +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY must be set}" +: "${GITHUB_SHA:?GITHUB_SHA must be set}" +: "${TAG:?TAG must be set}" +: "${RELEASE_CHANNEL:?RELEASE_CHANNEL must be set}" +: "${RESUME:?RESUME must be set}" + +if [[ "$RESUME" == "true" ]]; then + echo "Reusing draft release $TAG." + exit 0 +fi + +args=( + "$TAG" + --repo "$GITHUB_REPOSITORY" + --target "$GITHUB_SHA" + --title "$TAG" + --generate-notes + --draft +) + +if [[ -n "${PREVIOUS_TAG:-}" ]]; then + args+=(--notes-start-tag "$PREVIOUS_TAG") +fi + +if [[ "$RELEASE_CHANNEL" != "stable" ]]; then + args+=(--prerelease) +fi + +gh release create "${args[@]}" From 1ef0dc4e4893dba3c467c2d78f865f37641eb686 Mon Sep 17 00:00:00 2001 From: Renato Golia Date: Wed, 16 Sep 2026 00:29:10 +0200 Subject: [PATCH 12/19] Use script for draft release creation --- .github/workflows/release.yml | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cd2d164..8f1ba91 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -198,33 +198,7 @@ jobs: PREVIOUS_TAG: ${{ steps.manual-release.outputs['previous-tag'] }} RELEASE_CHANNEL: ${{ inputs.release_channel }} RESUME: ${{ steps.manual-release.outputs.resume }} - shell: bash - run: | - set -euo pipefail - - if [[ "$RESUME" == "true" ]]; then - echo "Reusing draft release $TAG." - exit 0 - fi - - args=( - "$TAG" - --repo "${{ github.repository }}" - --target "$GITHUB_SHA" - --title "$TAG" - --generate-notes - --draft - ) - - if [[ -n "$PREVIOUS_TAG" ]]; then - args+=(--notes-start-tag "$PREVIOUS_TAG") - fi - - if [[ "$RELEASE_CHANNEL" != "stable" ]]; then - args+=(--prerelease) - fi - - gh release create "${args[@]}" + run: bash .github/workflows/scripts/create-or-reuse-draft-release.sh - name: Configure GitHub Packages source if: github.event_name == 'release' || !inputs.dry_run From ac1bb1b2a1615c2140148b87101ce6e472b8d6c2 Mon Sep 17 00:00:00 2001 From: Renato Golia Date: Wed, 16 Sep 2026 00:36:03 +0200 Subject: [PATCH 13/19] Make release resume detection idempotent --- .../scripts/detect-resumable-release.sh | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/.github/workflows/scripts/detect-resumable-release.sh b/.github/workflows/scripts/detect-resumable-release.sh index 30c2e83..e4c697c 100644 --- a/.github/workflows/scripts/detect-resumable-release.sh +++ b/.github/workflows/scripts/detect-resumable-release.sh @@ -15,25 +15,41 @@ else pattern="^v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)-${RELEASE_CHANNEL}\\.[0-9]+$" fi -mapfile -t matching_tags < <( - jq -r \ +mapfile -t matching_releases < <( + jq -rc \ --arg sha "$GITHUB_SHA" \ --arg pattern "$pattern" \ - '.[] | select(.draft == true and .target_commitish == $sha) | .tag_name | select(test($pattern))' \ + '.[] + | select(.target_commitish == $sha) + | select(.tag_name | test($pattern)) + | {tag: .tag_name, draft: .draft}' \ <<< "$releases" ) -if (( ${#matching_tags[@]} > 1 )); then - echo "Multiple matching draft releases target $GITHUB_SHA: ${matching_tags[*]}" >&2 +if (( ${#matching_releases[@]} > 1 )); then + tags=$(printf '%s\n' "${matching_releases[@]}" | jq -r .tag | paste -sd ' ' -) + echo "Multiple matching releases target $GITHUB_SHA: $tags" >&2 exit 1 fi -if (( ${#matching_tags[@]} == 1 )); then - tag="${matching_tags[0]}" - echo "Resuming draft release $tag." +if (( ${#matching_releases[@]} == 1 )); then + release="${matching_releases[0]}" + tag=$(jq -r .tag <<< "$release") + draft=$(jq -r .draft <<< "$release") + + if [[ "$draft" == "true" ]]; then + echo "Resuming draft release $tag." + published=false + else + echo "Resuming published release $tag." + published=true + fi + echo "resume=true" >> "$GITHUB_OUTPUT" + echo "published=$published" >> "$GITHUB_OUTPUT" echo "tag=$tag" >> "$GITHUB_OUTPUT" echo "version=${tag#v}" >> "$GITHUB_OUTPUT" else echo "resume=false" >> "$GITHUB_OUTPUT" + echo "published=false" >> "$GITHUB_OUTPUT" fi From b90e26c7f6cedd852b43e00d29ecfea9f4809a07 Mon Sep 17 00:00:00 2001 From: Renato Golia Date: Wed, 16 Sep 2026 00:36:27 +0200 Subject: [PATCH 14/19] Fix dry-run and published release retries --- .github/workflows/release.yml | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8f1ba91..173dc83 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,7 +62,7 @@ jobs: with: fetch-depth: 0 - - name: Detect resumable draft release + - name: Detect resumable release if: github.event_name == 'workflow_dispatch' && !inputs.dry_run id: resumable-release env: @@ -71,7 +71,7 @@ jobs: run: bash .github/workflows/scripts/detect-resumable-release.sh - name: Determine minimum version - if: github.event_name == 'workflow_dispatch' && steps.resumable-release.outputs.resume != 'true' + if: github.event_name == 'workflow_dispatch' && (inputs.dry_run || steps.resumable-release.outputs.resume != 'true') id: minimum-version shell: bash run: | @@ -87,7 +87,7 @@ jobs: echo "version=${minimum_major_minor}.0" >> "$GITHUB_OUTPUT" - name: Calculate release version - if: github.event_name == 'workflow_dispatch' && steps.resumable-release.outputs.resume != 'true' + if: github.event_name == 'workflow_dispatch' && (inputs.dry_run || steps.resumable-release.outputs.resume != 'true') id: calculated-version uses: Kralizek/github-actions/actions/calculate-next-release@v0.2 with: @@ -100,6 +100,7 @@ jobs: id: manual-release env: RESUME: ${{ steps.resumable-release.outputs.resume }} + RESUMED_PUBLISHED: ${{ steps.resumable-release.outputs.published }} RESUMED_VERSION: ${{ steps.resumable-release.outputs.version }} RESUMED_TAG: ${{ steps.resumable-release.outputs.tag }} CALCULATED_VERSION: ${{ steps.calculated-version.outputs.version }} @@ -111,11 +112,13 @@ jobs: if [[ "$RESUME" == "true" ]]; then echo "resume=true" >> "$GITHUB_OUTPUT" + echo "published=$RESUMED_PUBLISHED" >> "$GITHUB_OUTPUT" echo "version=$RESUMED_VERSION" >> "$GITHUB_OUTPUT" echo "tag=$RESUMED_TAG" >> "$GITHUB_OUTPUT" echo "previous-tag=" >> "$GITHUB_OUTPUT" else echo "resume=false" >> "$GITHUB_OUTPUT" + echo "published=false" >> "$GITHUB_OUTPUT" echo "version=$CALCULATED_VERSION" >> "$GITHUB_OUTPUT" echo "tag=$CALCULATED_TAG" >> "$GITHUB_OUTPUT" echo "previous-tag=$CALCULATED_PREVIOUS_TAG" >> "$GITHUB_OUTPUT" @@ -191,7 +194,7 @@ jobs: echo "Package version: ${{ steps.package-version.outputs.version }}" - name: Create or reuse draft GitHub release - if: github.event_name == 'workflow_dispatch' && !inputs.dry_run + if: github.event_name == 'workflow_dispatch' && !inputs.dry_run && steps.manual-release.outputs.published != 'true' env: GH_TOKEN: ${{ github.token }} TAG: ${{ steps.manual-release.outputs.tag }} @@ -201,7 +204,7 @@ jobs: run: bash .github/workflows/scripts/create-or-reuse-draft-release.sh - name: Configure GitHub Packages source - if: github.event_name == 'release' || !inputs.dry_run + if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run && steps.manual-release.outputs.published != 'true') run: >- dotnet nuget add source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" @@ -211,7 +214,7 @@ jobs: --store-password-in-clear-text - name: Publish to GitHub Packages - if: github.event_name == 'release' || !inputs.dry_run + if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run && steps.manual-release.outputs.published != 'true') run: >- dotnet nuget push "./artifacts/packages/*.nupkg" @@ -220,14 +223,14 @@ jobs: --skip-duplicate - name: NuGet Login - if: github.event_name == 'release' || !inputs.dry_run + if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run && steps.manual-release.outputs.published != 'true') id: nuget-login uses: nuget/login@v1 with: user: Kralizek - name: Publish to NuGet.org - if: github.event_name == 'release' || !inputs.dry_run + if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run && steps.manual-release.outputs.published != 'true') run: >- dotnet nuget push "./artifacts/packages/*.nupkg" @@ -236,7 +239,7 @@ jobs: --skip-duplicate - name: Add packages to release - if: github.event_name == 'release' || !inputs.dry_run + if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run && steps.manual-release.outputs.published != 'true') env: GH_TOKEN: ${{ github.token }} RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || steps.manual-release.outputs.tag }} @@ -248,15 +251,17 @@ jobs: --clobber - name: Publish GitHub release - if: github.event_name == 'workflow_dispatch' && !inputs.dry_run + if: github.event_name == 'workflow_dispatch' && !inputs.dry_run && steps.manual-release.outputs.published != 'true' env: GH_TOKEN: ${{ github.token }} TAG: ${{ steps.manual-release.outputs.tag }} - shell: bash - run: | - set -euo pipefail - gh release edit "$TAG" --repo "${{ github.repository }}" --draft=false - git fetch origin "refs/tags/$TAG:refs/tags/$TAG" + run: gh release edit "$TAG" --repo "${{ github.repository }}" --draft=false + + - name: Fetch manual release tag + if: github.event_name == 'workflow_dispatch' && !inputs.dry_run + env: + TAG: ${{ steps.manual-release.outputs.tag }} + run: git fetch origin "refs/tags/$TAG:refs/tags/$TAG" - name: Validate created release tag if: github.event_name == 'workflow_dispatch' && !inputs.dry_run From 4025b5426ead77be7b68be9529d57dec9b15644a Mon Sep 17 00:00:00 2001 From: Renato Golia Date: Wed, 16 Sep 2026 01:01:00 +0200 Subject: [PATCH 15/19] Make draft release recovery independent of current HEAD --- .../scripts/detect-resumable-release.sh | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/.github/workflows/scripts/detect-resumable-release.sh b/.github/workflows/scripts/detect-resumable-release.sh index e4c697c..b477dc2 100644 --- a/.github/workflows/scripts/detect-resumable-release.sh +++ b/.github/workflows/scripts/detect-resumable-release.sh @@ -3,7 +3,6 @@ set -euo pipefail : "${GH_TOKEN:?GH_TOKEN must be set}" : "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY must be set}" -: "${GITHUB_SHA:?GITHUB_SHA must be set}" : "${RELEASE_CHANNEL:?RELEASE_CHANNEL must be set}" : "${GITHUB_OUTPUT:?GITHUB_OUTPUT must be set}" @@ -17,39 +16,31 @@ fi mapfile -t matching_releases < <( jq -rc \ - --arg sha "$GITHUB_SHA" \ --arg pattern "$pattern" \ '.[] - | select(.target_commitish == $sha) + | select(.draft == true) + | select(.author.login == "github-actions[bot]") | select(.tag_name | test($pattern)) - | {tag: .tag_name, draft: .draft}' \ + | {tag: .tag_name, target: .target_commitish}' \ <<< "$releases" ) if (( ${#matching_releases[@]} > 1 )); then tags=$(printf '%s\n' "${matching_releases[@]}" | jq -r .tag | paste -sd ' ' -) - echo "Multiple matching releases target $GITHUB_SHA: $tags" >&2 + echo "Multiple resumable draft releases match channel $RELEASE_CHANNEL: $tags" >&2 exit 1 fi if (( ${#matching_releases[@]} == 1 )); then release="${matching_releases[0]}" tag=$(jq -r .tag <<< "$release") - draft=$(jq -r .draft <<< "$release") - - if [[ "$draft" == "true" ]]; then - echo "Resuming draft release $tag." - published=false - else - echo "Resuming published release $tag." - published=true - fi + target=$(jq -r .target <<< "$release") + echo "Resuming draft release $tag at $target." echo "resume=true" >> "$GITHUB_OUTPUT" - echo "published=$published" >> "$GITHUB_OUTPUT" echo "tag=$tag" >> "$GITHUB_OUTPUT" echo "version=${tag#v}" >> "$GITHUB_OUTPUT" + echo "target=$target" >> "$GITHUB_OUTPUT" else echo "resume=false" >> "$GITHUB_OUTPUT" - echo "published=false" >> "$GITHUB_OUTPUT" fi From a2d712cacfc44fe51b54d5fd996d4355ff67a01c Mon Sep 17 00:00:00 2001 From: Renato Golia Date: Wed, 16 Sep 2026 01:01:26 +0200 Subject: [PATCH 16/19] Make manual release recovery resilient to master advancing --- .github/workflows/release.yml | 37 +++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 173dc83..a55f730 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -70,6 +70,12 @@ jobs: RELEASE_CHANNEL: ${{ inputs.release_channel }} run: bash .github/workflows/scripts/detect-resumable-release.sh + - name: Checkout resumable release target + if: github.event_name == 'workflow_dispatch' && !inputs.dry_run && steps.resumable-release.outputs.resume == 'true' + env: + TARGET: ${{ steps.resumable-release.outputs.target }} + run: git checkout --detach "$TARGET" + - name: Determine minimum version if: github.event_name == 'workflow_dispatch' && (inputs.dry_run || steps.resumable-release.outputs.resume != 'true') id: minimum-version @@ -100,7 +106,6 @@ jobs: id: manual-release env: RESUME: ${{ steps.resumable-release.outputs.resume }} - RESUMED_PUBLISHED: ${{ steps.resumable-release.outputs.published }} RESUMED_VERSION: ${{ steps.resumable-release.outputs.version }} RESUMED_TAG: ${{ steps.resumable-release.outputs.tag }} CALCULATED_VERSION: ${{ steps.calculated-version.outputs.version }} @@ -112,13 +117,11 @@ jobs: if [[ "$RESUME" == "true" ]]; then echo "resume=true" >> "$GITHUB_OUTPUT" - echo "published=$RESUMED_PUBLISHED" >> "$GITHUB_OUTPUT" echo "version=$RESUMED_VERSION" >> "$GITHUB_OUTPUT" echo "tag=$RESUMED_TAG" >> "$GITHUB_OUTPUT" echo "previous-tag=" >> "$GITHUB_OUTPUT" else echo "resume=false" >> "$GITHUB_OUTPUT" - echo "published=false" >> "$GITHUB_OUTPUT" echo "version=$CALCULATED_VERSION" >> "$GITHUB_OUTPUT" echo "tag=$CALCULATED_TAG" >> "$GITHUB_OUTPUT" echo "previous-tag=$CALCULATED_PREVIOUS_TAG" >> "$GITHUB_OUTPUT" @@ -194,7 +197,7 @@ jobs: echo "Package version: ${{ steps.package-version.outputs.version }}" - name: Create or reuse draft GitHub release - if: github.event_name == 'workflow_dispatch' && !inputs.dry_run && steps.manual-release.outputs.published != 'true' + if: github.event_name == 'workflow_dispatch' && !inputs.dry_run env: GH_TOKEN: ${{ github.token }} TAG: ${{ steps.manual-release.outputs.tag }} @@ -204,7 +207,7 @@ jobs: run: bash .github/workflows/scripts/create-or-reuse-draft-release.sh - name: Configure GitHub Packages source - if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run && steps.manual-release.outputs.published != 'true') + if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run) run: >- dotnet nuget add source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" @@ -214,7 +217,7 @@ jobs: --store-password-in-clear-text - name: Publish to GitHub Packages - if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run && steps.manual-release.outputs.published != 'true') + if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run) run: >- dotnet nuget push "./artifacts/packages/*.nupkg" @@ -223,14 +226,14 @@ jobs: --skip-duplicate - name: NuGet Login - if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run && steps.manual-release.outputs.published != 'true') + if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run) id: nuget-login uses: nuget/login@v1 with: user: Kralizek - name: Publish to NuGet.org - if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run && steps.manual-release.outputs.published != 'true') + if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run) run: >- dotnet nuget push "./artifacts/packages/*.nupkg" @@ -239,7 +242,7 @@ jobs: --skip-duplicate - name: Add packages to release - if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run && steps.manual-release.outputs.published != 'true') + if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run) env: GH_TOKEN: ${{ github.token }} RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || steps.manual-release.outputs.tag }} @@ -250,22 +253,22 @@ jobs: --repo "${{ github.repository }}" --clobber - - name: Publish GitHub release - if: github.event_name == 'workflow_dispatch' && !inputs.dry_run && steps.manual-release.outputs.published != 'true' - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ steps.manual-release.outputs.tag }} - run: gh release edit "$TAG" --repo "${{ github.repository }}" --draft=false - - name: Fetch manual release tag if: github.event_name == 'workflow_dispatch' && !inputs.dry_run env: TAG: ${{ steps.manual-release.outputs.tag }} run: git fetch origin "refs/tags/$TAG:refs/tags/$TAG" - - name: Validate created release tag + - name: Validate manual release tag if: github.event_name == 'workflow_dispatch' && !inputs.dry_run uses: Kralizek/github-actions/actions/validate-release@v0.2 with: tag: ${{ steps.manual-release.outputs.tag }} prerelease: ${{ inputs.release_channel != 'stable' }} + + - name: Publish GitHub release + if: github.event_name == 'workflow_dispatch' && !inputs.dry_run + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.manual-release.outputs.tag }} + run: gh release edit "$TAG" --repo "${{ github.repository }}" --draft=false From 517c09c583605ed8183ef26d537280fc4f68e822 Mon Sep 17 00:00:00 2001 From: Renato Golia Date: Wed, 16 Sep 2026 01:14:56 +0200 Subject: [PATCH 17/19] Scope resumable drafts to current commit --- .github/workflows/scripts/detect-resumable-release.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/scripts/detect-resumable-release.sh b/.github/workflows/scripts/detect-resumable-release.sh index b477dc2..f6d6a3b 100644 --- a/.github/workflows/scripts/detect-resumable-release.sh +++ b/.github/workflows/scripts/detect-resumable-release.sh @@ -3,6 +3,7 @@ set -euo pipefail : "${GH_TOKEN:?GH_TOKEN must be set}" : "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY must be set}" +: "${GITHUB_SHA:?GITHUB_SHA must be set}" : "${RELEASE_CHANNEL:?RELEASE_CHANNEL must be set}" : "${GITHUB_OUTPUT:?GITHUB_OUTPUT must be set}" @@ -17,9 +18,11 @@ fi mapfile -t matching_releases < <( jq -rc \ --arg pattern "$pattern" \ + --arg sha "$GITHUB_SHA" \ '.[] | select(.draft == true) | select(.author.login == "github-actions[bot]") + | select(.target_commitish == $sha) | select(.tag_name | test($pattern)) | {tag: .tag_name, target: .target_commitish}' \ <<< "$releases" From 624f513cedf57326e004d56b21d0935dc5efb0e8 Mon Sep 17 00:00:00 2001 From: Renato Golia Date: Thu, 17 Sep 2026 01:25:13 +0200 Subject: [PATCH 18/19] Update shared actions to v0.3 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa5d39b..91e90c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Build and test - uses: Kralizek/github-actions/actions/dotnet-build@v0.2 + uses: Kralizek/github-actions/actions/dotnet-build@v0.3 with: global_json_file: global.json From ccf146cb0e5b4715de4e9b0089669c5d9c7f5f0f Mon Sep 17 00:00:00 2001 From: Renato Golia Date: Thu, 17 Sep 2026 01:25:35 +0200 Subject: [PATCH 19/19] Update shared actions to v0.3 --- .github/workflows/release.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a55f730..fab1370 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -95,7 +95,7 @@ jobs: - name: Calculate release version if: github.event_name == 'workflow_dispatch' && (inputs.dry_run || steps.resumable-release.outputs.resume != 'true') id: calculated-version - uses: Kralizek/github-actions/actions/calculate-next-release@v0.2 + uses: Kralizek/github-actions/actions/calculate-next-version@v0.3 with: bump: ${{ inputs.bump }} channel: ${{ inputs.release_channel != 'stable' && inputs.release_channel || '' }} @@ -130,13 +130,13 @@ jobs: - name: Validate published release if: github.event_name == 'release' id: published-release - uses: Kralizek/github-actions/actions/validate-release@v0.2 + uses: Kralizek/github-actions/actions/validate-release@v0.3 with: tag: ${{ github.event.release.tag_name }} prerelease: ${{ github.event.release.prerelease }} - name: Build and test - uses: Kralizek/github-actions/actions/dotnet-build@v0.2 + uses: Kralizek/github-actions/actions/dotnet-build@v0.3 env: MinVerVersionOverride: ${{ github.event_name == 'workflow_dispatch' && steps.manual-release.outputs.version || '' }} with: @@ -261,7 +261,7 @@ jobs: - name: Validate manual release tag if: github.event_name == 'workflow_dispatch' && !inputs.dry_run - uses: Kralizek/github-actions/actions/validate-release@v0.2 + uses: Kralizek/github-actions/actions/validate-release@v0.3 with: tag: ${{ steps.manual-release.outputs.tag }} prerelease: ${{ inputs.release_channel != 'stable' }}