Skip to content

Commit c4378e7

Browse files
committed
Merge remote-tracking branch 'origin/main' into seed/python_by_example
2 parents 6ee7130 + 37a22cd commit c4378e7

92 files changed

Lines changed: 13652 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
name: Build HTML Preview for PR
2+
on:
3+
pull_request:
4+
types: [opened, synchronize, reopened]
5+
pull_request_target:
6+
types: [closed]
7+
8+
# Ported from QuantEcon.manual, which is the only repo in the fleet serving
9+
# /pr-N/ previews off GitHub Pages (the other translations deploy previews to
10+
# Netlify, which needs a Netlify site and a NETLIFY_SITE_ID secret this repo
11+
# does not have). Every job that writes to gh-pages shares one concurrency
12+
# group with publish.yml and reap-previews.yml: peaceiris/actions-gh-pages
13+
# force-pushes from a SHA read at job start, so two unserialised writers
14+
# silently erase each other.
15+
#
16+
# Note the group only bounds the damage, it does not eliminate it: GitHub
17+
# evicts an already-PENDING run when a newer one queues into the group, so a
18+
# burst of merges can still drop a cleanup. That is what reap-previews.yml is
19+
# for. Keep the locked section as short as possible so the window stays small.
20+
jobs:
21+
build-preview:
22+
if: github.event.action != 'closed'
23+
runs-on: ubuntu-latest
24+
steps:
25+
# No `ref:` — deliberately different from QuantEcon.manual, which pins
26+
# the PR head. The seed/* translation branches were cut from an empty
27+
# main and carry only `lectures/<name>.md`; the build scaffold lives on
28+
# main. Checking out the default merge ref gives us PR content plus the
29+
# scaffold, and previews the post-merge state, which is what a reviewer
30+
# wants to see.
31+
- name: Checkout (PR merge ref)
32+
uses: actions/checkout@v4
33+
34+
- name: Setup Anaconda
35+
uses: conda-incubator/setup-miniconda@v3
36+
with:
37+
auto-update-conda: true
38+
auto-activate-base: true
39+
miniconda-version: 'latest'
40+
python-version: "3.13"
41+
environment-file: environment.yml
42+
activate-environment: quantecon
43+
44+
- name: Display Conda Environment Versions
45+
shell: bash -l {0}
46+
run: conda list
47+
48+
# `shell: bash -l {0}` is a custom shell spec, so GitHub does NOT inject
49+
# `-eo pipefail`. Without the explicit set, a step ending in anything
50+
# after the build reports that command's status and masks a failed build
51+
# — this produced months of green-on-broken CI in the zh-cn edition.
52+
- name: Prune TOC to translated lectures
53+
shell: bash -l {0}
54+
run: |
55+
set -eo pipefail
56+
python scripts/prune_toc.py lectures/_toc.yml
57+
58+
# No -n -W: a partial translation has unresolved cross-references into
59+
# lectures that do not exist yet (as of this commit: writing_good_code,
60+
# python_advanced_features, scipy, getting_started, oop_intro,
61+
# need_for_speed, and the labels pyess_ex2 and oop_ex1). Those must stay
62+
# warnings until the translation is complete. The fa and fr editions
63+
# relax the same flag for the same reason.
64+
- name: Build HTML
65+
shell: bash -l {0}
66+
run: |
67+
set -eo pipefail
68+
jb build lectures --path-output ./ --keep-going
69+
70+
- name: Upload Execution Reports
71+
uses: actions/upload-artifact@v4
72+
if: failure()
73+
with:
74+
name: execution-reports
75+
path: _build/html/reports
76+
77+
- name: Upload preview artifact
78+
uses: actions/upload-artifact@v4
79+
with:
80+
name: html-preview
81+
path: _build/html/
82+
83+
deploy-preview:
84+
needs: build-preview
85+
# Fork PRs get the build as a status check but no deploy: the
86+
# pull_request GITHUB_TOKEN is read-only for forks, so the push would fail
87+
# anyway, and a fork build should never hold the gh-pages lock.
88+
if: github.event.pull_request.head.repo.full_name == github.repository
89+
runs-on: ubuntu-latest
90+
permissions:
91+
contents: write
92+
pull-requests: write
93+
concurrency:
94+
group: gh-pages
95+
cancel-in-progress: false
96+
steps:
97+
- name: Download preview artifact
98+
uses: actions/download-artifact@v4
99+
with:
100+
name: html-preview
101+
path: _build/html/
102+
103+
- name: Deploy Preview
104+
uses: peaceiris/actions-gh-pages@v4
105+
with:
106+
github_token: ${{ secrets.GITHUB_TOKEN }}
107+
publish_dir: _build/html/
108+
destination_dir: pr-${{ github.event.number }}
109+
# The `cname` input is deliberately OMITTED, not set to false.
110+
# QuantEcon.manual passes `cname: false` intending "no CNAME", but
111+
# YAML stringifies that to "false" and peaceiris writes a CNAME file
112+
# containing the literal text `false` at the gh-pages ROOT — which
113+
# GitHub then reads as a custom domain and the whole site 404s. It is
114+
# masked there because publish.yml overwrites the root CNAME with the
115+
# real domain; this edition has no custom domain, so nothing would
116+
# ever correct it. Omitting the input writes no CNAME at all.
117+
force_orphan: false
118+
119+
- name: Comment PR
120+
uses: actions/github-script@v7
121+
if: success()
122+
with:
123+
script: |
124+
const prNumber = context.payload.pull_request.number;
125+
const base = `https://quantecon.github.io/lecture-python-programming.ml/pr-${prNumber}`;
126+
const commitSha = context.payload.pull_request.head.sha.substring(0, 7);
127+
128+
// Deep-link the lectures this PR translates, so the reviewer lands
129+
// on the rendered Malayalam page rather than the landing page.
130+
const files = await github.paginate(github.rest.pulls.listFiles, {
131+
owner: context.repo.owner,
132+
repo: context.repo.repo,
133+
pull_number: prNumber,
134+
});
135+
136+
const pages = files
137+
.filter(f => f.status !== 'removed')
138+
.map(f => f.filename)
139+
.filter(f => f.startsWith('lectures/') && f.endsWith('.md'))
140+
// skip underscore files (_static, _admonition) — not built pages
141+
.filter(f => !f.split('/').some(part => part.startsWith('_')))
142+
.map(f => {
143+
const rel = f.replace(/^lectures\//, '').replace(/\.md$/, '.html');
144+
// intro.md is the TOC root, served at the preview root
145+
const href = rel === 'intro.html' ? `${base}/` : `${base}/${rel}`;
146+
return `- [${rel}](${href})`;
147+
});
148+
149+
let body = `📖 **HTML build** - [view preview](${base}/) (${commitSha})`;
150+
if (pages.length > 0) {
151+
body += `\n\n**Translated pages in this PR:**\n${pages.join('\n')}`;
152+
}
153+
154+
await github.rest.issues.createComment({
155+
owner: context.repo.owner,
156+
repo: context.repo.repo,
157+
issue_number: prNumber,
158+
body: body
159+
});
160+
161+
cleanup-preview:
162+
if: github.event.action == 'closed'
163+
runs-on: ubuntu-latest
164+
permissions:
165+
contents: write
166+
concurrency:
167+
group: gh-pages
168+
cancel-in-progress: false
169+
steps:
170+
# pull_request_target, not pull_request: a pull_request-triggered job on
171+
# a closed fork PR gets a read-only token. This is safe only because the
172+
# job checks out gh-pages and never executes PR-authored code — do not
173+
# add a PR checkout or a build step here.
174+
- name: Checkout gh-pages
175+
uses: actions/checkout@v4
176+
with:
177+
ref: gh-pages
178+
token: ${{ secrets.GITHUB_TOKEN }}
179+
fetch-depth: 1
180+
181+
- name: Remove PR preview directory
182+
run: |
183+
set -eo pipefail
184+
PR_DIR="pr-${{ github.event.number }}"
185+
if [ ! -d "$PR_DIR" ]; then
186+
echo "Preview directory $PR_DIR not found — nothing to clean up"
187+
exit 0
188+
fi
189+
rm -rf "$PR_DIR"
190+
git config user.name "github-actions[bot]"
191+
git config user.email \
192+
"41898282+github-actions[bot]@users.noreply.github.com"
193+
git add .
194+
if git diff --staged --quiet; then
195+
echo "No changes to commit"
196+
exit 0
197+
fi
198+
git commit -m "Remove preview for closed PR #${{ github.event.number }}"
199+
for i in 1 2 3; do
200+
if git push; then
201+
echo "Successfully pushed cleanup changes"
202+
exit 0
203+
fi
204+
echo "Push attempt $i failed, rebasing and retrying..."
205+
git pull --rebase origin gh-pages
206+
sleep 5
207+
done
208+
echo "Failed to push cleanup after 3 attempts"
209+
exit 1
210+
211+
# Check the pushed branch, not the working tree — the local rm -rf
212+
# always succeeds, so only origin/gh-pages proves the cleanup landed.
213+
- name: Verify cleanup completion
214+
run: |
215+
set -eo pipefail
216+
PR_DIR="pr-${{ github.event.number }}"
217+
git fetch origin gh-pages
218+
if git ls-tree -d --name-only origin/gh-pages "$PR_DIR" | grep -q .; then
219+
echo "❌ Cleanup failed: $PR_DIR still exists on origin/gh-pages"
220+
exit 1
221+
fi
222+
echo "✅ Cleanup verified: $PR_DIR is absent from origin/gh-pages"

.github/workflows/publish.yml

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
name: Build & Publish to GH Pages
2+
on:
3+
push:
4+
branches:
5+
- main
6+
7+
# Split into a lock-free build and a short locked deploy, which is where this
8+
# diverges from QuantEcon.manual. There the concurrency group wraps the whole
9+
# publish job, so the gh-pages lock is held across the conda solve and the
10+
# Jupyter Book build (~2 minutes). GitHub evicts an already-PENDING run when a
11+
# newer one queues into a group, so that long hold evicts queued cleanups: a
12+
# merge train on 2026-08-02 dropped two runs in 68 seconds and left an orphan
13+
# preview live on that site. Locking only the deploy keeps the window seconds
14+
# long. reap-previews.yml covers what still slips through.
15+
jobs:
16+
build:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v4
21+
22+
- name: Setup Anaconda
23+
uses: conda-incubator/setup-miniconda@v3
24+
with:
25+
auto-update-conda: true
26+
auto-activate-base: true
27+
miniconda-version: 'latest'
28+
python-version: "3.13"
29+
environment-file: environment.yml
30+
activate-environment: quantecon
31+
32+
- name: Display Conda Environment Versions
33+
shell: bash -l {0}
34+
run: conda list
35+
36+
# See ci.yml for why `set -eo pipefail` is explicit and why the TOC is
37+
# pruned at build time rather than maintained in the repo.
38+
- name: Prune TOC to translated lectures
39+
shell: bash -l {0}
40+
run: |
41+
set -eo pipefail
42+
python scripts/prune_toc.py lectures/_toc.yml
43+
44+
- name: Build HTML
45+
shell: bash -l {0}
46+
run: |
47+
set -eo pipefail
48+
jb build lectures --path-output ./ --keep-going
49+
50+
- name: Upload Execution Reports
51+
uses: actions/upload-artifact@v4
52+
if: failure()
53+
with:
54+
name: execution-reports
55+
path: _build/html/reports
56+
57+
- name: Upload site artifact
58+
uses: actions/upload-artifact@v4
59+
with:
60+
name: html-site
61+
path: _build/html/
62+
63+
deploy:
64+
needs: build
65+
runs-on: ubuntu-latest
66+
permissions:
67+
contents: write
68+
concurrency:
69+
group: gh-pages
70+
cancel-in-progress: false
71+
steps:
72+
- name: Download site artifact
73+
uses: actions/download-artifact@v4
74+
with:
75+
name: html-site
76+
path: _build/html/
77+
78+
- name: Deploy with Preview Preservation
79+
uses: peaceiris/actions-gh-pages@v4
80+
with:
81+
github_token: ${{ secrets.GITHUB_TOKEN }}
82+
publish_dir: _build/html/
83+
# keep_files is mandatory: without it every push to main wipes the
84+
# live pr-N preview directories out from under open reviews.
85+
keep_files: true
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
name: Reap Orphaned PR Previews
2+
on:
3+
schedule:
4+
# Weekly, Monday 04:00 UTC — after any weekend merge activity has settled.
5+
- cron: '0 4 * * 1'
6+
workflow_dispatch:
7+
8+
# ci.yml deletes a preview when its PR closes, but that cleanup can be lost:
9+
# GitHub evicts an already-PENDING run when a newer one queues into the shared
10+
# gh-pages concurrency group, and cleanup only ever fires on the close event,
11+
# so nothing retries it. QuantEcon.manual has no equivalent to this job and
12+
# consequently serves preview directories for PRs merged months ago. This
13+
# reconciles gh-pages against the set of open PRs and deletes the strays.
14+
jobs:
15+
reap:
16+
runs-on: ubuntu-latest
17+
permissions:
18+
contents: write
19+
pull-requests: read
20+
concurrency:
21+
group: gh-pages
22+
cancel-in-progress: false
23+
steps:
24+
- name: Checkout gh-pages
25+
uses: actions/checkout@v4
26+
with:
27+
ref: gh-pages
28+
token: ${{ secrets.GITHUB_TOKEN }}
29+
fetch-depth: 1
30+
31+
- name: Remove previews for PRs that are no longer open
32+
env:
33+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
34+
run: |
35+
set -eo pipefail
36+
37+
open_prs=$(gh pr list --repo "$GITHUB_REPOSITORY" \
38+
--state open --limit 500 --json number --jq '.[].number')
39+
echo "Open PRs: ${open_prs:-none}"
40+
41+
reaped=0
42+
while IFS= read -r dir; do
43+
[ -n "$dir" ] || continue
44+
number="${dir#./pr-}"
45+
if grep -qx "$number" <<<"$open_prs"; then
46+
echo "keep $dir (PR #$number is open)"
47+
else
48+
echo "reap $dir (PR #$number is not open)"
49+
rm -rf "$dir"
50+
reaped=$((reaped + 1))
51+
fi
52+
done < <(find . -maxdepth 1 -type d -name 'pr-*')
53+
54+
if [ "$reaped" -eq 0 ]; then
55+
echo "No orphaned previews found"
56+
exit 0
57+
fi
58+
59+
git config user.name "github-actions[bot]"
60+
git config user.email \
61+
"41898282+github-actions[bot]@users.noreply.github.com"
62+
git add .
63+
if git diff --staged --quiet; then
64+
echo "Nothing staged after reaping $reaped director(ies)"
65+
exit 0
66+
fi
67+
git commit -m "Reap $reaped orphaned PR preview(s)"
68+
for i in 1 2 3; do
69+
if git push; then
70+
echo "Reaped $reaped orphaned preview(s)"
71+
exit 0
72+
fi
73+
echo "Push attempt $i failed, rebasing and retrying..."
74+
git pull --rebase origin gh-pages
75+
sleep 5
76+
done
77+
echo "Failed to push after 3 attempts"
78+
exit 1

.translate/config.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ source-language: en
22
target-language: ml
33
docs-folder: lectures
44
tool-version: 0.24.0
5+
editors:
6+
primary: adisankarmt
7+
secondary: []

0 commit comments

Comments
 (0)