From 0c8e4e4b28404b5d69b3a0b187c645f2bc52893e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 14:47:08 +0000 Subject: [PATCH 1/4] CI - IMPROVEMENT - Run the Julia test suite only on PRs that touch Julia code The full suite takes 15-25 minutes per matrix leg and was running on every pull request, including docs- and CLAUDE.md-only changes. A `changes` job now inspects the PR's file list and gates the matrix on whether the change touches anything test/runtests.jl actually reads: any .jl file outside docs/, benchmarks/ and regression-harness/, plus Project.toml, test/, examples/ and this workflow. Pushes to main/develop still always run the suite. A `Tests` aggregate job runs unconditionally and reports success when the suite is legitimately skipped, so it can serve as the required status check without blocking merges on skipped runs. Also across the workflows: - Fix the copilot-setup-steps triggers, which referenced a .yml file that does not exist and had a stray space in the pull_request path, so neither the push nor the pull_request trigger ever matched. - Add a concurrency group to the test workflow so superseded PR runs are cancelled instead of running two full matrices in parallel. - Add timeout-minutes to every job; jobs previously inherited the 6-hour default, so a hung test would burn a full day of runner minutes. - Standardize on actions/checkout@v6 and julia-actions/cache@v3 everywhere. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019tFBCa5iBXyb7xp8LxqBJx --- .github/workflows/claude-code-review.yml | 3 +- .github/workflows/claude.yml | 3 +- .github/workflows/copilot-setup-steps.yaml | 13 ++-- .github/workflows/make_docs.yaml | 1 + .github/workflows/test.yaml | 86 ++++++++++++++++++++++ 5 files changed, 98 insertions(+), 8 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index e64a8a39..8c34c777 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -13,6 +13,7 @@ jobs: contains(github.event.comment.body, '@claude review') runs-on: ubuntu-latest + timeout-minutes: 60 permissions: contents: read pull-requests: read @@ -21,7 +22,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 1 diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index a5457cd7..26ea5ceb 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -18,6 +18,7 @@ jobs: (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && !contains(github.event.review.body, '@claude review')) || (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) runs-on: ubuntu-latest + timeout-minutes: 60 permissions: contents: read pull-requests: read @@ -26,7 +27,7 @@ jobs: actions: read # Required for Claude to read CI results on PRs steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 1 diff --git a/.github/workflows/copilot-setup-steps.yaml b/.github/workflows/copilot-setup-steps.yaml index 5de80c5e..9197e21c 100644 --- a/.github/workflows/copilot-setup-steps.yaml +++ b/.github/workflows/copilot-setup-steps.yaml @@ -6,15 +6,16 @@ on: workflow_dispatch: push: paths: - - .github/workflows/copilot-setup-steps.yml + - .github/workflows/copilot-setup-steps.yaml pull_request: paths: - - . github/workflows/copilot-setup-steps.yml + - .github/workflows/copilot-setup-steps.yaml jobs: # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot copilot-setup-steps: runs-on: ubuntu-latest + timeout-minutes: 30 # Set the permissions to the lowest permissions possible needed for your steps permissions: @@ -22,20 +23,20 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Julia uses: julia-actions/setup-julia@v2 with: - version: '1.11' + version: '1.11' - name: Cache Julia packages - uses: julia-actions/cache@v2 + uses: julia-actions/cache@v3 with: cache-name: julia-cache cache-packages: true cache-artifacts: true cache-registries: true - - name: Instantiate Julia environment + - name: Instantiate Julia environment run: julia --project="." -e "using Pkg; Pkg.instantiate()" diff --git a/.github/workflows/make_docs.yaml b/.github/workflows/make_docs.yaml index b7f4f0ee..f74f52f1 100644 --- a/.github/workflows/make_docs.yaml +++ b/.github/workflows/make_docs.yaml @@ -19,6 +19,7 @@ jobs: docs: name: Documentation runs-on: ubuntu-latest + timeout-minutes: 60 permissions: actions: write contents: write diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index afb2979e..4dad0bab 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -15,10 +15,74 @@ permissions: actions: write contents: read +concurrency: + # One in-flight run per branch/PR. Superseded pull request runs are cancelled; + # runs on main/develop are allowed to finish so every merged commit is verified. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: + changes: + name: Detect Julia changes + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + pull-requests: read + outputs: + julia: ${{ steps.filter.outputs.julia }} + steps: + - name: Check whether the pull request touches Julia code + id: filter + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + set -euo pipefail + + # Pushes to main/develop always run: every merged commit gets verified. + if [ "${{ github.event_name }}" != "pull_request" ]; then + echo "julia=true" >> "$GITHUB_OUTPUT" + echo "Not a pull request — running the full test suite." >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + files=$(gh api --paginate "repos/${REPO}/pulls/${PR_NUMBER}/files" --jq '.[].filename') + + # Directories whose Julia sources are not exercised by test/runtests.jl. + not_tested='^(docs|benchmarks|regression-harness)/' + # Everything the test suite actually reads: sources, tests and their + # fixtures, the example decks the tests run end-to-end, and this workflow. + tested='(\.jl$|^Project\.toml$|^test/|^examples/|^\.github/workflows/test\.yaml$)' + + relevant=$(printf '%s\n' "$files" | grep -Ev "$not_tested" | grep -E "$tested" || true) + + { + echo "### Julia change detection" + echo + if [ -n "$relevant" ]; then + echo "Running the test suite. Matched files:" + echo '```' + echo "$relevant" + echo '```' + else + echo "No Julia code, test data or example decks changed — skipping the test suite." + fi + } >> "$GITHUB_STEP_SUMMARY" + + if [ -n "$relevant" ]; then + echo "julia=true" >> "$GITHUB_OUTPUT" + else + echo "julia=false" >> "$GITHUB_OUTPUT" + fi + test: name: runtests ${{ matrix.version }} - ${{ matrix.os }} + needs: changes + if: needs.changes.outputs.julia == 'true' runs-on: ${{ matrix.os }} + timeout-minutes: 90 strategy: fail-fast: false matrix: @@ -45,3 +109,25 @@ jobs: - name: Run tests uses: julia-actions/julia-runtest@v1 + + # Stable check that is present on every pull request, whether or not the suite + # ran. Point branch protection at this job so skipped runs do not block merges. + test-status: + name: Tests + needs: [changes, test] + if: always() + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Report suite result + run: | + set -euo pipefail + if [ "${{ needs.changes.result }}" != "success" ]; then + echo "Change detection failed; cannot tell whether tests were needed." + exit 1 + fi + case "${{ needs.test.result }}" in + success) echo "Test suite passed." ;; + skipped) echo "No Julia code changed; test suite intentionally skipped." ;; + *) echo "Test suite result: ${{ needs.test.result }}"; exit 1 ;; + esac From 673a3a0b25f4bb921293f396ded4f1e95a369832 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 14:54:22 +0000 Subject: [PATCH 2/4] CI - NEW FEATURE - Add a pre-commit formatting workflow, disabled for now Adds a Format and Hygiene workflow that runs the repository's own pre-commit suite (JuliaFormatter plus the file-hygiene hooks) so contributors who have not run `pre-commit install` are still caught. It is wired to workflow_dispatch only: the repository has pre-existing formatting drift that must be cleaned up first, which is out of scope here. Running it manually from the Actions tab shows the current damage; uncommenting the pull_request trigger enables it as a gate. The julia-formatter hook is `language: system`, so it shells out to whatever JuliaFormatter is in the default depot environment rather than the one implied by the pinned hook revision. The workflow therefore installs JuliaFormatter v1.0.62 explicitly, matching the rev in .pre-commit-config.yaml, so CI and local runs agree. Also disables coverage instrumentation in the test job. julia-runtest enables it by default, but nothing consumes the .cov files: they are gitignored and never uploaded. Instrumenting compute-heavy numerical code for output that is discarded is pure cost. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019tFBCa5iBXyb7xp8LxqBJx --- .github/workflows/format.yaml | 65 +++++++++++++++++++++++++++++++++++ .github/workflows/test.yaml | 6 ++++ 2 files changed, 71 insertions(+) create mode 100644 .github/workflows/format.yaml diff --git a/.github/workflows/format.yaml b/.github/workflows/format.yaml new file mode 100644 index 00000000..d41c83bb --- /dev/null +++ b/.github/workflows/format.yaml @@ -0,0 +1,65 @@ +name: Format and Hygiene + +# Runs the repository's pre-commit suite (JuliaFormatter + file hygiene) so that +# contributors who have not run `pre-commit install` locally are still caught. +# +# DISABLED as a pull request gate for now: the repository has pre-existing +# formatting drift that has to be cleaned up first. Until then, run this +# manually from the Actions tab ("Run workflow") to see the current damage. +# +# To enable it as a gate, uncomment the `pull_request` trigger below. If the +# test suite's `Tests` job is a required status check, add `pre-commit` too. +on: + workflow_dispatch: + # pull_request: + # branches: + # - main + # - develop + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + pre-commit: + name: pre-commit + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + # Matches default_language_version in .pre-commit-config.yaml + python-version: '3.10' + + - name: Set up Julia + uses: julia-actions/setup-julia@v2 + with: + version: '1.11' + + # The julia-formatter hook is `language: system`: it shells out to + # `julia -e 'import JuliaFormatter: format; format(ARGS)'` and therefore + # uses whatever JuliaFormatter lives in the default depot environment. + # Pin it to the same version .pre-commit-config.yaml pins the hook repo + # to, otherwise CI and local pre-commit runs disagree on formatting. + - name: Install JuliaFormatter + run: julia -e 'using Pkg; Pkg.add(PackageSpec(name="JuliaFormatter", version="1.0.62"))' + + - name: Cache pre-commit environments + uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: pre-commit-${{ runner.os }}-py3.10-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: | + pre-commit-${{ runner.os }}-py3.10- + + - name: Run pre-commit + run: | + python -m pip install --upgrade pre-commit + pre-commit run --all-files --show-diff-on-failure --color=always diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 4dad0bab..40435623 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -107,8 +107,14 @@ jobs: - name: Build Julia packages uses: julia-actions/julia-buildpkg@v1 + # Coverage instrumentation is off: nothing consumes the .cov files (they are + # gitignored and never uploaded), and instrumenting slows compute-heavy + # numerical code noticeably. Set coverage: true and add a Codecov upload + # step together if coverage reporting is ever wanted. - name: Run tests uses: julia-actions/julia-runtest@v1 + with: + coverage: false # Stable check that is present on every pull request, whether or not the suite # ran. Point branch protection at this job so skipped runs do not block merges. From fadb72d21a01ac1f32c4f630867d9b05ff875396 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 15:02:07 +0000 Subject: [PATCH 3/4] CI - IMPROVEMENT - Log the Julia change-detection verdict to the job log The detector wrote its decision only to the run summary, so the job log showed the script and nothing else. That makes "why did the suite run?" unanswerable from the place people look first. Echo the verdict and the matched files to stdout as well, and list the files considered when the suite is skipped. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019tFBCa5iBXyb7xp8LxqBJx --- .github/workflows/test.yaml | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 40435623..cfa3bd1a 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -41,10 +41,14 @@ jobs: run: | set -euo pipefail + # Report the decision to the job log as well as the run summary, so + # `why did/didn't this run?` is answerable from either one. + say() { echo "$1"; echo "$1" >> "$GITHUB_STEP_SUMMARY"; } + # Pushes to main/develop always run: every merged commit gets verified. if [ "${{ github.event_name }}" != "pull_request" ]; then echo "julia=true" >> "$GITHUB_OUTPUT" - echo "Not a pull request — running the full test suite." >> "$GITHUB_STEP_SUMMARY" + say "Not a pull request — running the full test suite." exit 0 fi @@ -58,18 +62,21 @@ jobs: relevant=$(printf '%s\n' "$files" | grep -Ev "$not_tested" | grep -E "$tested" || true) - { - echo "### Julia change detection" - echo - if [ -n "$relevant" ]; then - echo "Running the test suite. Matched files:" - echo '```' - echo "$relevant" - echo '```' - else - echo "No Julia code, test data or example decks changed — skipping the test suite." - fi - } >> "$GITHUB_STEP_SUMMARY" + say "### Julia change detection" + say "" + if [ -n "$relevant" ]; then + say "Running the test suite. Matched files:" + say '```' + say "$relevant" + say '```' + else + say "No Julia code, test data or example decks changed — skipping the test suite." + say "" + say "Files considered:" + say '```' + say "$files" + say '```' + fi if [ -n "$relevant" ]; then echo "julia=true" >> "$GITHUB_OUTPUT" From f72b79feda4bc590f4b08e14e633b731f05a999b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 15:10:01 +0000 Subject: [PATCH 4/4] CI - IMPROVEMENT - Narrow test triggering to an allowlist of tested paths The detector matched any .jl file outside a denylist of docs, benchmarks and regression-harness. That meant new untested Julia triggered a full run unless someone remembered to extend the denylist: a benchmark, a root-level script, or a future tools/ directory all ran the suite for nothing. Replace it with an allowlist of what test/runtests.jl actually exercises: src/, test/, examples/, Project.toml, and this workflow. Untested code is now excluded by default rather than by maintenance. Kept as directory prefixes rather than *.jl: src/ForcingTerms/coil_geometries holds the .dat coil geometries the coil tests load, and test/test_data holds 28 non-Julia fixtures. Matching only *.jl would skip runs when either changes. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019tFBCa5iBXyb7xp8LxqBJx --- .github/workflows/test.yaml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index cfa3bd1a..7da3373a 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -54,13 +54,15 @@ jobs: files=$(gh api --paginate "repos/${REPO}/pulls/${PR_NUMBER}/files" --jq '.[].filename') - # Directories whose Julia sources are not exercised by test/runtests.jl. - not_tested='^(docs|benchmarks|regression-harness)/' - # Everything the test suite actually reads: sources, tests and their - # fixtures, the example decks the tests run end-to-end, and this workflow. - tested='(\.jl$|^Project\.toml$|^test/|^examples/|^\.github/workflows/test\.yaml$)' - - relevant=$(printf '%s\n' "$files" | grep -Ev "$not_tested" | grep -E "$tested" || true) + # Allowlist of what test/runtests.jl actually exercises: the package + # itself, the tests and their fixtures, the example decks the tests run + # end-to-end, the dependency set, and this workflow. Anything not listed + # here does not trigger a run, so new untested Julia (benchmarks, docs + # scripts, a future tools/ directory) is excluded by default rather than + # by remembering to exclude it. + tested='(^src/|^test/|^examples/|^Project\.toml$|^\.github/workflows/test\.yaml$)' + + relevant=$(printf '%s\n' "$files" | grep -E "$tested" || true) say "### Julia change detection" say ""