Harden GitHub Pages demo deployment for stable latest-build links #90
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | |
| name: Build Wolfenstein 3D | |
| on: | |
| - push | |
| - pull_request | |
| permissions: | |
| contents: write | |
| pages: write | |
| id-token: write | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| RETAINED_GH_PAGES_BUILDS: 3 | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Install dosbox-x and xvfb | |
| run: | | |
| sudo apt-get install -y xvfb | |
| sudo snap install dosbox-x | |
| - name: Run DosBox to build Wolf3D | |
| run: | | |
| export TERM=dumb | |
| xvfb-run --auto-servernum dosbox-x -silent -debug -conf dosbox.conf -c "mount c . && c: && call autoexec.bat" -log-con -exit | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: build-dos | |
| path: WOLF3D | |
| - name: Upload console log | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: dosbox-console-log | |
| path: dosbox_console_log.txt | |
| if-no-files-found: ignore | |
| generate-html: | |
| runs-on: ubuntu-latest | |
| needs: build | |
| steps: | |
| - name: Download build artifact | |
| uses: actions/download-artifact@v4.1.3 | |
| with: | |
| name: build-dos | |
| - name: List files in artifact directory | |
| run: ls -R . | |
| - name: Zip the build output | |
| run: | | |
| zip -r ./wolf3d.zip . | |
| - name: Base64 encode the build output | |
| run: | | |
| base64 ./wolf3d.zip > wolf3d_base64.txt | |
| - name: Create HTML file | |
| run: | | |
| echo '<!DOCTYPE html>' > wolf3d.html | |
| echo '<html>' >> wolf3d.html | |
| echo '<head>' >> wolf3d.html | |
| echo '<script src="https://js-dos.com/6.22/current/js-dos.js"></script>' >> wolf3d.html | |
| echo '</head>' >> wolf3d.html | |
| echo '<body>' >> wolf3d.html | |
| echo '<h1>Wolfenstein 3D</h1>' >> wolf3d.html | |
| echo '<div id="jsdos" style="width: 640px; height: 400px;"></div>' >> wolf3d.html | |
| echo '<script>' >> wolf3d.html | |
| echo 'async function initDosBox() {' >> wolf3d.html | |
| echo ' const dos = await Dos(ctx, { wdosboxUrl: "https://js-dos.com/6.22/current/wdosbox.js" });' >> wolf3d.html | |
| echo ' const zipFile = `' >> wolf3d.html | |
| cat wolf3d_base64.txt >> wolf3d.html | |
| echo '`;' >> wolf3d.html | |
| echo ' const zipBlob = await fetch("data:application/zip;base64," + zipFile).then(r => r.blob());' >> wolf3d.html | |
| echo ' await dos.fs.extract(zipBlob);' >> wolf3d.html | |
| echo ' dos.run("WOLF3D.EXE");' >> wolf3d.html | |
| echo '}' >> wolf3d.html | |
| echo 'initDosBox();' >> wolf3d.html | |
| echo '</script>' >> wolf3d.html | |
| echo '</body>' >> wolf3d.html | |
| echo '</html>' >> wolf3d.html | |
| - name: Upload HTML file | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: wolf3d-html | |
| path: wolf3d.html | |
| deploy-demo: | |
| runs-on: ubuntu-latest | |
| needs: build | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| steps: | |
| - name: Download build artifact | |
| uses: actions/download-artifact@v4.1.3 | |
| with: | |
| name: build-dos | |
| path: WOLF3D | |
| - name: Assert wolf3d.exe is present | |
| run: | | |
| echo "Files in WOLF3D/:" | |
| ls -la WOLF3D/ | |
| if [ ! -f WOLF3D/WOLF3D.EXE ]; then | |
| echo "ERROR: WOLF3D.EXE not found in build artifact" >&2 | |
| exit 1 | |
| fi | |
| - name: Create dosbox.conf for jsdos | |
| run: | | |
| cat > WOLF3D/dosbox.conf << 'EOF' | |
| [cpu] | |
| cycles=max | |
| [autoexec] | |
| mount c . | |
| c: | |
| WOLF3D.EXE | |
| EOF | |
| - name: Package hra.jsdos | |
| run: | | |
| cd WOLF3D | |
| zip -r ../hra.jsdos . | |
| cd .. | |
| echo "Created hra.jsdos ($(du -sh hra.jsdos | cut -f1))" | |
| - name: Compute short SHA | |
| id: sha | |
| run: echo "short=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" | |
| - name: Generate index.html | |
| run: | | |
| SHORT_SHA="${{ steps.sha.outputs.short }}" | |
| cat > index.html << EOF | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Wolfenstein 3D — build ${SHORT_SHA}</title> | |
| <link rel="stylesheet" href="https://js-dos.com/v8/js-dos.css"> | |
| <script src="https://js-dos.com/v8/js-dos.js"></script> | |
| <style> | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { background: #111; color: #eee; font-family: Arial, sans-serif; display: flex; flex-direction: column; align-items: center; min-height: 100vh; padding: 20px; } | |
| h1 { margin-bottom: 12px; font-size: 1.4rem; } | |
| #dos { width: 800px; height: 600px; max-width: 100%; } | |
| .meta { margin-top: 10px; font-size: 0.8rem; color: #888; } | |
| button { margin-top: 10px; padding: 6px 16px; background: #333; color: #eee; border: 1px solid #555; cursor: pointer; border-radius: 4px; } | |
| button:hover { background: #444; } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Wolfenstein 3D</h1> | |
| <div id="dos"></div> | |
| <button onclick="document.getElementById('dos').requestFullscreen && document.getElementById('dos').requestFullscreen()">Full screen</button> | |
| <p class="meta">build ${SHORT_SHA} — <a href="https://github.com/luke-b/DosTest/commit/${GITHUB_SHA}" style="color:#888">view commit</a></p> | |
| <script> | |
| Dos(document.getElementById("dos"), { | |
| url: "./hra.jsdos", | |
| onprogress: (stage, total, loaded) => { | |
| if (stage === "Resolving DosBox") return; | |
| console.log(stage, loaded, "/", total); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
| EOF | |
| - name: Generate latest redirect | |
| run: | | |
| SHORT_SHA="${{ steps.sha.outputs.short }}" | |
| mkdir -p latest-redirect | |
| cat > latest-redirect/index.html << EOF | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta http-equiv="refresh" content="0; url=../${SHORT_SHA}/"> | |
| <title>Redirecting…</title> | |
| </head> | |
| <body> | |
| <p>Redirecting to <a href="../${SHORT_SHA}/">latest build (${SHORT_SHA})</a>…</p> | |
| </body> | |
| </html> | |
| EOF | |
| - name: Stage demo deployment | |
| run: | | |
| SHORT_SHA="${{ steps.sha.outputs.short }}" | |
| mkdir -p "staging/demo/build/${SHORT_SHA}" "staging/demo/build/latest" | |
| cp index.html "staging/demo/build/${SHORT_SHA}/index.html" | |
| cp hra.jsdos "staging/demo/build/${SHORT_SHA}/hra.jsdos" | |
| cp latest-redirect/index.html "staging/demo/build/latest/index.html" | |
| - name: Deploy demo to gh-pages | |
| uses: peaceiris/actions-gh-pages@v4 | |
| with: | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| publish_dir: ./staging/demo/build | |
| destination_dir: demo/build | |
| keep_files: true | |
| - name: Smoke test deployed demo | |
| run: | | |
| SHORT_SHA="${{ steps.sha.outputs.short }}" | |
| for url in \ | |
| "https://luke-b.github.io/DosTest/demo/build/${SHORT_SHA}/" \ | |
| "https://luke-b.github.io/DosTest/demo/build/latest/" | |
| do | |
| echo "Checking ${url}" | |
| curl --fail --silent --show-error --location \ | |
| --retry 3 --retry-delay 5 --retry-all-errors \ | |
| "${url}" > /dev/null | |
| done | |
| - name: Checkout gh-pages for pruning | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: gh-pages | |
| path: gh-pages-prune | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Prune old gh-pages builds | |
| run: | | |
| set -euo pipefail | |
| cd gh-pages-prune | |
| build_root="demo/build" | |
| mkdir -p "${build_root}" | |
| retain_count="${RETAINED_GH_PAGES_BUILDS}" | |
| case "${retain_count}" in | |
| ''|*[!0-9]*|0) | |
| echo "RETAINED_GH_PAGES_BUILDS must be set to a positive integer." >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| build_timestamps_file="$(mktemp)" | |
| while read -r dir_path; do | |
| dir="$(basename "${dir_path}")" | |
| [ "${dir}" = "latest" ] && continue | |
| if [ ! -d "${build_root}/${dir}" ]; then | |
| echo "Build directory ${build_root}/${dir} does not exist." >&2 | |
| exit 1 | |
| fi | |
| if ! ts="$(git log -1 --format=%ct -- "${build_root}/${dir}" 2>&1)"; then | |
| echo "Failed to determine timestamp for ${build_root}/${dir}: ${ts}" >&2 | |
| exit 1 | |
| fi | |
| printf '%s %s\n' "${ts}" "${dir}" >> "${build_timestamps_file}" | |
| done < <(find "${build_root}" -mindepth 1 -maxdepth 1 -type d) | |
| mapfile -t sha_build_dirs < <(sort -rn "${build_timestamps_file}" | awk '{print $2}' | grep -v '^$') | |
| if [ "${#sha_build_dirs[@]}" -le "${retain_count}" ]; then | |
| echo "No old builds to prune." | |
| exit 0 | |
| fi | |
| for dir in "${sha_build_dirs[@]:${retain_count}}"; do | |
| echo "Removing old build ${dir}" | |
| rm -rf "${build_root}/${dir}" | |
| done | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add --all "${build_root}" | |
| if ! git diff --cached --quiet; then | |
| git commit -m "chore: prune old demo builds [skip ci]" | |
| git push | |
| fi | |
| update-readme: | |
| runs-on: ubuntu-latest | |
| needs: deploy-demo | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| steps: | |
| - name: Checkout main | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: main | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Compute short SHA | |
| id: sha | |
| run: echo "short=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" | |
| - name: Update demo link in README.md | |
| run: | | |
| DEMO_URL="https://luke-b.github.io/DosTest/demo/build/latest/" | |
| BADGE_LINE="[](${DEMO_URL})" | |
| BLOCK="<!-- DEMO_LINK -->\n${BADGE_LINE}\n<!-- /DEMO_LINK -->" | |
| if grep -q '<!-- DEMO_LINK -->' README.md; then | |
| # Replace existing block (handles multi-line with perl) | |
| perl -i -0pe "s|<!-- DEMO_LINK -->.*?<!-- /DEMO_LINK -->|${BLOCK}|s" README.md | |
| else | |
| # Sentinel missing: insert complete block after the first heading line | |
| perl -i -0pe "s|(# [^\n]+\n)|\$1\n${BLOCK}\n|" README.md | |
| fi | |
| - name: Commit and push README update | |
| run: | | |
| SHORT_SHA="${{ steps.sha.outputs.short }}" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add README.md | |
| git diff --cached --quiet || git commit -m "chore: update demo link to build ${SHORT_SHA} [skip ci]" | |
| git push |