11name : Build Project [using jupyter-book]
22on :
33 pull_request :
4+ types : [opened, synchronize, reopened]
45 workflow_dispatch :
6+ inputs :
7+ preview_page :
8+ description : ' Specific page to preview (e.g., aiyagari.html)'
9+ required : false
10+ type : string
511jobs :
612 preview :
713 runs-on : " runs-on=${{ github.run_id }}/family=g4dn.2xlarge/image=quantecon_ubuntu2404/disk=large"
814 permissions :
915 issues : write
1016 pull-requests : write
1117 steps :
12- - uses : actions/checkout@v4
18+ - uses : actions/checkout@v5
1319 with :
1420 ref : ${{ github.event.pull_request.head.sha }}
21+ fetch-depth : 0
1522 - name : Setup Anaconda
1623 uses : conda-incubator/setup-miniconda@v3
1724 with :
1825 auto-update-conda : true
1926 auto-activate-base : true
2027 miniconda-version : ' latest'
21- python-version : " 3.12 "
28+ python-version : " 3.13 "
2229 environment-file : environment.yml
2330 activate-environment : quantecon
2431 - name : Install JAX, Numpyro, PyTorch
5663 uses : actions/upload-artifact@v4
5764 if : failure()
5865 with :
59- name : execution-reports
66+ name : execution-reports-notebooks
6067 path : _build/jupyter/reports
6168 - name : Build PDF from LaTeX
6269 shell : bash -l {0}
7683 run : |
7784 jb build lectures --path-output ./ -n -W --keep-going
7885 - name : Check for Python warnings
79- uses : QuantEcon/meta/.github/actions/ check-warnings@main
86+ uses : QuantEcon/action- check-warnings@v1.0.0
8087 with :
8188 html-path : ' ./_build/html'
8289 fail-on-warning : ' false'
@@ -89,13 +96,255 @@ jobs:
8996 with :
9097 name : execution-reports
9198 path : _build/html/reports
99+ - name : Install Node.js and Netlify CLI
100+ shell : bash -l {0}
101+ run : |
102+ # Install Node.js via system package manager since conda-forge doesn't have npm
103+ sudo apt-get update
104+ sudo apt-get install -y nodejs npm
105+ sudo npm install -g netlify-cli
106+ - name : Detect Changed Lecture Files
107+ id : detect-changes
108+ shell : bash -l {0}
109+ run : |
110+ if [ "${{ github.event_name }}" = "pull_request" ]; then
111+ echo "Detecting changed lecture files..."
112+ echo "Base SHA: ${{ github.event.pull_request.base.sha }}"
113+ echo "Head SHA: ${{ github.event.pull_request.head.sha }}"
114+
115+ # Ensure we have both base and head commits available
116+ git fetch origin ${{ github.event.pull_request.base.sha }}:refs/remotes/origin/pr-base || true
117+ git fetch origin ${{ github.event.pull_request.head.sha }}:refs/remotes/origin/pr-head || true
118+
119+ # Get changed files using git diff with status to see the type of change
120+ echo "Getting diff between commits..."
121+ all_changed=$(git diff --name-status ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} 2>/dev/null || echo "")
122+
123+ if [ -z "$all_changed" ]; then
124+ echo "No changes detected or error in git diff"
125+ echo "changed_files=" >> $GITHUB_OUTPUT
126+ exit 0
127+ fi
128+
129+ echo "All changed files with status:"
130+ echo "$all_changed"
131+
132+ # Filter for lecture files that are Added or Modified (not Deleted)
133+ # Format: M lectures/file.md or A lectures/file.md
134+ changed_lecture_files=""
135+ while IFS=$'\t' read -r status file; do
136+ # Skip if empty line
137+ [ -z "$status" ] && continue
138+
139+ echo "Processing: status='$status' file='$file'"
140+
141+ # Only include Added (A) or Modified (M) files, skip Deleted (D)
142+ if [[ "$status" =~ ^[AM] ]] && [[ "$file" =~ ^lectures/.*\.md$ ]] && [[ ! "$file" =~ ^lectures/_ ]] && [[ "$file" != "lectures/intro.md" ]]; then
143+ # Double-check that the file exists and has real content changes
144+ if [ -f "$file" ]; then
145+ # Use git show to check if there are actual content changes (not just metadata)
146+ content_diff=$(git diff ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} -- "$file" | grep -E '^[+-]' | grep -v '^[+-]{3}' | wc -l)
147+ if [ "$content_diff" -gt 0 ]; then
148+ echo "✓ Confirmed content changes in: $file"
149+ if [ -z "$changed_lecture_files" ]; then
150+ changed_lecture_files="$file"
151+ else
152+ changed_lecture_files="$changed_lecture_files"$'\n'"$file"
153+ fi
154+ else
155+ echo "⚠ No content changes found in: $file (possibly metadata only)"
156+ fi
157+ else
158+ echo "⚠ File not found in working directory: $file"
159+ fi
160+ else
161+ echo "⚠ Skipping: $file (status: $status, doesn't match lecture file pattern or is excluded)"
162+ fi
163+ done <<< "$all_changed"
164+
165+ if [ ! -z "$changed_lecture_files" ]; then
166+ echo ""
167+ echo "Final validated changed lecture files:"
168+ echo "$changed_lecture_files"
169+ echo "changed_files<<EOF" >> $GITHUB_OUTPUT
170+ echo "$changed_lecture_files" >> $GITHUB_OUTPUT
171+ echo "EOF" >> $GITHUB_OUTPUT
172+ else
173+ echo "No lecture files with actual content changes found"
174+ echo "changed_files=" >> $GITHUB_OUTPUT
175+ fi
176+ else
177+ echo "Not a PR, skipping change detection"
178+ echo "changed_files=" >> $GITHUB_OUTPUT
179+ fi
92180 - name : Preview Deploy to Netlify
93- uses : nwtgck/actions-netlify@v3
94- with :
95- publish-dir : ' _build/html/'
96- production-branch : main
97- github-token : ${{ secrets.GITHUB_TOKEN }}
98- deploy-message : " Preview Deploy from GitHub Actions"
181+ id : netlify-deploy
182+ shell : bash -l {0}
183+ run : |
184+ if [ "${{ github.event_name }}" = "pull_request" ]; then
185+ # Deploy to Netlify and capture the response
186+ deploy_message="Preview Deploy from GitHub Actions PR #${{ github.event.pull_request.number }} (commit: ${{ github.event.pull_request.head.sha }})"
187+
188+ netlify_output=$(netlify deploy \
189+ --dir _build/html/ \
190+ --site ${{ secrets.NETLIFY_SITE_ID }} \
191+ --auth ${{ secrets.NETLIFY_AUTH_TOKEN }} \
192+ --context pr-preview \
193+ --alias pr-${{ github.event.pull_request.number }} \
194+ --message "${deploy_message}" \
195+ --json)
196+
197+ echo "Netlify deployment output:"
198+ echo "$netlify_output"
199+
200+ # Extract the actual deploy URL from the JSON response
201+ deploy_url=$(echo "$netlify_output" | jq -r '.deploy_url')
202+
203+ echo "deploy_url=$deploy_url" >> $GITHUB_OUTPUT
204+ echo "✅ Deployment completed!"
205+ echo "🌐 Actual Deploy URL: $deploy_url"
206+
207+ # Generate preview URLs for changed files using the actual deploy URL
208+ if [ ! -z "${{ steps.detect-changes.outputs.changed_files }}" ]; then
209+ echo ""
210+ echo "📚 Direct links to changed lecture pages:"
211+ while read -r file; do
212+ if [ ! -z "$file" ]; then
213+ basename=$(basename "$file" .md)
214+ html_file="${basename}.html"
215+ echo "- ${basename}: ${deploy_url}/${html_file}"
216+ fi
217+ done <<< "${{ steps.detect-changes.outputs.changed_files }}"
218+ fi
219+
220+ # Display manual preview page if specified
221+ if [ ! -z "${{ github.event.inputs.preview_page }}" ]; then
222+ echo ""
223+ echo "🎯 Manual preview page: ${deploy_url}/${{ github.event.inputs.preview_page }}"
224+ fi
225+ else
226+ # Handle manual deployment
227+ deploy_message="Manual Deploy from GitHub Actions (commit: ${{ github.sha }})"
228+
229+ netlify_output=$(netlify deploy \
230+ --dir _build/html/ \
231+ --site ${{ secrets.NETLIFY_SITE_ID }} \
232+ --auth ${{ secrets.NETLIFY_AUTH_TOKEN }} \
233+ --alias manual-${{ github.run_id }} \
234+ --context dev \
235+ --message "${deploy_message}" \
236+ --json)
237+
238+ echo "Netlify deployment output:"
239+ echo "$netlify_output"
240+
241+ # Extract the actual deploy URL from the JSON response
242+ deploy_url=$(echo "$netlify_output" | jq -r '.deploy_url')
243+
244+ echo "deploy_url=$deploy_url" >> $GITHUB_OUTPUT
245+ echo "✅ Manual deployment completed!"
246+ echo "🌐 Actual Deploy URL: $deploy_url"
247+
248+ if [ ! -z "${{ github.event.inputs.preview_page }}" ]; then
249+ echo "🎯 Preview page: ${deploy_url}/${{ github.event.inputs.preview_page }}"
250+ fi
251+ fi
99252 env :
100253 NETLIFY_AUTH_TOKEN : ${{ secrets.NETLIFY_AUTH_TOKEN }}
101254 NETLIFY_SITE_ID : ${{ secrets.NETLIFY_SITE_ID }}
255+ - name : Post PR Comment with Preview Links
256+ if : github.event_name == 'pull_request'
257+ uses : actions/github-script@v7
258+ with :
259+ script : |
260+ const changedFiles = `${{ steps.detect-changes.outputs.changed_files }}`;
261+ const manualPage = `${{ github.event.inputs.preview_page }}`;
262+ const deployUrl = `${{ steps.netlify-deploy.outputs.deploy_url }}`;
263+ const prNumber = ${{ github.event.pull_request.number }};
264+ const commitSha = `${{ github.event.pull_request.head.sha }}`;
265+ const shortSha = commitSha.substring(0, 7);
266+
267+ console.log(`Checking for existing comments for commit: ${commitSha}`);
268+ console.log(`Deploy URL: ${deployUrl}`);
269+ console.log(`Changed files: ${changedFiles}`);
270+
271+ // Get all comments on this PR to check for duplicates
272+ const comments = await github.rest.issues.listComments({
273+ issue_number: prNumber,
274+ owner: context.repo.owner,
275+ repo: context.repo.repo,
276+ });
277+
278+ console.log(`Found ${comments.data.length} comments on PR`);
279+
280+ // Look for existing comment with this exact commit SHA and deploy URL
281+ const duplicateComment = comments.data.find(comment => {
282+ const hasMarker = comment.body.includes('**📖 Netlify Preview Ready!**');
283+ const hasCommitSha = comment.body.includes(`([${shortSha}]`);
284+ const hasDeployUrl = comment.body.includes(deployUrl);
285+
286+ console.log(`Comment ${comment.id}: hasMarker=${hasMarker}, hasCommitSha=${hasCommitSha}, hasDeployUrl=${hasDeployUrl}`);
287+
288+ return hasMarker && hasCommitSha && hasDeployUrl;
289+ });
290+
291+ if (duplicateComment) {
292+ console.log(`Duplicate comment found (${duplicateComment.id}) for commit ${shortSha} and deploy URL ${deployUrl}, skipping...`);
293+ return;
294+ }
295+
296+ console.log(`No duplicate found, creating new comment for commit ${shortSha}`);
297+
298+ const commitUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${commitSha}`;
299+
300+ let comment = `**📖 Netlify Preview Ready!**\n\n`;
301+ comment += `**Preview URL:** ${deployUrl} ([${shortSha}](${commitUrl}))\n\n`;
302+
303+ // Add manual preview page if specified
304+ if (manualPage) {
305+ comment += `🎯 **Manual Preview:** [${manualPage}](${deployUrl}/${manualPage})\n\n`;
306+ }
307+
308+ // Add direct links to changed lecture pages
309+ if (changedFiles && changedFiles.trim()) {
310+ console.log('Processing changed files for preview links...');
311+ const files = changedFiles.split('\n').filter(f => f.trim() && f.includes('lectures/') && f.endsWith('.md'));
312+ console.log('Filtered lecture files:', files);
313+
314+ if (files.length > 0) {
315+ comment += `📚 **Changed Lecture Pages:** `;
316+
317+ const pageLinks = [];
318+ for (const file of files) {
319+ const cleanFile = file.trim();
320+ if (cleanFile && cleanFile.startsWith('lectures/') && cleanFile.endsWith('.md')) {
321+ const fileName = cleanFile.replace('lectures/', '').replace('.md', '');
322+ console.log(`Creating preview link: ${cleanFile} -> ${fileName}.html`);
323+ const pageUrl = `${deployUrl}/${fileName}.html`;
324+ pageLinks.push(`[${fileName}](${pageUrl})`);
325+ }
326+ }
327+
328+ if (pageLinks.length > 0) {
329+ comment += pageLinks.join(', ') + '\n\n';
330+ } else {
331+ console.log('No valid page links created');
332+ }
333+ } else {
334+ console.log('No lecture files in changed files list');
335+ }
336+ } else {
337+ console.log('No changed files detected');
338+ }
339+
340+ console.log('Final comment:', comment);
341+
342+ // Post the comment
343+ await github.rest.issues.createComment({
344+ issue_number: prNumber,
345+ owner: context.repo.owner,
346+ repo: context.repo.repo,
347+ body: comment
348+ });
349+
350+ console.log('Comment posted successfully');
0 commit comments