diff --git a/.github/workflows/release-core.yml b/.github/workflows/release-core.yml index 1bd1338..1432b63 100644 --- a/.github/workflows/release-core.yml +++ b/.github/workflows/release-core.yml @@ -57,6 +57,9 @@ on: # yamllint disable-line rule:truthy finalize_sha: description: "Release branch SHA after finalization" value: ${{ jobs.finalize.outputs.finalize_sha }} + tag_already_exists: + description: "Remote publish tag already exists at finalize SHA (retry path)" + value: ${{ jobs.finalize.outputs.tag_already_exists }} image_tag: description: "Resolved devcontainer image tag" value: ${{ jobs.validate.outputs.image_tag }} @@ -239,18 +242,7 @@ jobs: echo "publish_version=$PUBLISH_VERSION" >> "$GITHUB_OUTPUT" echo "next_rc=$NEXT_RC" >> "$GITHUB_OUTPUT" - - name: Verify publish tag does not exist - env: - PUBLISH_VERSION: ${{ steps.publish_meta.outputs.publish_version }} - run: | - if git ls-remote --exit-code --tags --refs origin "refs/tags/$PUBLISH_VERSION" > /dev/null 2>&1; then - echo "ERROR: Tag $PUBLISH_VERSION already exists" - exit 1 - fi - if git rev-parse -q --verify "refs/tags/$PUBLISH_VERSION" > /dev/null; then - echo "ERROR: Local tag $PUBLISH_VERSION already exists" - exit 1 - fi + # Remote tag vs finalize SHA is validated in the finalize job (tag_state) after finalize_sha is known. - name: Find and verify PR id: pr @@ -349,6 +341,7 @@ jobs: if: ${{ inputs.dry_run != true }} outputs: finalize_sha: ${{ steps.finalize.outputs.finalize_sha }} + tag_already_exists: ${{ steps.tag_state.outputs.tag_already_exists }} steps: - name: Generate commit app token @@ -492,6 +485,34 @@ jobs: fi echo "finalize_sha=$FINALIZE_SHA" >> "$GITHUB_OUTPUT" + - name: Check if publish tag already exists at finalize SHA + id: tag_state + env: + PUBLISH_VERSION: ${{ needs.validate.outputs.publish_version }} + FINALIZE_SHA: ${{ steps.finalize.outputs.finalize_sha }} + run: | + set -euo pipefail + REMOTE_LINE=$(git ls-remote origin "refs/tags/${PUBLISH_VERSION}^{}" || true) + if [ -z "$REMOTE_LINE" ]; then + REMOTE_LINE=$(git ls-remote origin "refs/tags/${PUBLISH_VERSION}" || true) + fi + if [ -z "$REMOTE_LINE" ]; then + echo "tag_already_exists=false" >> "$GITHUB_OUTPUT" + echo "No remote tag ${PUBLISH_VERSION} yet" + exit 0 + fi + REMOTE_TARGET_SHA=$(printf '%s\n' "$REMOTE_LINE" | awk '{print $1}') + if [ -z "$REMOTE_TARGET_SHA" ]; then + echo "tag_already_exists=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ "$REMOTE_TARGET_SHA" != "$FINALIZE_SHA" ]; then + echo "ERROR: Tag $PUBLISH_VERSION exists but target commit is $REMOTE_TARGET_SHA; expected $FINALIZE_SHA (finalize SHA)" + exit 1 + fi + echo "tag_already_exists=true" >> "$GITHUB_OUTPUT" + echo "Remote tag $PUBLISH_VERSION already points to finalize SHA; publish will skip tag create/push" + test: name: Test Finalized Release needs: [validate, finalize] diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 60ae8dd..00afaef 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -33,6 +33,11 @@ on: # yamllint disable-line rule:truthy required: false default: "41898282+github-actions[bot]@users.noreply.github.com" type: string + tag_already_exists: + description: "Skip tag create/push when remote tag already points at finalize SHA" + required: false + default: false + type: boolean secrets: token: required: false @@ -134,6 +139,7 @@ jobs: git config user.email "$GIT_USER_EMAIL" - name: Create and push tag + if: ${{ !inputs.tag_already_exists }} env: PUBLISH_VERSION: ${{ inputs.publish_version }} run: | @@ -143,6 +149,9 @@ jobs: if retry --retries 3 --backoff 5 --max-backoff 30 -- git ls-remote --tags --refs origin "$PUBLISH_VERSION" | grep -q "refs/tags/$PUBLISH_VERSION$"; then LOCAL_TAG_TARGET_SHA=$(git rev-parse "$PUBLISH_VERSION^{}") REMOTE_TAG_TARGET_SHA=$(retry --retries 3 --backoff 5 --max-backoff 30 -- git ls-remote --tags origin "refs/tags/$PUBLISH_VERSION^{}" | awk '{print $1}') + if [ -z "$REMOTE_TAG_TARGET_SHA" ]; then + REMOTE_TAG_TARGET_SHA=$(retry --retries 3 --backoff 5 --max-backoff 30 -- git ls-remote --tags origin "refs/tags/$PUBLISH_VERSION" | awk '{print $1}') + fi if [ -z "$REMOTE_TAG_TARGET_SHA" ]; then echo "ERROR: Remote tag exists but target SHA could not be resolved: $PUBLISH_VERSION" exit 1 @@ -181,8 +190,20 @@ jobs: GH_TOKEN: ${{ steps.auth.outputs.token }} run: | set -euo pipefail - if retry --retries 2 --backoff 5 --max-backoff 20 -- gh release view "$PUBLISH_VERSION" >/dev/null 2>&1; then - echo "ERROR: GitHub Release already exists for tag $PUBLISH_VERSION" + if RELEASE_JSON=$(retry --retries 2 --backoff 5 --max-backoff 20 -- gh release view "$PUBLISH_VERSION" --json isDraft,isPrerelease 2>/dev/null); then + IS_DRAFT=$(printf '%s' "$RELEASE_JSON" | jq -r '.isDraft') + if [ "$IS_DRAFT" = "true" ]; then + echo "Draft GitHub Release already exists for $PUBLISH_VERSION; skipping create (retry path)." + exit 0 + fi + if [ "$RELEASE_KIND" = "candidate" ]; then + IS_PRERELEASE=$(printf '%s' "$RELEASE_JSON" | jq -r '.isPrerelease') + if [ "$IS_PRERELEASE" = "true" ]; then + echo "Pre-release already exists for $PUBLISH_VERSION; skipping create (candidate retry path)." + exit 0 + fi + fi + echo "ERROR: Published (non-draft) GitHub Release already exists for tag $PUBLISH_VERSION" exit 1 fi if [ "$RELEASE_KIND" = "candidate" ]; then @@ -195,7 +216,9 @@ jobs: retry --retries 3 --backoff 5 --max-backoff 30 -- gh release create "$PUBLISH_VERSION" \ --title "$PUBLISH_VERSION" \ --notes-file /tmp/release-notes.md \ - --verify-tag + --verify-tag \ + --draft + echo "Draft GitHub Release created; publish from ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases when review is complete." fi - name: Set outputs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 81b097e..e32d1c5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -104,6 +104,7 @@ jobs: release_kind: ${{ needs.core.outputs.release_kind }} git_user_name: ${{ inputs.git-user-name }} git_user_email: ${{ inputs.git-user-email }} + tag_already_exists: ${{ needs.core.outputs.tag_already_exists }} secrets: inherit rollback: @@ -183,17 +184,6 @@ jobs: fi fi - - name: Delete tag if created - if: ${{ needs.core.outputs.publish_version != '' }} - continue-on-error: true - env: - PUBLISH_VERSION: ${{ needs.core.outputs.publish_version }} - run: | - set -euo pipefail - if git ls-remote origin "refs/tags/$PUBLISH_VERSION" | grep -q "$PUBLISH_VERSION"; then - retry --retries 3 --backoff 5 --max-backoff 30 -- git push origin ":refs/tags/$PUBLISH_VERSION" || true - fi - - name: Create failure issue if: ${{ needs.core.outputs.version != '' }} env: @@ -213,5 +203,9 @@ jobs: **Release PR:** #$PR_NUMBER **Automatic rollback attempted:** - - Release branch reset to pre-finalization state - - Release tag deleted (if created)" + - Release branch reset to pre-finalization state (best-effort) + + **Tag status (forward-fix policy):** + - Release tags are not deleted by automation (workflow choice; GitHub immutable-release lock-in applies only after a release is **published** when that setting is enabled). If a tag was pushed before the failure, it remains on the remote. + - Use a new release candidate to validate fixes, then re-run the final release when ready. + - If a draft GitHub Release exists, manage it from the Releases UI; **publishing** locks the linked tag and assets when **immutable releases** are enabled." diff --git a/.github/workflows/repository-dispatch.yml b/.github/workflows/repository-dispatch.yml index bc12e5c..882cd21 100644 --- a/.github/workflows/repository-dispatch.yml +++ b/.github/workflows/repository-dispatch.yml @@ -894,7 +894,8 @@ jobs: ## Manual cleanup guidance - Inspect deploy/release PRs and workflow logs before retrying. - If needed, close stale release PRs and delete stale \`release/\` branch. - - Re-dispatch using a new RC tag/version once root cause is fixed. + - Do not rewrite or delete **published** GitHub Releases (or their linked tags when **immutable releases** are enabled) to retry the same version; bare git tags without a published release are not locked by that feature unless a tag ruleset applies. + - After fixing the root cause upstream, publish a **new** RC tag (or a new final attempt only after branch/tag state matches your release policy), then rely on a fresh dispatch. EOF )"