diff --git a/.github/workflows/render-and-publish.yml b/.github/workflows/render-and-publish.yml index de274ae3..c881b2fb 100644 --- a/.github/workflows/render-and-publish.yml +++ b/.github/workflows/render-and-publish.yml @@ -1,4 +1,7 @@ on: + pull_request: + branches: + - main push: branches: - main @@ -49,8 +52,11 @@ jobs: key: cljdeps-${{ hashFiles('deps.edn') }} restore-keys: cljdeps- + - name: Test strict Clay failure detection + run: scripts/test-render-clay-strict.sh + - name: Build the content notebooks - run: clojure -M:clay -A:markdown + run: scripts/render-clay-strict.sh - name: Install lsof and apt prerequisites for Janqua run: sudo apt-get update && sudo apt-get install -y lsof gnupg lsb-release @@ -91,6 +97,7 @@ jobs: path: site/_site deploy: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' needs: build permissions: pages: write diff --git a/README.md b/README.md index b6efeb18..4e07be3c 100644 --- a/README.md +++ b/README.md @@ -310,6 +310,13 @@ Goal: Minimize friction in authoring while ensuring publishable reproducibility. The site is built and deployed using GitHub Actions with two workflows: +Before opening a pull request, run `scripts/pre-publish-gate.sh` from the +repository root. The same strict Clay renderer is used locally and in the full +GitHub Pages workflow. It requires Java 21, uses the runner's 1 MiB JVM thread +stack, fails when any source prints `Clay FAILED:`, then renders the full site +with Quarto. `scripts/test-render-clay-strict.sh` reproduces the historical +stack-overflow signal and checks that CI rejects it with an error annotation. + - **Full Build and Publish**: Triggered on pushes to `main`. Rebuilds all notebooks with Clay, renders the entire site with Quarto, and deploys to GitHub Pages. See [.github/workflows/render-and-publish.yml](.github/workflows/render-and-publish.yml). diff --git a/scripts/pre-publish-gate.sh b/scripts/pre-publish-gate.sh new file mode 100755 index 00000000..580656be --- /dev/null +++ b/scripts/pre-publish-gate.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$repo_root" + +scripts/render-clay-strict.sh +quarto render site diff --git a/scripts/render-clay-strict.sh b/scripts/render-clay-strict.sh new file mode 100755 index 00000000..4ab529a2 --- /dev/null +++ b/scripts/render-clay-strict.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$repo_root" + +java_version="$({ java -XshowSettings:properties -version; } 2>&1 \ + | awk -F= '/java.specification.version/ {gsub(/[[:space:]]/, "", $2); print $2; exit}')" +if [[ "$java_version" != "21" ]]; then + echo "Expected Java 21 to match GitHub Pages, found Java ${java_version:-unknown}." >&2 + exit 1 +fi + +# GitHub's Ubuntu runner uses a 1 MiB JVM thread stack. macOS commonly uses +# 2 MiB, which can hide recursive parser/regex failures until publication. +export JAVA_TOOL_OPTIONS="${JAVA_TOOL_OPTIONS:+${JAVA_TOOL_OPTIONS} }-Xss1m" + +log_file="$(mktemp "${TMPDIR:-/tmp}/clay-render.XXXXXX.log")" +trap 'rm -f "$log_file"' EXIT + +set +e +clojure -M:clay -A:markdown "$@" 2>&1 | tee "$log_file" +clay_status="${PIPESTATUS[0]}" +set -e + +if [[ "$clay_status" -ne 0 ]]; then + exit "$clay_status" +fi + +# Clay reports individual source failures but can still return success. +if grep -q 'Clay FAILED:' "$log_file"; then + echo "Clay reported source failures despite exiting successfully:" >&2 + grep 'Clay FAILED:' "$log_file" >&2 + if [[ "${GITHUB_ACTIONS:-}" == "true" ]]; then + while IFS= read -r failure; do + echo "::error title=Clay source render failed::${failure}" >&2 + done < <(grep 'Clay FAILED:' "$log_file") + fi + exit 1 +fi diff --git a/scripts/test-render-clay-strict.sh b/scripts/test-render-clay-strict.sh new file mode 100755 index 00000000..015a87ff --- /dev/null +++ b/scripts/test-render-clay-strict.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +fixture_bin="$(mktemp -d "${TMPDIR:-/tmp}/clay-strict-fixture.XXXXXX")" +output_file="$(mktemp "${TMPDIR:-/tmp}/clay-strict-output.XXXXXX.log")" +trap 'rm -rf "$fixture_bin"; rm -f "$output_file"' EXIT + +cat >"$fixture_bin/java" <<'EOF' +#!/usr/bin/env bash +echo ' java.specification.version = 21' >&2 +EOF + +cat >"$fixture_bin/clojure" <<'EOF' +#!/usr/bin/env bash +echo 'Clay FAILED: src/language_learning/vocabulary_estimation/beta_binomial_first_pass.clj' +echo 'java.lang.StackOverflowError' +exit 0 +EOF + +chmod +x "$fixture_bin/java" "$fixture_bin/clojure" + +set +e +GITHUB_ACTIONS=true PATH="$fixture_bin:$PATH" \ + "$repo_root/scripts/render-clay-strict.sh" >"$output_file" 2>&1 +result_status=$? +set -e + +if [[ "$result_status" -ne 1 ]]; then + sed -n '1,120p' "$output_file" >&2 + echo "Expected the strict Clay renderer to reject the historical failure; got status $result_status." >&2 + exit 1 +fi + +grep -Fq 'java.lang.StackOverflowError' "$output_file" +grep -Fq 'Clay reported source failures despite exiting successfully:' "$output_file" +grep -Fq '::error title=Clay source render failed::Clay FAILED: src/language_learning/vocabulary_estimation/beta_binomial_first_pass.clj' "$output_file" + +echo 'Strict Clay regression passed: the historical stack-overflow signal was rejected with a GitHub error annotation.'