If your docs already publish to a gh-pages branch (the keep_files model), this
moves you onto the reconstruct-from-durable-sources model in a single pipeline PR,
without losing any served version, and with an instant rollback until the very last
step. It is two local migrate.sh runs, with your pipeline PR doing the deploy in
between:
migrate.sh ORG/REPO— backfill releases, seed the default branch, flip the Pages source to Actions + open the environment policy. Uploads and flips only — no deploy.Open + merge your pipeline PR — its CI runs the first publish, which reads the seed and persists the default branch durably into the site.
migrate.sh ORG/REPO --delete-gh-pages— verify the live site, then deletegh-pages+ the seed release.
What you are migrating onto (read this first)¶
Every deploy reconstructs the whole site from sources with very different durability — and the safety of the migration hinges on that difference (see the architecture explanation):
| version | source under the new model | durable? |
|---|---|---|
| released tags | a docs.zip asset on each GitHub Release | yes, permanent — but only once attached |
default branch (main) | persisted each deploy into the site at _sources/<branch>.zip | yes — once one deploy has captured it (seeded in run 1) |
open PRs (pr-<n>) | each PR’s build artifact | no — drops on merge/close |
Two consequences drive the procedure:
Your old releases are not durable yet. Their docs exist only as directories on
gh-pages; the matching Releases have nodocs.zip. So run 1 includes a one-time backfill ofdocs.ziponto those Releases — not just a config flip. (A tag with agh-pagesdirectory but no Release at all gets one created with thedocs.zip— covering repos that tag without releasing, and fork rehearsals, since forking copies tags but not Releases.)The default branch has no permanent source until a publish captures it. Each deploy persists the branch’s
docs.zipinto the site (_sources/<branch>.zip) and restores from there when the CI artifact expires — but before the first publish there is nothing in the site. So run 1 seeds it: it captures the gh-pages<default>/tree as a published seed release, which the first publish reads and persists to_sources/<branch>.zip. After that the in-site copy carries/<default>/— before the default branch ever builds docs itself.
For why this ordering is safe — the non-destructive source flip and the seed-before-publish rule — see the architecture explanation.
Optional: rehearse on a fork first¶
To de-risk the real migration, run the whole thing on a fork before touching the
upstream site. A fork copies the repo’s gh-pages branch and tags — but not the
Releases — so the backfill creates a Release (with the docs.zip) for every tag
that has a gh-pages directory. The migration therefore has the same inputs and keeps
every version, and it deploys to your github.io, never upstream’s.
Fork the repo and enable Actions on the fork (the Actions tab → enable workflows; forks start with Actions disabled). You have admin on your own fork, which is what the Pages-source flip in step 2 needs.
Run
migrate.shagainst the fork — dry-run first, then for real:scripts/migrate.sh FORKORG/REPO --dry-run scripts/migrate.sh FORKORG/REPOPoint the publish guard at your fork. The pipeline’s
publishjob is gatedif: github.repository == 'ORG/REPO'so only the canonical repo deploys — on a fork that is false, so nothing publishes. On your pipeline branch, comment that line out (or set it toFORKORG/REPO) so the fork deploys.Open and merge the pipeline PR on the fork, working on the fork’s
main. Its CI runs the first publish and deploys tohttps://FORKORG.github.io/REPO/— open that and check the site and switcher.When it works, undo the trial change and go upstream. Restore the guard to
github.repository == 'ORG/REPO'(uncomment / set it back) and open the real PR against upstream, then follow the steps below on the upstream repo.
Before you start¶
Run the script from the myst-version-switcher-plugin devcontainer (it needs
gh,zip, andnode, which the devcontainer provides).Authenticate
ghwith repo-admin on the target repo — rungh auth loginif needed (flipping the Pages source and setting the environment policy need admin; a CI token can’t — which is why this is a local script).If the script detects it is running in a clone of the target repo it will use that, otherwise it will clone the repo itself.
Step 1 — dry-run (recommended)¶
scripts/migrate.sh ORG/REPO --dry-runIt prints the backfill + seed plan, then a drop report: every version the live
site’s switcher.json currently lists that the new model will not serve (not the
default branch, not an open-PR preview, and no tag + gh-pages directory to backfill
from or existing docs.zip Release). Dropped versions stay on gh-pages until
finalize, so they are recoverable — cut a real Release (or restore the tag) for any you
want to keep before proceeding. It ends by probing the current site; nothing is
uploaded and nothing is flipped.
Step 2 — prepare (uploads + flips; no deploy)¶
scripts/migrate.sh ORG/REPOIt does the following, then stops with gh-pages intact and still serving:
Backfill (non-destructive, idempotent). For each tag that is a
gh-pagesdirectory: zip that directory (barehtml/root) and attach it asdocs.zipto its Release — or, if no Release exists for the tag, create one with thedocs.zip(flagged prerelease fora/b/rctags). Tags containing/are skipped.Seed the default branch. Capture the gh-pages
<default>/tree (or the--seed-from <dir>directory, if the old site published it under another name) as the publishedpages-default-seedrelease, so the default branch is durable before any publish.Flip the Pages source → GitHub Actions and open the
github-pagesenvironment’sdeployment_branch_policyto “no restriction” (so deploys from PR/tag refs — which run under the nested-publish model — aren’t rejected by the environment). The flip is non-destructive; the site keeps serving the lastgh-pagesdeployment until step 3 publishes.
No deploy is triggered here — that is your pipeline PR’s job.
Step 3 — publish, via your pipeline PR¶
Prepare the new pipeline + myst.yml changes from the
tutorial on a branch, and open the PR only
after run 1 has seeded the default branch (so its first publish is safe). Then open
and merge it: its CI runs the first publish — with the source on Actions, the seed
present, and the env policy open, it reconstructs the whole site
(default branch from the seed, the backfilled releases, any open PRs) and deploys it,
persisting _sources/<default>.zip into the published site. Merging then has the
default branch build its own docs, so _sources/<default>.zip refreshes with real
content and the seed becomes redundant.
Confirm the site is live on the new model (visit https://ORG.github.io/REPO/ and the
switcher) before finalizing.
Step 4 — finalize (irreversible)¶
Once the publish has deployed and _sources/<default>.zip is live:
scripts/migrate.sh ORG/REPO --delete-gh-pages(Or run it right after merging with --wait: the guard then polls until the durable
copy goes live — up to 30 minutes — instead of failing immediately.)
This guards the deletion: it refuses unless
https://ORG.github.io/REPO/_sources/<default>.zip returns 200 — i.e. the deployed
site holds a durable copy of the default branch, so the new model can reconstruct
/<default>/ without gh-pages. It then verifies the live site, asks you to type the
repo name, deletes gh-pages, and deletes the seed release (the in-site _sources
copy supersedes it). After this, the rollback is gone.
Rollback¶
Between run 1 (step 2) and the deletion (step 4), gh-pages is no longer served but
still exists — your rollback. If anything is wrong, flip the Pages source back to
Deploy from a branch / gh-pages and serving is restored instantly, with nothing
lost. That is exactly why the deletion is a separate, gated run.
Flags¶
| flag | effect |
|---|---|
--dry-run | Print the backfill + seed plan and the drop report + probe the current site only; upload nothing; skip the flip. |
--delete-gh-pages | The only mode that deletes. Guard that _sources/<default>.zip is live (200), verify the live site, then delete gh-pages and the seed release. |
--pages-ref <ref> | gh-pages ref to read (default origin/gh-pages). |
--seed-from <dir> | gh-pages directory to seed the default branch from, when the old site published it under a different name (e.g. latest/). |
--yes | Skip the typed confirmation before deleting gh-pages (with --delete-gh-pages; use with care). |
--wait | With --delete-gh-pages: poll (up to 30 min) until _sources/<default>.zip goes live instead of failing the guard immediately — lets you finalize right after merging the pipeline PR. |