From 9dcf01aa74a7aac69b07a6f4dccebfc641e12fa9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 20 Jul 2026 21:56:10 +0100 Subject: [PATCH 1/2] fix(ci): retry action-dist network ops and auto-heal failed releases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two complementary layers on top of the git-derived action tag (#593), covering the two remaining failure classes: - Retry the network/API operations in _action-release (git push, gh release create) up to 3x. A transient push rejection or API blip is recoverable while the job is alive, so it shouldn't strand a dist-less tag. Local git ops and pnpm build are not retried — those fail deterministically and should surface. - Trigger the heal on a non-success Release run (workflow_run), not just the daily schedule. The realistic remaining failure is the release run dying mid-build, which nothing in-workflow can catch — you can't self-heal from inside a run that died. Success/skipped runs are ignored (the in-run build already committed dist there). The daily schedule stays as the cause-agnostic backstop. No loop: heal is not tag-triggered, and _action-release commits [skip ci] then force-pushes, so a heal never re-triggers CI/Release. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01K6CmYRXTZQYs4MNo6AETxT --- .../workflows/_action-release.reusable.yml | 25 ++++++++-- .github/workflows/action-dist-heal.yml | 49 ++++++++++++++----- 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/.github/workflows/_action-release.reusable.yml b/.github/workflows/_action-release.reusable.yml index 23b259f5..ca054756 100644 --- a/.github/workflows/_action-release.reusable.yml +++ b/.github/workflows/_action-release.reusable.yml @@ -63,6 +63,23 @@ jobs: RELEASE_TAG: ${{ inputs.tag }} GH_TOKEN: ${{ github.token }} run: | + # Retry the network/API operations only — a transient push rejection or GitHub API blip + # shouldn't strand a dist-less tag while the job is otherwise healthy. Local git ops and + # `pnpm build` are deliberately not retried: those fail deterministically and should surface. + retry() { + local -r what="$*" + local n=1 + until "$@"; do + if [ "$n" -ge 3 ]; then + echo "::error::failed after 3 attempts: ${what}" + return 1 + fi + echo "attempt ${n}/3 failed: ${what} — retrying in $((n * 5))s" + sleep "$((n * 5))" + n=$((n + 1)) + done + } + git add -f packages/release/dist/ git commit --no-verify -m "chore: build action dist for ${RELEASE_TAG} [skip ci]" @@ -76,21 +93,21 @@ jobs: echo "Release v${VERSION} already exists, skipping" else if [[ "${VERSION}" != *"-"* ]]; then - gh release create "v${VERSION}" --title "v${VERSION}" --generate-notes + retry gh release create "v${VERSION}" --title "v${VERSION}" --generate-notes else - gh release create "v${VERSION}" --title "v${VERSION}" --generate-notes --prerelease + retry gh release create "v${VERSION}" --title "v${VERSION}" --generate-notes --prerelease fi fi # Force-move the exact version tag to the dist commit so consumers # pinning `uses: goosewobbler/releasekit@v1.2.3` get the built dist. git tag -f "v${VERSION}" - git push origin -f "v${VERSION}" + retry git push origin -f "v${VERSION}" # Moving major alias — only update for stable releases if [[ "${VERSION}" != *"-"* ]]; then git tag -f "v${MAJOR}" - git push origin -f "v${MAJOR}" + retry git push origin -f "v${MAJOR}" else echo "Prerelease ${VERSION} — skipping major alias update" fi \ No newline at end of file diff --git a/.github/workflows/action-dist-heal.yml b/.github/workflows/action-dist-heal.yml index 2ec47990..e6271c02 100644 --- a/.github/workflows/action-dist-heal.yml +++ b/.github/workflows/action-dist-heal.yml @@ -1,10 +1,19 @@ name: Action Dist Heal # Backstop for the action tag's committed dist. The in-run build (release.yml -> _action-release) is -# the primary path; this heals a vX.Y.Z tag that ended up dist-less anyway — e.g. the release job died -# after pushing the tag but before committing dist. Reuses _action-release, which no-ops when the dist -# is already committed, so it is safe to run any time. Deliberately NOT triggered on tag push, so it -# never races the in-run path. +# the primary path; this heals a vX.Y.Z tag that ended up dist-less anyway. Reuses _action-release, +# which no-ops when the dist is already committed, so it is safe to run any time. +# +# Triggers, in order of responsiveness: +# - workflow_run on a non-success Release run — the realistic remaining failure is the release run +# dying mid-build, which nothing in-workflow can catch (you can't self-heal from inside a run that +# died). Fires within minutes; success/skipped runs are ignored because the in-run build already +# committed dist there. +# - schedule — cause-agnostic daily backstop for any unknown mode that leaves a green run dist-less. +# - workflow_dispatch — manual lever for a specific tag. +# +# NOT triggered on tag push, and _action-release commits with [skip ci] then force-pushes, so a heal +# never re-triggers CI/Release — no loop. on: workflow_dispatch: @@ -13,8 +22,11 @@ on: description: 'Version tag to heal (e.g. v1.2.3)' required: true type: string + workflow_run: + workflows: [Release] + types: [completed] schedule: - - cron: '17 6 * * *' # daily: heal the newest stable release if it is missing dist + - cron: '17 6 * * *' permissions: contents: write @@ -23,6 +35,12 @@ jobs: resolve: name: Resolve tag runs-on: ubuntu-latest + # On workflow_run, only act when the Release run didn't cleanly succeed: a successful run already + # committed dist in-run, and a skipped run released nothing. dispatch/schedule always run. + if: >- + github.event_name != 'workflow_run' || + (github.event.workflow_run.conclusion != 'success' && + github.event.workflow_run.conclusion != 'skipped') outputs: tag: ${{ steps.pick.outputs.tag }} steps: @@ -33,16 +51,21 @@ jobs: - name: Pick tag to heal id: pick env: + EVENT: ${{ github.event_name }} DISPATCH_TAG: ${{ github.event.inputs.tag }} run: | - # Manual dispatch heals the requested tag; the scheduled run heals the newest stable - # release (the realistic failure — a just-cut release missing its dist). Use the exact-tag - # dispatch input to heal a prerelease. - if [ -n "$DISPATCH_TAG" ]; then - TAG="$DISPATCH_TAG" - else - TAG=$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' | grep -vE -- '-' | sort -V | tail -1) - fi + case "$EVENT" in + workflow_dispatch) + # Heal exactly the requested tag. + TAG="$DISPATCH_TAG" ;; + workflow_run) + # A Release run just failed/cancelled — target the most recently created version tag + # (stable or prerelease); if it already carries dist the guard no-ops. + TAG=$(git tag -l --sort=-creatordate 'v[0-9]*.[0-9]*.[0-9]*' | head -1) ;; + *) + # Scheduled backstop — the newest stable release, the most consumer-critical tag. + TAG=$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' | grep -vE -- '-' | sort -V | tail -1) ;; + esac echo "tag=${TAG}" >> "$GITHUB_OUTPUT" heal: From e0380d00f2b977155f959574c77237098db12837 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 20 Jul 2026 22:08:29 +0100 Subject: [PATCH 2/2] chore(ci): add trailing newline to _action-release.reusable.yml Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01K6CmYRXTZQYs4MNo6AETxT --- .github/workflows/_action-release.reusable.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/_action-release.reusable.yml b/.github/workflows/_action-release.reusable.yml index ca054756..572e4aa8 100644 --- a/.github/workflows/_action-release.reusable.yml +++ b/.github/workflows/_action-release.reusable.yml @@ -110,4 +110,4 @@ jobs: retry git push origin -f "v${MAJOR}" else echo "Prerelease ${VERSION} — skipping major alias update" - fi \ No newline at end of file + fi