Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 33 additions & 12 deletions .github/workflows/release-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
29 changes: 26 additions & 3 deletions .github/workflows/release-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: |
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
20 changes: 7 additions & 13 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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."
3 changes: 2 additions & 1 deletion .github/workflows/repository-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/<version>\` 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
)"

Expand Down
Loading