Skip to content
Open
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
4 changes: 2 additions & 2 deletions deploy-github-pages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Note: snapshots ignore the VERSION parameter and always publish to the `snapshot

## Push Retry Logic

When multiple branches build concurrently, their documentation deployments may race to push to the same documentation branch. To handle this, the action includes automatic retry logic: if a push is rejected (e.g., because another build pushed first), it will pull remote changes with rebase and retry, up to 5 attempts. If all attempts fail, the action exits with an error.
When publishers race to update an existing documentation branch, the action makes up to five push attempts, including the initial attempt. After a retryable non-fast-forward rejection, it fetches the new remote tip, requires that tip to be a strict descendant of the previously observed remote tip, rebases the unpublished local deployment in the existing checkout, and attempts another normal fast-forward push. The action never force-pushes. Rebase conflicts, fetched retry tips that fail the descendant check, concurrent creation of a previously missing documentation branch, and non-contention push failures stop the deployment without changing the remote branch.

## Requirements
If using the default `GITHUB_TOKEN`, this action requires permission `contents: write`. Otherwise, the provided `GH_TOKEN` must be able to commit to the documentation branch.
Expand Down Expand Up @@ -71,4 +71,4 @@ Release Usage:
GRADLE_PUBLISH_RELEASE: 'true'
SOURCE_FOLDER: build/docs
VERSION: ${{ needs.publish.outputs.release_version }}
```
```
41 changes: 35 additions & 6 deletions deploy-github-pages/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ set_value_or_error() {
exit 1
fi

if [[ -n "$value" ]]; then
if [[ -n "$value" ]]; then
decidedValue="$value"
else
echo "${variableName}: Using default value: ${defaultValue}"
Expand Down Expand Up @@ -101,7 +101,7 @@ publish_artifacts() {

is_highest_version() {
local new_folder="$1" # e.g. "7.0.x"

# Strip the trailing ".x" → "7.0", then parse into major/minor
local new_major new_minor
local folder_no_x="${new_folder%.x}" # "7.0"
Expand Down Expand Up @@ -207,6 +207,7 @@ if git ls-remote --heads "${GIT_REPO_URL}" "${DOCUMENTATION_BRANCH}" | grep -q "
echo "documentation branch found, cloning"
git clone "${GIT_REPO_URL}" "${DOCUMENTATION_BRANCH}" --branch "${DOCUMENTATION_BRANCH}" --single-branch --depth 1
cd ${DOCUMENTATION_BRANCH}
LAST_REMOTE_TIP="$(git rev-parse HEAD)"
echo "::endgroup::"
else
echo "::group::Creating documentation branch"
Expand All @@ -216,6 +217,7 @@ else
git init
git checkout -b "${DOCUMENTATION_BRANCH}"
git remote add origin "${GIT_REPO_URL}"
LAST_REMOTE_TIP=""
echo "::endgroup::"
fi

Expand Down Expand Up @@ -285,7 +287,7 @@ else
echo "Published release documentation to ${genericVersionFolder}"
echo "::endgroup::"

# Publish to the latest release folder if needed
# Publish to the latest release folder if needed
if [[ "$SKIP_RELEASE_FOLDER" == "false" ]]; then
if is_highest_version "${genericVersionFolder}"; then
echo "::group::Overwriting ${LAST_RELEASE_FOLDER} with the latest release documentation"
Expand Down Expand Up @@ -321,16 +323,43 @@ MAX_PUSH_ATTEMPTS=5
PUSH_ATTEMPT=1
while [ $PUSH_ATTEMPT -le $MAX_PUSH_ATTEMPTS ]; do
echo "Push attempt ${PUSH_ATTEMPT}/${MAX_PUSH_ATTEMPTS}"
if git push "${GIT_REPO_URL}" "${DOCUMENTATION_BRANCH}" 2>&1; then
PUSH_EXIT=0
PUSH_OUTPUT="$(LC_ALL=C git push --porcelain "${GIT_REPO_URL}" "HEAD:refs/heads/${DOCUMENTATION_BRANCH}" 2>&1)" || PUSH_EXIT=$?
printf '%s\n' "${PUSH_OUTPUT}"
if [ $PUSH_EXIT -eq 0 ]; then
echo "Deployment successful!"
break
fi
if ! grep -Eq $'^!\t.*\t\[rejected\] \((non-fast-forward|fetch first)\)$' <<< "${PUSH_OUTPUT}"; then
echo "ERROR: Push failed without a retryable non-fast-forward rejection." >&2
exit 1
fi
if [ -z "${LAST_REMOTE_TIP}" ]; then
echo "ERROR: Documentation branch was created by another publisher before the first push." >&2
exit 1
fi
if [ $PUSH_ATTEMPT -eq $MAX_PUSH_ATTEMPTS ]; then
echo "ERROR: Push failed after ${MAX_PUSH_ATTEMPTS} attempts." >&2
exit 1
fi
echo "Push rejected, pulling remote changes and retrying..."
git pull --rebase "${GIT_REPO_URL}" "${DOCUMENTATION_BRANCH}"
echo "Push rejected by a concurrent publisher, rebasing and retrying..."
if ! git fetch --no-tags "${GIT_REPO_URL}" "refs/heads/${DOCUMENTATION_BRANCH}"; then
echo "ERROR: Failed to fetch the concurrent documentation branch update." >&2
exit 1
fi
NEW_REMOTE_TIP="$(git rev-parse FETCH_HEAD)"
if [[ "${NEW_REMOTE_TIP}" == "${LAST_REMOTE_TIP}" ]] || ! git merge-base --is-ancestor "${LAST_REMOTE_TIP}" "${NEW_REMOTE_TIP}"; then
echo "ERROR: Concurrent documentation branch update is not a descendant of the observed remote tip." >&2
exit 1
fi
if ! git rebase --onto "${NEW_REMOTE_TIP}" "${LAST_REMOTE_TIP}"; then
echo "ERROR: Rebase failed; aborting without changing the remote branch." >&2
git rebase --abort
exit 1
fi
LAST_REMOTE_TIP="${NEW_REMOTE_TIP}"
PUSH_ATTEMPT=$((PUSH_ATTEMPT + 1))
BACKOFF_SECONDS=$((1 << (PUSH_ATTEMPT - 2)))
sleep $((BACKOFF_SECONDS + RANDOM % (BACKOFF_SECONDS + 1)))
done
echo "::endgroup::"
Loading
Loading