Compare commits

..

8 Commits

Author SHA1 Message Date
argoyle e3c17ceaa0 fix: make release PR creation robust against race conditions (#19)
Two fixes for intermittent release workflow failures:

- **Use `BASE_BRANCH` instead of `DEFAULT_BRANCH`** for the PR `base` field — `BASE_BRANCH` has a `:-main` fallback, preventing an empty base from causing a 404
- **Replace fixed `sleep 3` with proper polling and retry** — polls for branch readiness (up to 10 attempts) before creating the PR, then retries PR creation (up to 5 attempts) with backoff

Fixes the `Error creating PR (HTTP 404)` seen when Gitea hasn't fully indexed the `next-release` branch by the time the PR creation request fires.

🤖 Generated with [Claude Code](https://claude.ai/claude-code)

Reviewed-on: #19
2026-03-23 15:52:25 +00:00
argoyle 068c6ef686 Merge pull request 'fix(release): add retry and delay for PR creation to handle Gitea API race condition' (#18) from fix-release-pr-race-condition into main
Reviewed-on: #18
2026-02-23 13:13:23 +00:00
argoyle d5623bdf9c fix(release): add retry and delay for PR creation to handle Gitea API race condition
The PR creation curl immediately follows branch creation via the Contents
API, but Gitea may not have fully indexed the new branch for pull request
operations yet. This causes intermittent HTTP errors on the first run.

- Add sleep 3 before PR creation to allow Gitea to process the new branch
- Use --retry-all-errors so curl retries on HTTP 4xx/5xx (not just
  connection failures)
- Capture and display the actual HTTP error code and response body on
  failure for easier debugging

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 14:12:43 +01:00
argoyle e3f1ba4e0a Merge pull request 'fix(release): replace rebase with branch recreation to prevent PR merge conflicts' (#17) from fix-release-pr-conflicts into main
Reviewed-on: #17
2026-02-12 11:02:43 +00:00
argoyle 4688810928 fix(release): replace rebase with branch recreation to prevent PR merge conflicts
Delete and recreate the next-release branch from base on each run instead of
rebasing, which caused 409 conflicts on CHANGELOG.md. Add concurrency group
to prevent parallel workflow runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 12:02:01 +01:00
renovate 646d0e4e0c chore(deps): update dependency orhun/git-cliff to v2.12.0 (#16) 2026-01-20 17:38:12 +00:00
argoyle c564063d20 Merge pull request 'fix(release): add retry logic to curl API calls' (#15) from fix-curl-retry into main
Reviewed-on: #15
2026-01-14 14:14:24 +00:00
argoyle a9458d03dd fix(release): add retry logic to curl API calls
Add --retry 3 --retry-delay 2 --retry-connrefused to all Gitea API
curl calls to handle transient connection failures. This addresses
intermittent exit code 7 (connection refused) errors when the K8s
internal service briefly drops connections.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 15:08:17 +01:00
2 changed files with 121 additions and 176 deletions
+2 -1
View File
@@ -5,7 +5,8 @@
"Bash(but rub --help:*)", "Bash(but rub --help:*)",
"WebSearch", "WebSearch",
"WebFetch(domain:docs.gitea.com)", "WebFetch(domain:docs.gitea.com)",
"WebFetch(domain:gitea.com)" "WebFetch(domain:gitea.com)",
"Bash(but status:*)"
] ]
} }
} }
+119 -175
View File
@@ -9,10 +9,14 @@ on:
default: false default: false
type: boolean type: boolean
concurrency:
group: release-${{ github.repository }}
cancel-in-progress: false
env: env:
GITEA_URL: http://gitea-http.gitea.svc.cluster.local:3000 GITEA_URL: http://gitea-http.gitea.svc.cluster.local:3000
RELEASE_TOKEN_FILE: /runner-secrets/release-token RELEASE_TOKEN_FILE: /runner-secrets/release-token
GIT_CLIFF_VERSION: "2.11.0" GIT_CLIFF_VERSION: "2.12.0"
jobs: jobs:
preconditions: preconditions:
@@ -94,39 +98,18 @@ jobs:
PR_NOTE="**Note:** Please use **Squash Merge** when merging this PR." PR_NOTE="**Note:** Please use **Squash Merge** when merging this PR."
DESCRIPTION="${CHANGES_CONTENT}"$'\n\n---\n\n'"${PR_NOTE}" DESCRIPTION="${CHANGES_CONTENT}"$'\n\n---\n\n'"${PR_NOTE}"
echo "Checking for existing release PRs..." # Delete existing next-release branch to start fresh (auto-closes any open PR)
PRS=$(curl -sf \
-H "Authorization: token ${TOKEN}" \
"${API_URL}/pulls?state=open" | jq '[.[] | select(.head.ref == "next-release")]')
PR_INDEX=$(echo "${PRS}" | jq -r '.[0].number // empty')
# If PR exists, rebase the branch onto the latest base branch
if [ -n "${PR_INDEX}" ]; then
echo "Rebasing PR #${PR_INDEX} branch onto ${BASE_BRANCH}..."
REBASE_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
-H "Authorization: token ${TOKEN}" \
"${API_URL}/pulls/${PR_INDEX}/update?style=rebase")
REBASE_CODE=$(echo "${REBASE_RESPONSE}" | tail -1)
if [ "${REBASE_CODE}" = "200" ]; then
echo "Successfully rebased branch onto ${BASE_BRANCH}"
elif [ "${REBASE_CODE}" = "409" ]; then
echo "Branch already up to date or rebase conflict - continuing with update"
else
echo "Warning: Rebase returned ${REBASE_CODE}, continuing anyway"
fi
fi
echo "Checking for existing next-release branch..." echo "Checking for existing next-release branch..."
BRANCH_CHECK=$(curl -s -w "%{http_code}" -o /dev/null \ BRANCH_CHECK=$(curl -s --retry 3 --retry-delay 2 --retry-connrefused -w "%{http_code}" -o /dev/null \
-H "Authorization: token ${TOKEN}" \ -H "Authorization: token ${TOKEN}" \
"${API_URL}/branches/next-release") "${API_URL}/branches/next-release")
echo "Branch check HTTP status: ${BRANCH_CHECK}"
if [ "${BRANCH_CHECK}" = "200" ]; then if [ "${BRANCH_CHECK}" = "200" ]; then
BRANCH_EXISTS="true" echo "Deleting existing next-release branch..."
else curl -sf --retry 3 --retry-delay 2 --retry-connrefused -X DELETE \
BRANCH_EXISTS="false" -H "Authorization: token ${TOKEN}" \
"${API_URL}/branches/next-release"
echo "Branch deleted"
fi fi
echo "Branch exists: ${BRANCH_EXISTS}"
# Prepare CHANGELOG.md content # Prepare CHANGELOG.md content
CHANGELOG_CONTENT=$(base64 -w0 < CHANGELOG.md) CHANGELOG_CONTENT=$(base64 -w0 < CHANGELOG.md)
@@ -135,161 +118,122 @@ jobs:
VERSION_JSON=$(jq -n --arg v "${VERSION}" '{"version":$v}') VERSION_JSON=$(jq -n --arg v "${VERSION}" '{"version":$v}')
VERSION_CONTENT=$(echo "${VERSION_JSON}" | base64 -w0) VERSION_CONTENT=$(echo "${VERSION_JSON}" | base64 -w0)
if [ "${BRANCH_EXISTS}" = "true" ]; then echo "Creating new next-release branch from ${BASE_BRANCH}..."
echo "Updating existing next-release branch..."
# Get SHA of existing CHANGELOG.md # Check if CHANGELOG.md exists on base branch to determine create vs update
CHANGELOG_SHA=$(curl -sf \ CHANGELOG_SHA=$(curl -sf --retry 3 --retry-delay 2 --retry-connrefused \
-H "Authorization: token ${TOKEN}" \
"${API_URL}/contents/CHANGELOG.md?ref=${BASE_BRANCH}" | jq -r '.sha // empty')
if [ -n "${CHANGELOG_SHA}" ]; then
echo "Updating CHANGELOG.md (exists on ${BASE_BRANCH}) on new branch..."
RESPONSE=$(curl -s --retry 3 --retry-delay 2 --retry-connrefused -w "\n%{http_code}" -X PUT \
-H "Authorization: token ${TOKEN}" \ -H "Authorization: token ${TOKEN}" \
"${API_URL}/contents/CHANGELOG.md?ref=next-release" | jq -r '.sha // empty') -H "Content-Type: application/json" \
--data "$(jq -n \
# Update or create CHANGELOG.md --arg content "${CHANGELOG_CONTENT}" \
if [ -n "${CHANGELOG_SHA}" ]; then --arg sha "${CHANGELOG_SHA}" \
curl -sf -X PUT \ --arg message "${TITLE}" \
-H "Authorization: token ${TOKEN}" \ --arg branch "${BASE_BRANCH}" \
-H "Content-Type: application/json" \ --arg new_branch "next-release" \
--data "$(jq -n \ '{content: $content, sha: $sha, message: $message, branch: $branch, new_branch: $new_branch}')" \
--arg content "${CHANGELOG_CONTENT}" \ "${API_URL}/contents/CHANGELOG.md")
--arg sha "${CHANGELOG_SHA}" \
--arg message "${TITLE}" \
--arg branch "next-release" \
'{content: $content, sha: $sha, message: $message, branch: $branch}')" \
"${API_URL}/contents/CHANGELOG.md"
else
curl -sf -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
--data "$(jq -n \
--arg content "${CHANGELOG_CONTENT}" \
--arg message "${TITLE}" \
--arg branch "next-release" \
'{content: $content, message: $message, branch: $branch, new_branch: $branch}')" \
"${API_URL}/contents/CHANGELOG.md"
fi
# Get SHA of existing .version
VERSION_SHA=$(curl -sf \
-H "Authorization: token ${TOKEN}" \
"${API_URL}/contents/.version?ref=next-release" | jq -r '.sha // empty')
# Update or create .version
if [ -n "${VERSION_SHA}" ]; then
curl -sf -X PUT \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
--data "$(jq -n \
--arg content "${VERSION_CONTENT}" \
--arg sha "${VERSION_SHA}" \
--arg message "${TITLE}" \
--arg branch "next-release" \
'{content: $content, sha: $sha, message: $message, branch: $branch}')" \
"${API_URL}/contents/.version"
else
curl -sf -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
--data "$(jq -n \
--arg content "${VERSION_CONTENT}" \
--arg message "${TITLE}" \
--arg branch "next-release" \
'{content: $content, message: $message, branch: $branch}')" \
"${API_URL}/contents/.version"
fi
else else
echo "Creating new next-release branch from ${BASE_BRANCH}..." echo "Creating CHANGELOG.md on new branch..."
RESPONSE=$(curl -s --retry 3 --retry-delay 2 --retry-connrefused -w "\n%{http_code}" -X POST \
# Check if CHANGELOG.md exists on base branch to determine create vs update
CHANGELOG_SHA=$(curl -sf \
-H "Authorization: token ${TOKEN}" \ -H "Authorization: token ${TOKEN}" \
"${API_URL}/contents/CHANGELOG.md?ref=${BASE_BRANCH}" | jq -r '.sha // empty') -H "Content-Type: application/json" \
--data "$(jq -n \
--arg content "${CHANGELOG_CONTENT}" \
--arg message "${TITLE}" \
--arg branch "${BASE_BRANCH}" \
--arg new_branch "next-release" \
'{content: $content, message: $message, branch: $branch, new_branch: $new_branch}')" \
"${API_URL}/contents/CHANGELOG.md")
fi
HTTP_CODE=$(echo "${RESPONSE}" | tail -1)
BODY=$(echo "${RESPONSE}" | sed '$d')
if [ "${HTTP_CODE}" -ge 400 ]; then
echo "Error with CHANGELOG.md: ${BODY}"
exit 1
fi
if [ -n "${CHANGELOG_SHA}" ]; then # Check if .version exists on base branch
echo "Updating CHANGELOG.md (exists on ${BASE_BRANCH}) on new branch..." VERSION_SHA=$(curl -sf --retry 3 --retry-delay 2 --retry-connrefused \
RESPONSE=$(curl -s -w "\n%{http_code}" -X PUT \ -H "Authorization: token ${TOKEN}" \
-H "Authorization: token ${TOKEN}" \ "${API_URL}/contents/.version?ref=${BASE_BRANCH}" | jq -r '.sha // empty')
-H "Content-Type: application/json" \
--data "$(jq -n \ if [ -n "${VERSION_SHA}" ]; then
--arg content "${CHANGELOG_CONTENT}" \ echo "Updating .version on next-release branch..."
--arg sha "${CHANGELOG_SHA}" \ curl -sf --retry 3 --retry-delay 2 --retry-connrefused -X PUT \
--arg message "${TITLE}" \ -H "Authorization: token ${TOKEN}" \
--arg branch "${BASE_BRANCH}" \ -H "Content-Type: application/json" \
--arg new_branch "next-release" \ --data "$(jq -n \
'{content: $content, sha: $sha, message: $message, branch: $branch, new_branch: $new_branch}')" \ --arg content "${VERSION_CONTENT}" \
"${API_URL}/contents/CHANGELOG.md") --arg sha "${VERSION_SHA}" \
else --arg message "${TITLE}" \
echo "Creating CHANGELOG.md on new branch..." --arg branch "next-release" \
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ '{content: $content, sha: $sha, message: $message, branch: $branch}')" \
-H "Authorization: token ${TOKEN}" \ "${API_URL}/contents/.version"
-H "Content-Type: application/json" \ else
--data "$(jq -n \ echo "Creating .version on next-release branch..."
--arg content "${CHANGELOG_CONTENT}" \ curl -sf --retry 3 --retry-delay 2 --retry-connrefused -X POST \
--arg message "${TITLE}" \ -H "Authorization: token ${TOKEN}" \
--arg branch "${BASE_BRANCH}" \ -H "Content-Type: application/json" \
--arg new_branch "next-release" \ --data "$(jq -n \
'{content: $content, message: $message, branch: $branch, new_branch: $new_branch}')" \ --arg content "${VERSION_CONTENT}" \
"${API_URL}/contents/CHANGELOG.md") --arg message "${TITLE}" \
--arg branch "next-release" \
'{content: $content, message: $message, branch: $branch}')" \
"${API_URL}/contents/.version"
fi
echo "Creating new PR..."
echo "Waiting for next-release branch to be ready..."
for i in $(seq 1 10); do
BRANCH_STATUS=$(curl -s --retry 3 --retry-delay 2 --retry-connrefused \
-w "%{http_code}" -o /dev/null \
-H "Authorization: token ${TOKEN}" \
"${API_URL}/branches/next-release")
if [ "${BRANCH_STATUS}" = "200" ]; then
echo "Branch ready after ${i} attempt(s)"
break
fi fi
HTTP_CODE=$(echo "${RESPONSE}" | tail -1) if [ "${i}" = "10" ]; then
BODY=$(echo "${RESPONSE}" | sed '$d') echo "Branch next-release not found after 10 attempts, giving up"
if [ "${HTTP_CODE}" -ge 400 ]; then
echo "Error with CHANGELOG.md: ${BODY}"
exit 1 exit 1
fi fi
echo "Branch not ready yet (attempt ${i}/10), waiting..."
sleep 3
done
# Check if .version exists on base branch PR_DATA=$(jq -n \
VERSION_SHA=$(curl -sf \ --arg title "${TITLE}" \
--arg body "${DESCRIPTION}" \
--arg head "next-release" \
--arg base "${BASE_BRANCH}" \
'{title: $title, body: $body, head: $head, base: $base}')
for i in $(seq 1 5); do
RESPONSE=$(curl -s --retry 3 --retry-delay 2 --retry-connrefused \
-w "\n%{http_code}" -X POST \
-H "Authorization: token ${TOKEN}" \ -H "Authorization: token ${TOKEN}" \
"${API_URL}/contents/.version?ref=${BASE_BRANCH}" | jq -r '.sha // empty') -H "Content-Type: application/json" \
--data "${PR_DATA}" \
if [ -n "${VERSION_SHA}" ]; then "${API_URL}/pulls")
echo "Updating .version on next-release branch..." HTTP_CODE=$(echo "${RESPONSE}" | tail -1)
curl -sf -X PUT \ BODY=$(echo "${RESPONSE}" | sed '$d')
-H "Authorization: token ${TOKEN}" \ if [ "${HTTP_CODE}" -lt 400 ]; then
-H "Content-Type: application/json" \ echo "PR created successfully"
--data "$(jq -n \ break
--arg content "${VERSION_CONTENT}" \
--arg sha "${VERSION_SHA}" \
--arg message "${TITLE}" \
--arg branch "next-release" \
'{content: $content, sha: $sha, message: $message, branch: $branch}')" \
"${API_URL}/contents/.version"
else
echo "Creating .version on next-release branch..."
curl -sf -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
--data "$(jq -n \
--arg content "${VERSION_CONTENT}" \
--arg message "${TITLE}" \
--arg branch "next-release" \
'{content: $content, message: $message, branch: $branch}')" \
"${API_URL}/contents/.version"
fi fi
fi if [ "${i}" = "5" ]; then
echo "Error creating PR after 5 attempts (HTTP ${HTTP_CODE}): ${BODY}"
if [ -n "${PR_INDEX}" ]; then exit 1
echo "Updating existing PR #${PR_INDEX}..." fi
curl -sf -X PATCH \ echo "PR creation attempt ${i}/5 failed (HTTP ${HTTP_CODE}), retrying..."
-H "Authorization: token ${TOKEN}" \ sleep 3
-H "Content-Type: application/json" \ done
--data "$(jq -n \
--arg title "${TITLE}" \
--arg body "${DESCRIPTION}" \
'{title: $title, body: $body}')" \
"${API_URL}/pulls/${PR_INDEX}"
else
echo "Creating new PR..."
curl -sf -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
--data "$(jq -n \
--arg title "${TITLE}" \
--arg body "${DESCRIPTION}" \
--arg head "next-release" \
--arg base "${DEFAULT_BRANCH}" \
'{title: $title, body: $body, head: $head, base: $base}')" \
"${API_URL}/pulls"
fi
create-release: create-release:
name: Create Release name: Create Release
@@ -347,7 +291,7 @@ jobs:
MESSAGE=$(cat CHANGES.md) MESSAGE=$(cat CHANGES.md)
echo "Creating release ${VERSION}..." echo "Creating release ${VERSION}..."
curl -sf -X POST \ curl -sf --retry 3 --retry-delay 2 --retry-connrefused -X POST \
-H "Authorization: token ${TOKEN}" \ -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
--data "$(jq -n \ --data "$(jq -n \
@@ -409,7 +353,7 @@ jobs:
API_URL="${GITEA_URL}/api/v1/repos/${OWNER}/${REPO}" API_URL="${GITEA_URL}/api/v1/repos/${OWNER}/${REPO}"
echo "Creating tag ${VERSION}..." echo "Creating tag ${VERSION}..."
curl -sf -X POST \ curl -sf --retry 3 --retry-delay 2 --retry-connrefused -X POST \
-H "Authorization: token ${TOKEN}" \ -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
--data "$(jq -n \ --data "$(jq -n \