From eb1e7a7215705167c9511eb06e8001246d0b9ce2 Mon Sep 17 00:00:00 2001 From: Carlos Vigo Date: Thu, 26 Feb 2026 08:54:27 +0000 Subject: [PATCH] fix: move floating-tag updates to separate job and resolve tags via exact ref - Resolve each floating tag via the exact "Get a reference" API (GET repos/.../git/ref/tags/$TAG) so the SHA is for the exact tag. - Run floating-tag updates in a dedicated job (update-floating-tags) that runs only after the release job succeeds. The main rollback job no longer captures or restores floating tags. - CHANGELOG: add Fixed entry for #38 Refs: #38 --- .github/workflows/release.yml | 179 ++++++++++++++++++---------------- CHANGELOG.md | 5 +- 2 files changed, 101 insertions(+), 83 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f09efea..066ea6d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,8 +18,9 @@ # 1. Validate — Check all prerequisites # 2. Finalize — Bump package.json, set release date in CHANGELOG, commit, push # 3. Test — Build verification and full test suite on finalized code -# 4. Release — Create tag (vX.Y.Z), GitHub Release, and floating tags (vX, vX.Y) -# 5. Rollback — Revert changes and create issue on failure +# 4. Release — Create tag (vX.Y.Z), GitHub Release +# 5. Update floating tags — Job after release: update vX, vX.Y (with on-failure restore) +# 6. Rollback — Revert changes and create issue on failure (no floating-tag restore) # # Trigger: # - Manual workflow_dispatch with version input @@ -67,8 +68,6 @@ jobs: pr_number: ${{ steps.pr.outputs.pr_number }} release_date: ${{ steps.vars.outputs.release_date }} pre_finalize_sha: ${{ steps.pre_sha.outputs.pre_finalize_sha }} - floating_major_sha: ${{ steps.floating-shas.outputs.floating_major_sha }} - floating_minor_sha: ${{ steps.floating-shas.outputs.floating_minor_sha }} steps: - name: Validate and prepare variables @@ -121,32 +120,6 @@ jobs: exit 1 fi - - name: Capture existing floating tag SHAs - id: floating-shas - env: - VERSION: ${{ steps.vars.outputs.version }} - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - FLOAT_MAJOR="v${VERSION%%.*}" - FLOAT_MINOR="v${VERSION%.*}" - - get_tag_sha() { - local TAG="$1" - local SHA - SHA=$(gh api "repos/$GITHUB_REPOSITORY/git/matching-refs/tags/$TAG" \ - --jq '.[0].object.sha // empty' 2>/dev/null || true) - if echo "$SHA" | grep -qE '^[0-9a-f]{40}$'; then - echo "$SHA" - fi - } - - MAJOR_SHA=$(get_tag_sha "$FLOAT_MAJOR") - MINOR_SHA=$(get_tag_sha "$FLOAT_MINOR") - echo "floating_major_sha=$MAJOR_SHA" >> $GITHUB_OUTPUT - echo "floating_minor_sha=$MINOR_SHA" >> $GITHUB_OUTPUT - echo "Captured floating tag SHAs: $FLOAT_MAJOR=$MAJOR_SHA $FLOAT_MINOR=$MINOR_SHA" - - name: Find and verify PR id: pr env: @@ -389,23 +362,84 @@ jobs: --verify-tag echo "GitHub Release v$VERSION created with checksums-sha256.txt attached" + - name: Attest build provenance + uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3 + with: + subject-path: dist/index.js + + - name: Summary + env: + VERSION: ${{ needs.validate.outputs.version }} + run: | + echo "Release published successfully" + echo "" + echo " Version: $VERSION" + echo " Tag: v$VERSION" + echo " Release: https://github.com/${{ github.repository }}/releases/tag/v$VERSION" + echo "" + echo "Floating tags (v${VERSION%%.*}, v${VERSION%.*}) will be updated in the next job." + echo "" + echo "Phase 3 (post-release.yml) will:" + echo " 1. Merge the release PR to main" + echo " 2. Sync dev with main" + + update-floating-tags: + name: Update Floating Tags + needs: [validate, finalize, release] + runs-on: ubuntu-22.04 + timeout-minutes: 10 + permissions: + contents: write + steps: + - name: Generate GitHub App Token + id: app-token + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2 + with: + app-id: ${{ secrets.RELEASE_APP_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + + - name: Capture existing floating tag SHAs + id: capture + env: + VERSION: ${{ needs.validate.outputs.version }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + set -euo pipefail + FLOAT_MAJOR="v${VERSION%%.*}" + FLOAT_MINOR="v${VERSION%.*}" + + get_tag_sha() { + local TAG="$1" + local SHA + SHA=$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$TAG" \ + --jq '.object.sha' 2>/dev/null || true) + if [ -n "$SHA" ] && echo "$SHA" | grep -qE '^[0-9a-f]{40}$'; then + echo "$SHA" + fi + } + + MAJOR_SHA=$(get_tag_sha "$FLOAT_MAJOR") + MINOR_SHA=$(get_tag_sha "$FLOAT_MINOR") + echo "floating_major_sha=$MAJOR_SHA" >> $GITHUB_OUTPUT + echo "floating_minor_sha=$MINOR_SHA" >> $GITHUB_OUTPUT + echo "Captured floating tag SHAs: $FLOAT_MAJOR=$MAJOR_SHA $FLOAT_MINOR=$MINOR_SHA" + - name: Update floating version tags - id: floating-tags env: VERSION: ${{ needs.validate.outputs.version }} + COMMIT_SHA: ${{ needs.finalize.outputs.finalize_sha }} GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | set -euo pipefail - COMMIT_SHA=$(git rev-parse HEAD) FLOAT_MAJOR="v${VERSION%%.*}" FLOAT_MINOR="v${VERSION%.*}" get_tag_sha() { local TAG="$1" local SHA - SHA=$(gh api "repos/$GITHUB_REPOSITORY/git/matching-refs/tags/$TAG" \ - --jq '.[0].object.sha // empty' 2>/dev/null || true) - if echo "$SHA" | grep -qE '^[0-9a-f]{40}$'; then + SHA=$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$TAG" \ + --jq '.object.sha' 2>/dev/null || true) + if [ -n "$SHA" ] && echo "$SHA" | grep -qE '^[0-9a-f]{40}$'; then echo "$SHA" fi } @@ -428,25 +462,37 @@ jobs: upsert_tag "$FLOAT_MAJOR" "$COMMIT_SHA" upsert_tag "$FLOAT_MINOR" "$COMMIT_SHA" - - name: Attest build provenance - uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3 - with: - subject-path: dist/index.js - - - name: Summary + - name: Restore floating version tags on failure + if: failure() + continue-on-error: true env: VERSION: ${{ needs.validate.outputs.version }} + PREV_MAJOR_SHA: ${{ steps.capture.outputs.floating_major_sha }} + PREV_MINOR_SHA: ${{ steps.capture.outputs.floating_minor_sha }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | - echo "Release published successfully" - echo "" - echo " Version: $VERSION" - echo " Tag: v$VERSION" - echo " Floating: v${VERSION%%.*}, v${VERSION%.*}" - echo " Release: https://github.com/${{ github.repository }}/releases/tag/v$VERSION" - echo "" - echo "Phase 3 (post-release.yml) will:" - echo " 1. Merge the release PR to main" - echo " 2. Sync dev with main" + FLOAT_MAJOR="v${VERSION%%.*}" + FLOAT_MINOR="v${VERSION%.*}" + + restore_or_delete() { + local TAG="$1" PREV_SHA="$2" + if [ -n "$PREV_SHA" ] && echo "$PREV_SHA" | grep -qE '^[0-9a-f]{40}$'; then + gh api "repos/$GITHUB_REPOSITORY/git/refs/tags/$TAG" \ + --method PATCH -f sha="$PREV_SHA" -F force=true 2>/dev/null \ + && echo "Restored $TAG → $PREV_SHA" \ + || echo "Warning: Could not restore $TAG" + elif [ -n "$PREV_SHA" ]; then + echo "Warning: Invalid previous SHA for $TAG ($PREV_SHA), skipping restore/delete" + else + gh api "repos/$GITHUB_REPOSITORY/git/refs/tags/$TAG" \ + --method DELETE 2>/dev/null \ + && echo "Deleted floating tag $TAG" \ + || echo "No floating tag $TAG to delete" + fi + } + + restore_or_delete "$FLOAT_MAJOR" "$PREV_MAJOR_SHA" + restore_or_delete "$FLOAT_MINOR" "$PREV_MINOR_SHA" dry-run-preview: name: Dry-Run Preview @@ -566,37 +612,6 @@ jobs: echo "Tag does not exist (not created yet)" fi - - name: Restore floating version tags - continue-on-error: true - env: - VERSION: ${{ needs.validate.outputs.version }} - PREV_MAJOR_SHA: ${{ needs.validate.outputs.floating_major_sha }} - PREV_MINOR_SHA: ${{ needs.validate.outputs.floating_minor_sha }} - GH_TOKEN: ${{ steps.app-token.outputs.token }} - run: | - FLOAT_MAJOR="v${VERSION%%.*}" - FLOAT_MINOR="v${VERSION%.*}" - - restore_or_delete() { - local TAG="$1" PREV_SHA="$2" - if [ -n "$PREV_SHA" ] && echo "$PREV_SHA" | grep -qE '^[0-9a-f]{40}$'; then - gh api "repos/$GITHUB_REPOSITORY/git/refs/tags/$TAG" \ - --method PATCH -f sha="$PREV_SHA" -F force=true 2>/dev/null \ - && echo "Restored $TAG → $PREV_SHA" \ - || echo "Warning: Could not restore $TAG" - elif [ -n "$PREV_SHA" ]; then - echo "Warning: Invalid previous SHA for $TAG ($PREV_SHA), skipping restore/delete" - else - gh api "repos/$GITHUB_REPOSITORY/git/refs/tags/$TAG" \ - --method DELETE 2>/dev/null \ - && echo "Deleted floating tag $TAG" \ - || echo "No floating tag $TAG to delete" - fi - } - - restore_or_delete "$FLOAT_MAJOR" "$PREV_MAJOR_SHA" - restore_or_delete "$FLOAT_MINOR" "$PREV_MINOR_SHA" - - name: Create failure issue env: VERSION: ${{ needs.validate.outputs.version }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 63bd567..2876a9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,7 +39,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Corrected heading hierarchy in `formatPRAsMarkdown`: promoted the Comments section header from `##` to `#` and individual comment entry headers from `###` to `##` - **Release workflow avoids immutable-release upload failures** - Generates `checksums-sha256.txt` before creating the GitHub release and attaches it during `gh release create` instead of uploading afterward - - Hardens floating-tag SHA handling by resolving tags via `git/matching-refs`, validating SHA format, and skipping invalid rollback SHAs +- **Release workflow: floating-tag updates and rollback** ([#38](https://github.com/vig-os/sync-issues-action/issues/38)) + - Floating-tag updates (vX, vX.Y) run in a separate job after the release job succeeds; main rollback no longer restores floating tags + - Resolve floating tags via exact "Get a reference" API (`git/ref/tags/$TAG`) instead of `git/matching-refs` to avoid wrong-SHA from prefix matches + - New job captures current SHAs, updates tags, and on failure restores from captured SHAs (self-contained) - **`--force-update` does not re-sync issues (only PRs)** ([#10](https://github.com/vig-os/sync-issues-action/issues/10)) - Added `force-update` action input that bypasses the `hasContentChanged` content-comparison gate - When active, all fetched items are re-written (with updated `synced:` frontmatter) even if body content is unchanged