Skip to content

PREQ-8689: Create release tags via draft GitHub release - #318

Open
tomverin wants to merge 1 commit into
masterfrom
bugfix/tom/PREQ-8689-draft-first-tag
Open

tomverin wants to merge 1 commit into
masterfrom
bugfix/tom/PREQ-8689-draft-first-tag

Conversation

@tomverin

@tomverin tomverin commented Sep 15, 2026

Copy link
Copy Markdown

Summary

  • Release of sonar-scanner-cli-docker fails at Create and push release tag with GH013: Cannot create ref due to creations being restricted (git tag + git push of 12.2.0.3249_8.1.0). The tag was never created.
  • Replace that git-protocol tag push with the documented draft-first flow: gh release create --draft --target "$GITHUB_SHA" (same pattern as gh-action_release v7). Existing drafts are reused so a later step failure can be retried; a published immutable release is rejected.
  • gh-action_sbom@v3 still attaches the SBOM to the draft; gh release edit --draft=false still publishes.

Jira: PREQ-8689

Test plan

  • Confirm no GitHub release or tag exists for 12.2.0.3249_8.1.0 (none after the failed run).
  • After merge to master, dispatch Actions → Release with tag_name=12.2.0.3249_8.1.0.
  • Create or reuse draft release succeeds and creates a draft for that tag at $GITHUB_SHA.
  • SBOM is attached to the draft; promote / Docker Hub / publish complete.
  • Re-running the same workflow while the release is still a draft reuses it instead of failing.

@hashicorp-vault-sonar-prod

hashicorp-vault-sonar-prod Bot commented Sep 15, 2026

Copy link
Copy Markdown

PREQ-8689

@sonarqube-next

Copy link
Copy Markdown

Comment on lines +53 to +67
- name: Create or reuse draft release
env:
GH_TOKEN: ${{ github.token }}
TAG_NAME: ${{ inputs.tag_name }}
run: |
if git ls-remote --exit-code --tags origin "$TAG_NAME" > /dev/null 2>&1; then
echo "Tag '$TAG_NAME' already exists on origin. To retry, clean up the partial release first:"
echo " 1. Delete the tag: git push origin :refs/tags/$TAG_NAME"
echo " 2. If a draft GitHub release exists for '$TAG_NAME', delete it before re-dispatching."
exit 1
set -euo pipefail
if release_details="$(gh release view "$TAG_NAME" --json id,isDraft 2>/dev/null)"; then
release_is_draft=$(jq -r '.isDraft' <<< "$release_details")
if [[ "$release_is_draft" == "true" ]]; then
echo "Reusing existing draft release $TAG_NAME"
else
echo "::error::Release $TAG_NAME already exists and is NOT a draft. Aborting to avoid mutating an immutable release."
exit 1
fi
else

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Bug: Draft reuse/create never validates the release target commit

The step only reads isDraft, so two cases silently publish a release at the wrong commit. (a) A git tag already exists without a release (exactly the state the old pre-flight check guarded against — e.g. an old attempt that pushed the tag, or a manually pushed tag): gh release create --draft --target "$GITHUB_SHA" succeeds, but GitHub ignores target_commitish when the tag ref already exists, so the final gh release edit --draft=false publishes against the pre-existing tag/commit instead of $GITHUB_SHA. (b) On the reuse path a draft left over from an earlier dispatch keeps its original targetCommitish, so re-dispatching after new commits publishes the old commit while the log just says "Reusing existing draft release". Query targetCommitish and compare it with $GITHUB_SHA, and fail fast if a tag ref for $TAG_NAME already exists on origin.

Fail fast on a pre-existing tag ref and on a draft whose target does not match the dispatched SHA:

set -euo pipefail
if git ls-remote --exit-code --tags origin "$TAG_NAME" > /dev/null 2>&1; then
  echo "::error::Tag $TAG_NAME already exists on origin; publishing would release that ref, not $GITHUB_SHA. Delete it first: git push origin :refs/tags/$TAG_NAME"
  exit 1
fi
if release_details="$(gh release view "$TAG_NAME" --json isDraft,targetCommitish 2>/dev/null)"; then
  if [[ "$(jq -r '.isDraft' <<< "$release_details")" != "true" ]]; then
    echo "::error::Release $TAG_NAME already exists and is NOT a draft. Aborting to avoid mutating an immutable release."
    exit 1
  fi
  draft_target=$(jq -r '.targetCommitish' <<< "$release_details")
  if [[ "$draft_target" != "$GITHUB_SHA" ]]; then
    echo "::error::Existing draft $TAG_NAME targets $draft_target but this run is $GITHUB_SHA. Delete the draft or re-dispatch from $draft_target."
    exit 1
  fi
  echo "Reusing existing draft release $TAG_NAME"
else
  gh release create "$TAG_NAME" --draft --title "Release $TAG_NAME" --target "$GITHUB_SHA"
fi
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

Comment on lines +59 to +72
if release_details="$(gh release view "$TAG_NAME" --json id,isDraft 2>/dev/null)"; then
release_is_draft=$(jq -r '.isDraft' <<< "$release_details")
if [[ "$release_is_draft" == "true" ]]; then
echo "Reusing existing draft release $TAG_NAME"
else
echo "::error::Release $TAG_NAME already exists and is NOT a draft. Aborting to avoid mutating an immutable release."
exit 1
fi
else
gh release create "$TAG_NAME" \
--draft \
--title "Release $TAG_NAME" \
--target "$GITHUB_SHA"
echo "Created draft release $TAG_NAME"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Bug: Non-404 gh release view failures create a duplicate draft

2>/dev/null plus a bare exit-status test treats every gh release view failure — network blip, 5xx, rate limit, auth/repo-resolution error — as "release does not exist", so the script falls through to gh release create --draft. Because draft releases have no tag ref, GitHub happily accepts a second draft with the same tag name, and the later gh release upload (gh-action_sbom) and gh release edit --draft=false each resolve the tag to whichever draft the API returns first, so the SBOM can be attached to one draft while a different, empty one gets published. Capture stderr and only take the create path when gh reports the release as not found, otherwise re-raise the error.

Only treat an explicit "release not found" as absent:

set -euo pipefail
view_err=$(mktemp)
if release_details="$(gh release view "$TAG_NAME" --json id,isDraft,targetCommitish 2>"$view_err")"; then
  : # handled below
elif grep -qi 'release not found' "$view_err"; then
  release_details=""
else
  echo "::error::Failed to query release $TAG_NAME:"; cat "$view_err"; exit 1
fi
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

Comment on lines +53 to +67
- name: Create or reuse draft release
env:
GH_TOKEN: ${{ github.token }}
TAG_NAME: ${{ inputs.tag_name }}
run: |
if git ls-remote --exit-code --tags origin "$TAG_NAME" > /dev/null 2>&1; then
echo "Tag '$TAG_NAME' already exists on origin. To retry, clean up the partial release first:"
echo " 1. Delete the tag: git push origin :refs/tags/$TAG_NAME"
echo " 2. If a draft GitHub release exists for '$TAG_NAME', delete it before re-dispatching."
exit 1
set -euo pipefail
if release_details="$(gh release view "$TAG_NAME" --json id,isDraft 2>/dev/null)"; then
release_is_draft=$(jq -r '.isDraft' <<< "$release_details")
if [[ "$release_is_draft" == "true" ]]; then
echo "Reusing existing draft release $TAG_NAME"
else
echo "::error::Release $TAG_NAME already exists and is NOT a draft. Aborting to avoid mutating an immutable release."
exit 1
fi
else

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Quality: DEVELOPER.md release/recovery procedure is now wrong

The release runbook still describes the removed behaviour: it says the workflow "creates and pushes the git tag at HEAD of the dispatched branch" (the tag is now only created when the draft is published in the last step) and tells operators that "re-dispatching with the same tag will fail at the pre-flight check" and that they must delete the remote tag and the draft release to recover. With the draft-first flow a retry is expected to reuse the draft, and there is no tag to delete, so an operator following this section during a failed release takes unnecessary destructive steps. Update the Releasing / Recovering sections to describe draft-first tag creation and retry-by-reuse.

Rewrite the two stale passages in DEVELOPER.md:

The workflow validates the tag format, creates (or reuses) a draft GitHub release targeting the dispatched commit, generates the SBOM, promotes the staged Docker image, pushes it to Docker Hub, and finally publishes the GitHub release — which is what creates the git tag.

### Recovering from a failed release

If the workflow fails before the release is published, simply re-dispatch with the same tag: the existing draft release is reused and no tag exists yet. If the release was already published, the tag and release are immutable — release a new tag instead.
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Sep 15, 2026

Copy link
Copy Markdown
Code Review ⚠️ Changes requested 0 resolved / 3 findings

Replaces git-protocol tag push with a draft-first GitHub release flow to work around creation restrictions, but the implementation has critical gaps: the draft reuse/create step never validates that the release targets the correct commit, so a pre-existing tag or stale draft from an earlier dispatch will silently publish against the wrong commit. Additionally, non-404 failures in the draft lookup fall through to create duplicate drafts, and the DEVELOPER.md runbook describes the removed behaviour and gives incorrect recovery steps. Validate targetCommitish against $GITHUB_SHA, fail fast if a tag ref already exists, distinguish gh release view not-found from other errors, and update the release/recovery documentation.

⚠️ Bug: Draft reuse/create never validates the release target commit

📄 .github/workflows/release.yml:53-67 📄 .github/workflows/release.yml:122-126 🔗 target_commitish unused if tag exists

The step only reads isDraft, so two cases silently publish a release at the wrong commit. (a) A git tag already exists without a release (exactly the state the old pre-flight check guarded against — e.g. an old attempt that pushed the tag, or a manually pushed tag): gh release create --draft --target "$GITHUB_SHA" succeeds, but GitHub ignores target_commitish when the tag ref already exists, so the final gh release edit --draft=false publishes against the pre-existing tag/commit instead of $GITHUB_SHA. (b) On the reuse path a draft left over from an earlier dispatch keeps its original targetCommitish, so re-dispatching after new commits publishes the old commit while the log just says "Reusing existing draft release". Query targetCommitish and compare it with $GITHUB_SHA, and fail fast if a tag ref for $TAG_NAME already exists on origin.

Fail fast on a pre-existing tag ref and on a draft whose target does not match the dispatched SHA
set -euo pipefail
if git ls-remote --exit-code --tags origin "$TAG_NAME" > /dev/null 2>&1; then
  echo "::error::Tag $TAG_NAME already exists on origin; publishing would release that ref, not $GITHUB_SHA. Delete it first: git push origin :refs/tags/$TAG_NAME"
  exit 1
fi
if release_details="$(gh release view "$TAG_NAME" --json isDraft,targetCommitish 2>/dev/null)"; then
  if [[ "$(jq -r '.isDraft' <<< "$release_details")" != "true" ]]; then
    echo "::error::Release $TAG_NAME already exists and is NOT a draft. Aborting to avoid mutating an immutable release."
    exit 1
  fi
  draft_target=$(jq -r '.targetCommitish' <<< "$release_details")
  if [[ "$draft_target" != "$GITHUB_SHA" ]]; then
    echo "::error::Existing draft $TAG_NAME targets $draft_target but this run is $GITHUB_SHA. Delete the draft or re-dispatch from $draft_target."
    exit 1
  fi
  echo "Reusing existing draft release $TAG_NAME"
else
  gh release create "$TAG_NAME" --draft --title "Release $TAG_NAME" --target "$GITHUB_SHA"
fi
💡 Bug: Non-404 gh release view failures create a duplicate draft

📄 .github/workflows/release.yml:59-72

2>/dev/null plus a bare exit-status test treats every gh release view failure — network blip, 5xx, rate limit, auth/repo-resolution error — as "release does not exist", so the script falls through to gh release create --draft. Because draft releases have no tag ref, GitHub happily accepts a second draft with the same tag name, and the later gh release upload (gh-action_sbom) and gh release edit --draft=false each resolve the tag to whichever draft the API returns first, so the SBOM can be attached to one draft while a different, empty one gets published. Capture stderr and only take the create path when gh reports the release as not found, otherwise re-raise the error.

Only treat an explicit "release not found" as absent
set -euo pipefail
view_err=$(mktemp)
if release_details="$(gh release view "$TAG_NAME" --json id,isDraft,targetCommitish 2>"$view_err")"; then
  : # handled below
elif grep -qi 'release not found' "$view_err"; then
  release_details=""
else
  echo "::error::Failed to query release $TAG_NAME:"; cat "$view_err"; exit 1
fi
💡 Quality: DEVELOPER.md release/recovery procedure is now wrong

📄 .github/workflows/release.yml:53-67

The release runbook still describes the removed behaviour: it says the workflow "creates and pushes the git tag at HEAD of the dispatched branch" (the tag is now only created when the draft is published in the last step) and tells operators that "re-dispatching with the same tag will fail at the pre-flight check" and that they must delete the remote tag and the draft release to recover. With the draft-first flow a retry is expected to reuse the draft, and there is no tag to delete, so an operator following this section during a failed release takes unnecessary destructive steps. Update the Releasing / Recovering sections to describe draft-first tag creation and retry-by-reuse.

Rewrite the two stale passages in DEVELOPER.md
The workflow validates the tag format, creates (or reuses) a draft GitHub release targeting the dispatched commit, generates the SBOM, promotes the staged Docker image, pushes it to Docker Hub, and finally publishes the GitHub release — which is what creates the git tag.

### Recovering from a failed release

If the workflow fails before the release is published, simply re-dispatch with the same tag: the existing draft release is reused and no tag exists yet. If the release was already published, the tag and release are immutable — release a new tag instead.
🤖 Prompt for agents
Code Review: Replaces git-protocol tag push with a draft-first GitHub release flow to work around creation restrictions, but the implementation has critical gaps: the draft reuse/create step never validates that the release targets the correct commit, so a pre-existing tag or stale draft from an earlier dispatch will silently publish against the wrong commit. Additionally, non-404 failures in the draft lookup fall through to create duplicate drafts, and the DEVELOPER.md runbook describes the removed behaviour and gives incorrect recovery steps. Validate `targetCommitish` against `$GITHUB_SHA`, fail fast if a tag ref already exists, distinguish `gh release view` not-found from other errors, and update the release/recovery documentation.

1. ⚠️ Bug: Draft reuse/create never validates the release target commit
   Files: .github/workflows/release.yml:53-67, .github/workflows/release.yml:122-126

   The step only reads `isDraft`, so two cases silently publish a release at the wrong commit. (a) A git tag already exists without a release (exactly the state the old pre-flight check guarded against — e.g. an old attempt that pushed the tag, or a manually pushed tag): `gh release create --draft --target "$GITHUB_SHA"` succeeds, but GitHub ignores `target_commitish` when the tag ref already exists, so the final `gh release edit --draft=false` publishes against the pre-existing tag/commit instead of `$GITHUB_SHA`. (b) On the reuse path a draft left over from an earlier dispatch keeps its original `targetCommitish`, so re-dispatching after new commits publishes the old commit while the log just says "Reusing existing draft release". Query `targetCommitish` and compare it with `$GITHUB_SHA`, and fail fast if a tag ref for `$TAG_NAME` already exists on origin.

   Fix (Fail fast on a pre-existing tag ref and on a draft whose target does not match the dispatched SHA):
   set -euo pipefail
   if git ls-remote --exit-code --tags origin "$TAG_NAME" > /dev/null 2>&1; then
     echo "::error::Tag $TAG_NAME already exists on origin; publishing would release that ref, not $GITHUB_SHA. Delete it first: git push origin :refs/tags/$TAG_NAME"
     exit 1
   fi
   if release_details="$(gh release view "$TAG_NAME" --json isDraft,targetCommitish 2>/dev/null)"; then
     if [[ "$(jq -r '.isDraft' <<< "$release_details")" != "true" ]]; then
       echo "::error::Release $TAG_NAME already exists and is NOT a draft. Aborting to avoid mutating an immutable release."
       exit 1
     fi
     draft_target=$(jq -r '.targetCommitish' <<< "$release_details")
     if [[ "$draft_target" != "$GITHUB_SHA" ]]; then
       echo "::error::Existing draft $TAG_NAME targets $draft_target but this run is $GITHUB_SHA. Delete the draft or re-dispatch from $draft_target."
       exit 1
     fi
     echo "Reusing existing draft release $TAG_NAME"
   else
     gh release create "$TAG_NAME" --draft --title "Release $TAG_NAME" --target "$GITHUB_SHA"
   fi

2. 💡 Bug: Non-404 `gh release view` failures create a duplicate draft
   Files: .github/workflows/release.yml:59-72

   `2>/dev/null` plus a bare exit-status test treats every `gh release view` failure — network blip, 5xx, rate limit, auth/repo-resolution error — as "release does not exist", so the script falls through to `gh release create --draft`. Because draft releases have no tag ref, GitHub happily accepts a second draft with the same tag name, and the later `gh release upload` (gh-action_sbom) and `gh release edit --draft=false` each resolve the tag to whichever draft the API returns first, so the SBOM can be attached to one draft while a different, empty one gets published. Capture stderr and only take the create path when gh reports the release as not found, otherwise re-raise the error.

   Fix (Only treat an explicit "release not found" as absent):
   set -euo pipefail
   view_err=$(mktemp)
   if release_details="$(gh release view "$TAG_NAME" --json id,isDraft,targetCommitish 2>"$view_err")"; then
     : # handled below
   elif grep -qi 'release not found' "$view_err"; then
     release_details=""
   else
     echo "::error::Failed to query release $TAG_NAME:"; cat "$view_err"; exit 1
   fi

3. 💡 Quality: DEVELOPER.md release/recovery procedure is now wrong
   Files: .github/workflows/release.yml:53-67

   The release runbook still describes the removed behaviour: it says the workflow "creates and pushes the git tag at HEAD of the dispatched branch" (the tag is now only created when the draft is published in the last step) and tells operators that "re-dispatching with the same tag will fail at the pre-flight check" and that they must delete the remote tag and the draft release to recover. With the draft-first flow a retry is expected to reuse the draft, and there is no tag to delete, so an operator following this section during a failed release takes unnecessary destructive steps. Update the Releasing / Recovering sections to describe draft-first tag creation and retry-by-reuse.

   Fix (Rewrite the two stale passages in DEVELOPER.md):
   The workflow validates the tag format, creates (or reuses) a draft GitHub release targeting the dispatched commit, generates the SBOM, promotes the staged Docker image, pushes it to Docker Hub, and finally publishes the GitHub release — which is what creates the git tag.
   
   ### Recovering from a failed release
   
   If the workflow fails before the release is published, simply re-dispatch with the same tag: the existing draft release is reused and no tag exists yet. If the release was already published, the tag and release are immutable — release a new tag instead.

Review coverage

Functional validation 1 of 1 objectives covered

Rules No rules evaluated

Auto-approval Not enabled · Set up

Implementation Status ✅ 1 of 1 objectives covered
PREQ-8689 - 1 of 1 objectives covered

This PR covers the objective of creating release tags via draft GitHub releases.

✅ 1 covered here
  • ✅ Create release tags via draft GitHub release
Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Counting what did not apply, without listing it.
Unblock → Override a blocking verdict and allow merging.

Comment with these commands to change the behavior for this request:

Auto-apply Compact Unblock
gitar auto-apply:on         
gitar display:verbose         
gitar unblock         

Was this helpful? React with 👍 / 👎 | Gitar

@tomverin
tomverin marked this pull request as ready for review September 15, 2026 12:45
@tomverin

Copy link
Copy Markdown
Author

@sonarsource/code-orchestration-ci-experience-squad @antoine-vinot-sonarsource could you review this? It unblocks the scanner CLI Docker release (PREQ-8689 / tag 12.2.0.3249_8.1.0) by replacing git push of the release tag with draft-first gh release create.

Comment on lines 48 to 52
- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2
- uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2
with:
repository: SonarSource/sonar-scanning-examples
path: target_repository

@antoine-vinot-sonarsource antoine-vinot-sonarsource Sep 15, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This was not changed in this PR; but its very suspicious. The checkout action appear twice and the repository is set to 'sonar-scanning-examples' ?

@antoine-vinot-sonarsource antoine-vinot-sonarsource left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@tomverin I'm good to give this a try. What about Gitar comments? Do they make sense?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants