Context
actions/mkdocs-deploy deploys docs by running mike deploy --push <alias> against the shared gh-pages
branch. All three trigger paths (pull_request / main / release) do a plain mike deploy --push with no
fetch-rebase-retry around the push. Surfaced repeatedly by a consumer (serapeum-org/pyramids), where the
Deploy MkDocs workflow intermittently fails and a manual re-run fixes it.
Problem / Current Behaviour
mike deploy --push fetches gh-pages, builds the version commit on that tip, and pushes — all in one step,
with no recovery if the remote advanced in between. If anything pushes to gh-pages in the short window
between mike's fetch and mike's push, the push is a non-fast-forward and the whole job fails:
error: failed to push branch gh-pages to origin:
! [rejected] gh-pages -> gh-pages (fetch first)
hint: Updates were rejected because the remote contains work that you do not have locally
Because there is no retry, a single lost race kills the deploy and the maintainer must re-run it by hand (the
re-run succeeds because it fetches a now-current tip). Consumers work around it by serializing every deploy of
the workflow through one global concurrency lane, but that only prevents this workflow's runs from racing
each other — it does not protect against an out-of-band gh-pages push (e.g. a history squash to reclaim
clone size) landing in the fetch→push window, and it forces a single lane that cancels other PRs' pending
deploys. A rebase/retry here is what makes the deploy resilient and lets consumers relax that lane.
Affected locations
| File |
Symbol |
Notes |
actions/mkdocs-deploy/action.yml |
Deploy to GitHub Pages (Pull Request) (lines 199-212) |
mike deploy --push develop, no retry |
actions/mkdocs-deploy/action.yml |
Deploy to GitHub Pages (Main) (lines 214-229) |
mike deploy --push main + set-default, no retry |
actions/mkdocs-deploy/action.yml |
Deploy to GitHub Pages (Release) (lines 231-245) |
mike deploy --push --update-aliases ..., no retry |
Steps to Reproduce
1. Trigger two mkdocs deploys close together, or push anything to gh-pages (e.g. a history squash) while a
deploy is between mike's fetch and its push.
2. The losing deploy fails with `! [rejected] gh-pages -> gh-pages (fetch first)`.
3. Re-run the failed job manually -> it succeeds (it fetched a current tip).
Observed in serapeum-org/pyramids Deploy MkDocs run 29649210655 (job failed at the deploy step with the
rejection above; a squash commit landed on gh-pages inside the fetch->push window).
Proposed Solution
Wrap each mike deploy --push in a fetch-reset-retry loop so a lost push self-heals instead of failing.
Reset the local gh-pages to the current remote tip and let mike re-apply the version (mike is idempotent, so
there are no versions.json conflicts to rebase), then push; retry a few times with backoff:
MIKE="mike"; [ "$PACKAGE_MANAGER" != "pip" ] && MIKE="$PACKAGE_MANAGER run mike"
for attempt in 1 2 3 4 5; do
git fetch origin gh-pages && git branch -f gh-pages FETCH_HEAD 2>/dev/null || true
if $MIKE deploy --push develop; then exit 0; fi
echo "::warning::gh-pages push rejected (attempt $attempt) - refetching and retrying"
sleep $((attempt * 3))
done
echo "::error::mike deploy --push failed after retries"; exit 1
Apply the same wrapper to the main and release steps (including the set-default --push, which is subject to
the same race). A shared helper/composite step is preferable to duplicating the loop three times.
Out of Scope
- The consumer-side
concurrency configuration (each caller owns that; once this retries, callers can move
from a single global lane to a per-ref group).
- Any change to how notebooks are executed or cached.
Effort Estimate
Size: S — a retry wrapper around the three existing deploy steps; the loop logic is small but touches all
three paths and warrants a quick manual verification.
Definition of Done
Context
actions/mkdocs-deploydeploys docs by runningmike deploy --push <alias>against the sharedgh-pagesbranch. All three trigger paths (pull_request / main / release) do a plain
mike deploy --pushwith nofetch-rebase-retry around the push. Surfaced repeatedly by a consumer (
serapeum-org/pyramids), where theDeploy MkDocs workflow intermittently fails and a manual re-run fixes it.
Problem / Current Behaviour
mike deploy --pushfetchesgh-pages, builds the version commit on that tip, and pushes — all in one step,with no recovery if the remote advanced in between. If anything pushes to
gh-pagesin the short windowbetween mike's fetch and mike's push, the push is a non-fast-forward and the whole job fails:
Because there is no retry, a single lost race kills the deploy and the maintainer must re-run it by hand (the
re-run succeeds because it fetches a now-current tip). Consumers work around it by serializing every deploy of
the workflow through one global
concurrencylane, but that only prevents this workflow's runs from racingeach other — it does not protect against an out-of-band
gh-pagespush (e.g. a history squash to reclaimclone size) landing in the fetch→push window, and it forces a single lane that cancels other PRs' pending
deploys. A rebase/retry here is what makes the deploy resilient and lets consumers relax that lane.
Affected locations
actions/mkdocs-deploy/action.ymlDeploy to GitHub Pages (Pull Request)(lines 199-212)mike deploy --push develop, no retryactions/mkdocs-deploy/action.ymlDeploy to GitHub Pages (Main)(lines 214-229)mike deploy --push main+set-default, no retryactions/mkdocs-deploy/action.ymlDeploy to GitHub Pages (Release)(lines 231-245)mike deploy --push --update-aliases ..., no retrySteps to Reproduce
Observed in
serapeum-org/pyramidsDeploy MkDocs run 29649210655 (job failed at the deploy step with therejection above; a squash commit landed on
gh-pagesinside the fetch->push window).Proposed Solution
Wrap each
mike deploy --pushin a fetch-reset-retry loop so a lost push self-heals instead of failing.Reset the local
gh-pagesto the current remote tip and let mike re-apply the version (mike is idempotent, sothere are no
versions.jsonconflicts to rebase), then push; retry a few times with backoff:Apply the same wrapper to the main and release steps (including the
set-default --push, which is subject tothe same race). A shared helper/composite step is preferable to duplicating the loop three times.
Out of Scope
concurrencyconfiguration (each caller owns that; once this retries, callers can movefrom a single global lane to a per-ref group).
Effort Estimate
Size:
S— a retry wrapper around the three existing deploy steps; the loop logic is small but touches allthree paths and warrants a quick manual verification.
Definition of Done
pull_request/main/release) retriesmike deploy --push(andset-default)on a non-fast-forward, refetching
gh-pagesbetween attempts.gh-pagescommit (no behaviour change on thehappy path).