You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
GitHub Pages Deployment Strategy: Analysis and Migration Guide
Prepared for QuantEcon Infrastructure Team Date: January 2026 Updated: March 2026 (with lessons from lecture-python-programming migration) Related Issues: #261
This report compares two approaches to GitHub Pages deployment:
Traditional gh-pages branch method (currently used by lecture-python.myst)
Modern artifact-based deployment with deployment environments (used by 2026-tom-course)
Decision: We recommend migrating to artifact-based deployment for all QuantEcon lecture repositories.
Key reasons:
Eliminates repository bloat from accumulated deployment history
Maintains fast clone times regardless of deployment frequency
Uses official GitHub-maintained actions with first-party support
Deployed site persists indefinitely (artifacts expiring does NOT affect the live site)
Migration impact: Zero downtime when following the documented procedure. The gh-pages branch and all its history can be safely deleted after migration, reclaiming significant repository space.
Proven results:lecture-python-programming migrated on 2026-03-26 — repo size reduced from 358 MB to 15 MB (96% reduction).
2. Background
2.1 Current State
lecture-python.myst uses the traditional approach:
peaceiris/actions-gh-pages@v4 pushes built HTML to a gh-pages branch
GitHub Pages serves content from this branch
Each deployment creates a new commit, accumulating history
Repository: 727+ commits on main, 182 releases
2026-tom-course uses the modern approach:
actions/upload-pages-artifact and actions/deploy-pages
Content deployed via GitHub's deployment environment system
No persistent branch; artifacts stored separately
Clean repository structure
2.2 The Problem with gh-pages Branches
The gh-pages branch bloat is a well-documented issue across the GitHub ecosystem:
Repository
Reported Size
Cause
Mozilla VPN Client
1.5 GB+
WASM binaries in gh-pages history
Eclipse Theia
2.6 GB
API documentation history
Scratch GUI
2 GB+
Built JavaScript bundles
lecture-python-programming
358 MB
Jupyter Book HTML deploy snapshots (153 commits)
For lecture repositories with:
Large built outputs (Jupyter Book HTML, CSS, JS)
Frequent updates
Binary files that don't delta compress well
This problem compounds over time, affecting every contributor's clone operation.
3. Deployment Approaches Compared
3.1 How Each Approach Works
gh-pages Branch Method:
Build → Commit to gh-pages branch → GitHub detects change → Serves content
↓
Accumulates in git history forever
Artifact-Based Method:
Build → Upload artifact → Deploy to Pages infrastructure → Serves content
↓ ↓
Expires after 90 days Persists indefinitely
(configurable) (until replaced)
Important: Artifact expiration does NOT affect the live site.
Workflow artifacts (downloadable from Actions tab) expire after retention period
Deployed site content persists indefinitely in GitHub Pages infrastructure
Once actions/deploy-pages succeeds, content is copied to GitHub's hosting. The original artifact is just a staging mechanism—its expiration has no effect on the live site.
You could deploy once and never touch the repository for years—the site would remain live.
4. Pros and Cons Analysis
4.1 gh-pages Branch Method
Advantages:
Complete deployment history preserved in git
Instant rollback via git operations (git reset, git revert)
Simpler workflow configuration (single action)
Well-documented with extensive community examples
Can inspect deployed files directly in branch
Disadvantages:
Repository size bloat: Each deployment adds commits; large sites grow to GB+
Clone time degradation: All contributors download gh-pages history
Binary files compress poorly: Built JS bundles, images don't delta well
Requires periodic maintenance: Force-pushing or orphan branches needed
Third-party action dependency
4.2 Artifact-Based Deployment
Advantages:
Zero repository bloat: Artifacts stored separately, expire automatically
Fast clones forever: Repository size constant regardless of deployment frequency
First-party GitHub support: Official actions maintained by GitHub
Critical: Follow this exact order to avoid downtime.
Step 1: Update publish.yml (replace peaceiris with native deploy steps)
+ MIGRATE linkcheck.yml off any `ref: gh-pages` checkout (same PR)
↓
Step 2: Merge workflow changes to main
↓
Step 3: Change Pages source to "GitHub Actions" in Settings → Pages
↓
Step 4: Configure environment protection rules for publish tags ← IMPORTANT
↓
Step 5: Run new workflow (push a publish tag) → deploys via artifacts
↓
Step 6: Verify site works correctly (grep live HTML for fresh content, not just HTTP 200)
↓
Step 7: Confirm dependent workflows (linkcheck) run against the release asset
+ confirm an off-repo backup covers the repo before deleting
↓
Step 8: Delete gh-pages branch (record its SHA first; reclaims space)
Grep the workflows first. Before starting, run grep -rn 'gh-pages' .github/workflows/ in the target repo. Any ref: gh-pages checkout (commonly in linkcheck.yml) is a hard blocker for Step 8 and must be migrated in Step 1's PR — point it at the latest release .tar.gz instead of the branch. See the July 2026 lessons at the end of this issue.
7.3 Step-by-Step Instructions
Step 1: Update Workflow File
Replace the peaceiris/actions-gh-pages deployment step with the native Pages steps. Key changes:
Add workflow-level permissions:
permissions:
contents: write # write if also uploading release assets; read otherwiseactions: read # needed if other workflows download artifactspages: writeid-token: write # required for OIDC-based Pages deployment
Note on single vs multi-job workflows: The guide in Section 9.1 shows a two-job pattern (build + deploy). For repositories using GPU runners (like lecture-python-programming), a single-job workflow avoids transferring large build artifacts between jobs. Choose the pattern that fits your runner setup.
Step 2: Merge Workflow Changes
Create a PR, get review, merge to main.
Step 3: Change GitHub Pages Source Setting
Go to repository Settings → Pages
Under "Build and deployment":
Change Source from "Deploy from a branch" to "GitHub Actions"
Click Save
Note: This creates a github-pages environment automatically.
Step 4: Configure Environment Protection Rules
⚠️ This step is critical. The auto-created github-pages environment only allows the main branch by default. If your workflow triggers on tags (e.g., publish*), the deploy will fail with: "Tag is not allowed to deploy to github-pages due to environment protection rules."
Go to Settings → Environments → github-pages
Under "Deployment branches and tags", click "Add deployment branch or tag rule"
Select Tag (not Branch)
Enter pattern: publish*
Save
After configuration, you should see: "1 branch and N tags allowed"
Step 5: Test New Workflow
git checkout main
git pull origin main
git tag publish-YYYY-MMM-DD-test
git push origin publish-YYYY-MMM-DD-test
This ensures the CNAME is included in every artifact upload. The Settings → Pages custom domain setting alone is not sufficient since each artifact deployment overwrites the Pages content.
8. Post-Migration Procedures
8.1 Verify Migration Success
# Check deployment source
gh api repos/:owner/:repo/pages --jq '.build_type'# Should return: workflow# Check site is live
curl -sI https://your-domain.quantecon.org/ | head -5
# Verify no gh-pages branch
git fetch --prune
git branch -r | grep gh-pages # Should return nothing# Check fresh clone sizecd /tmp && git clone https://github.com/QuantEcon/repo.git size-check
du -sh size-check/.git
8.2 Monitor Repository Size
GitHub runs garbage collection periodically. Size reduction is typically immediate for fresh clones but the API-reported size may take up to 24 hours to update.
# Check repository size via API
gh api repos/:owner/:repo --jq '.size'# Compare with fresh clone
git clone --bare https://github.com/QuantEcon/repo.git /tmp/bare-check
du -sh /tmp/bare-check
8.3 Rollback If Needed
If migration fails and gh-pages still exists:
Settings → Pages → Source → "Deploy from a branch"
Select gh-pages
Site immediately serves from gh-pages again
If gh-pages already deleted:
Keep/restore old workflow with peaceiris/actions-gh-pages
Change Pages source to "Deploy from a branch"
Run old workflow (recreates gh-pages branch)
For lecture-python-programming specifically:
Rollback commit documented in #500: fc6487c (March 20, 2026 deploy)
Final recommendation: Proceed with migration for all lecture repositories. Use lecture-python-programming (PR #499) as the reference implementation.
Lessons Learned from lecture-python-programming Migration
Environment protection rules are critical — the github-pages environment auto-creates with only main branch allowed. You must add a tag rule (not branch) for publish* before triggering the first deploy.
Use release assets for linkcheck, not Pages artifacts — release tarballs are permanent and don't expire, making them more reliable for scheduled link checking.
CNAME must be in the build output — add it as a build step before upload-pages-artifact, since each deploy overwrites the full Pages content.
Single-job works for GPU runners — avoid the two-job (build+deploy) pattern when using expensive GPU runners to prevent large artifact transfers between jobs.
GitHub GC is fast — after deleting gh-pages, a fresh clone immediately reflected the reduced size (15 MB vs 358 MB). No need to contact GitHub Support for manual GC.
Lessons Learned from the July 2026 wave (lecture-jax, lecture-python-advanced.myst, lecture-python.myst)
linkcheck.yml often depends on the gh-pages branch — it must be migrated, not just "verified". Both lecture-python-advanced.myst and lecture-python.myst had a linkcheck workflow that did actions/checkout with ref: gh-pages. This is a hard blocker for branch deletion. The fix is to point linkcheck at the latest release .tar.gz instead: read the asset URL with gh api repos/${{ github.repository }}/releases/latest, download it, and extract into _site. Do this in the same PR as the publish.yml change so the branch can be deleted immediately after cutover.
Don't blindly swap a tuned link checker for the family default.lecture-jax and lecture-python-advanced.myst moved to QuantEcon/action-link-checker. But lecture-python.myst had lychee args carefully tuned to suppress specific false positives (--accept 202, --exclude-path for genindex/search/prf-prf, a Journal-of-Derivatives DOI exclude — see #906, #933). Swapping engines there would have regressed that work, because action-link-checker has no direct equivalent for path/URL excludes. Decision: keep lychee, change only the input source (gh-pages checkout → extracted release archive; repoint --root-dir/glob at _site), and track the eventual engine upgrade separately (linkcheck: upgrade to QuantEcon/action-link-checker (preserve #906/#933 tuning) lecture-python.myst#954).
Extract with an explicit tar -xzf -. When piping the release tarball from curl into tar, pass -f - explicitly rather than relying on GNU tar's compiled-in default device. It works on ubuntu-latest either way, but the explicit form removes ambiguity and is portable (Copilot review of #953; backported to #355 and #786).
Confirm an off-repo backup exists before deleting gh-pages. For each repo we verified the QuantEcon/workflow-backups S3 backup covered it (a recent, "uploaded and verified" tarball) and recorded the gh-pages tip SHA in the deletion step, so the branch is recoverable two ways. Note the exclude list in that repo's config.yml is exact-name — lecture-python excludes only the deprecated repo, not lecture-python.myst.
Record the gh-pages SHA at deletion. Every deletion captured the branch tip (e.g. lecture-python.myst was 0f2fac38…) in the command output before git push origin --delete, so it can be restored if ever needed.
API-reported repo size lags up to 24h — measure with a fresh bare clone.gh api repos/<r> --jq .size still showed the pre-deletion size immediately after deleting gh-pages; a git clone --bare reflected the true post-deletion size right away (e.g. lecture-python.myst: API still ~4.5 GB vs fresh clone ~14 MB).
spot=false was already set on the GPU runners. A preview build failed once mid-run on lecture-jax with a runner shutdown; because spot=false was already configured (matching lecture-python.myst), this was a transient on-demand instance blip, not a spot reclaim — it cleared on re-run. Don't chase a spot-instance fix that's already in place.
The publish tag re-deploys current main, not just the migration commit. The first post-migration publish serves everything merged since the previous publish. Validate by grepping the live rendered HTML for a distinctive string from a content PR in that range, not just an HTTP 200.
Migration Log — what was actually done
Durable per-repo record (details in the linked completion comments on this issue).
Repo
Date
Deleted gh-pages SHA
linkcheck handling
Size (fresh clone)
Record
lecture-jax
2026-07-08
3b0f9dbf…
n/a (didn't check out gh-pages); uses QuantEcon/action-link-checker
GitHub Pages Deployment Strategy: Analysis and Migration Guide
Prepared for QuantEcon Infrastructure Team
Date: January 2026
Updated: March 2026 (with lessons from lecture-python-programming migration)
Related Issues: #261
Migration Status Tracker
lecture-python-programminglecture-python.mystlecture-julia.mystlecture-datascience.mystlecture-python-introlecture-python-advanced.mystlecture-dplecture-jaxcontinuous_time_mcsTable of Contents
1. Executive Summary
This report compares two approaches to GitHub Pages deployment:
Decision: We recommend migrating to artifact-based deployment for all QuantEcon lecture repositories.
Key reasons:
Migration impact: Zero downtime when following the documented procedure. The gh-pages branch and all its history can be safely deleted after migration, reclaiming significant repository space.
Proven results:
lecture-python-programmingmigrated on 2026-03-26 — repo size reduced from 358 MB to 15 MB (96% reduction).2. Background
2.1 Current State
lecture-python.myst uses the traditional approach:
peaceiris/actions-gh-pages@v4pushes built HTML to agh-pagesbranch2026-tom-course uses the modern approach:
actions/upload-pages-artifactandactions/deploy-pages2.2 The Problem with gh-pages Branches
The gh-pages branch bloat is a well-documented issue across the GitHub ecosystem:
For lecture repositories with:
This problem compounds over time, affecting every contributor's clone operation.
3. Deployment Approaches Compared
3.1 How Each Approach Works
gh-pages Branch Method:
Artifact-Based Method:
3.2 Detailed Comparison
3.3 Critical Clarification: Artifact Expiration
Important: Artifact expiration does NOT affect the live site.
Once
actions/deploy-pagessucceeds, content is copied to GitHub's hosting. The original artifact is just a staging mechanism—its expiration has no effect on the live site.You could deploy once and never touch the repository for years—the site would remain live.
4. Pros and Cons Analysis
4.1 gh-pages Branch Method
Advantages:
git reset,git revert)Disadvantages:
4.2 Artifact-Based Deployment
Advantages:
Disadvantages:
5. Recommendation
5.1 Decision
We recommend adopting artifact-based deployment as the standard for all QuantEcon lecture repositories.
5.2 Justification
Sustainable infrastructure: Repository size remains constant regardless of how many times you deploy
Better contributor experience: New contributors always get fast clones
Official support: First-party GitHub actions ensure long-term maintenance and compatibility
The trade-off is acceptable: Limited deployment history is mitigated by:
Clean break: Deleting gh-pages branch removes all accumulated bloat permanently
Proven results:
lecture-python-programmingachieved 96% size reduction (358 MB → 15 MB)5.3 Implementation Strategy
Phase 1 — New Projects:
Phase 2 — Existing Projects:
lecture-python-programming(March 2026)6. Technical Deep Dive
6.1 Rollback Strategies
With gh-pages Branch (Old Method)
With Artifact Deployment (New Method)
Option A: Re-run Previous Workflow
Option B: Deploy from Tag
# Trigger workflow on specific tag gh workflow run deploy.yml --ref publish-2025dec15Option C: Dedicated Rollback Workflow
See Section 9.2 for complete workflow file.
# Rollback to specific run's artifact gh workflow run rollback.yml -f run_id=123456786.2 Can We Safely Delete gh-pages After Migration?
Yes. Once migrated to artifact-based deployment:
The only thing you lose: The ability to instantly rollback via git. But you retain:
7. Migration Guide
7.1 Pre-Migration Checklist
Document current configuration:
python.quantecon.org)7.2 Migration Sequence
Critical: Follow this exact order to avoid downtime.
7.3 Step-by-Step Instructions
Step 1: Update Workflow File
Replace the
peaceiris/actions-gh-pagesdeployment step with the native Pages steps. Key changes:Add workflow-level permissions:
Add concurrency group:
Add environment to the job:
Replace the deploy step:
Step 2: Merge Workflow Changes
Create a PR, get review, merge to main.
Step 3: Change GitHub Pages Source Setting
Note: This creates a
github-pagesenvironment automatically.Step 4: Configure Environment Protection Rules
publish*After configuration, you should see: "1 branch and N tags allowed"
Step 5: Test New Workflow
Monitor at:
https://github.com/QuantEcon/<repo>/actionsStep 6: Verify Site
github-pagesenvironment shows the deploymenthttps://python-programming.quantecon.org/)build_type: workflow:Step 7: Verify Dependent Workflows
If you have a linkcheck or other workflow that previously checked out
gh-pages:workflow_dispatch)Step 8: Delete gh-pages Branch
Via Command Line:
Notify team members to clean up their local clones:
7.4 Custom Domain Handling
If you have a custom domain (e.g.,
python.quantecon.org):Recommended: Include CNAME in Build Output
This ensures the CNAME is included in every artifact upload. The Settings → Pages custom domain setting alone is not sufficient since each artifact deployment overwrites the Pages content.
8. Post-Migration Procedures
8.1 Verify Migration Success
8.2 Monitor Repository Size
GitHub runs garbage collection periodically. Size reduction is typically immediate for fresh clones but the API-reported size may take up to 24 hours to update.
8.3 Rollback If Needed
If migration fails and gh-pages still exists:
If gh-pages already deleted:
peaceiris/actions-gh-pagesFor
lecture-python-programmingspecifically:Rollback commit documented in #500:
fc6487c(March 20, 2026 deploy)9. Appendix: Complete Workflow Examples
9.1 Full Production Workflow
Adapted for QuantEcon lecture repositories:
9.2 Rollback Workflow
Save as
.github/workflows/rollback.yml:Usage:
Summary
lecture-python-programming: 358 MB → 15 MBFinal recommendation: Proceed with migration for all lecture repositories. Use
lecture-python-programming(PR #499) as the reference implementation.Lessons Learned from lecture-python-programming Migration
github-pagesenvironment auto-creates with onlymainbranch allowed. You must add a tag rule (not branch) forpublish*before triggering the first deploy.upload-pages-artifact, since each deploy overwrites the full Pages content.Lessons Learned from the July 2026 wave (lecture-jax, lecture-python-advanced.myst, lecture-python.myst)
linkcheck.ymloften depends on the gh-pages branch — it must be migrated, not just "verified". Bothlecture-python-advanced.mystandlecture-python.mysthad a linkcheck workflow that didactions/checkoutwithref: gh-pages. This is a hard blocker for branch deletion. The fix is to point linkcheck at the latest release.tar.gzinstead: read the asset URL withgh api repos/${{ github.repository }}/releases/latest, download it, and extract into_site. Do this in the same PR as the publish.yml change so the branch can be deleted immediately after cutover.lecture-jaxandlecture-python-advanced.mystmoved toQuantEcon/action-link-checker. Butlecture-python.mysthad lychee args carefully tuned to suppress specific false positives (--accept 202,--exclude-pathfor genindex/search/prf-prf, a Journal-of-Derivatives DOI exclude — see #906, #933). Swapping engines there would have regressed that work, becauseaction-link-checkerhas no direct equivalent for path/URL excludes. Decision: keep lychee, change only the input source (gh-pages checkout → extracted release archive; repoint--root-dir/glob at_site), and track the eventual engine upgrade separately (linkcheck: upgrade to QuantEcon/action-link-checker (preserve #906/#933 tuning) lecture-python.myst#954).tar -xzf -. When piping the release tarball fromcurlintotar, pass-f -explicitly rather than relying on GNU tar's compiled-in default device. It works onubuntu-latesteither way, but the explicit form removes ambiguity and is portable (Copilot review of #953; backported to #355 and #786).QuantEcon/workflow-backupsS3 backup covered it (a recent, "uploaded and verified" tarball) and recorded the gh-pages tip SHA in the deletion step, so the branch is recoverable two ways. Note the exclude list in that repo'sconfig.ymlis exact-name —lecture-pythonexcludes only the deprecated repo, notlecture-python.myst.lecture-python.mystwas0f2fac38…) in the command output beforegit push origin --delete, so it can be restored if ever needed.gh api repos/<r> --jq .sizestill showed the pre-deletion size immediately after deleting gh-pages; agit clone --barereflected the true post-deletion size right away (e.g.lecture-python.myst: API still ~4.5 GB vs fresh clone ~14 MB).spot=falsewas already set on the GPU runners. A preview build failed once mid-run onlecture-jaxwith a runner shutdown; becausespot=falsewas already configured (matchinglecture-python.myst), this was a transient on-demand instance blip, not a spot reclaim — it cleared on re-run. Don't chase a spot-instance fix that's already in place.main, not just the migration commit. The first post-migration publish serves everything merged since the previous publish. Validate by grepping the live rendered HTML for a distinctive string from a content PR in that range, not just an HTTP 200.Migration Log — what was actually done
Durable per-repo record (details in the linked completion comments on this issue).
lecture-jax3b0f9dbf…QuantEcon/action-link-checkerlecture-python-advanced.myst7d599225…QuantEcon/action-link-checker; removedlychee.tomllecture-python.myst0f2fac38…Cross-repo hardening from this wave: the explicit
tar -xzf -extract fix was backported to QuantEcon/lecture-python-advanced.myst#355 and QuantEcon/lecture-python-intro#786.