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/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/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..7da3373a 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -15,10 +15,83 @@ 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 + + # 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" + say "Not a pull request — running the full test suite." + exit 0 + fi + + files=$(gh api --paginate "repos/${REPO}/pulls/${PR_NUMBER}/files" --jq '.[].filename') + + # 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 "" + 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" + 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: @@ -43,5 +116,33 @@ 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. + 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