diff --git a/.github/workflows/drift.yml b/.github/workflows/drift.yml new file mode 100644 index 0000000..177d297 --- /dev/null +++ b/.github/workflows/drift.yml @@ -0,0 +1,98 @@ +name: Drift + +# Scheduled drift detection: re-run the full test workflow (unit matrix + +# notebook execution) against a fresh dependency resolve of an unchanged +# main. No lockfile is committed AND this repo has no push-to-main CI at +# all, so between PRs nothing else proves main still works. +# Failures file ONE deduped `drift`-labeled issue; green runs close it. +on: + schedule: + - cron: "20 5 * * 1" # weekly + workflow_dispatch: + inputs: + force_fail: + description: "Exercise the drift-issue path without a real failure" + type: boolean + default: false + +permissions: + contents: read + +jobs: + tests: + # Only `report` needs issue-write; the token that runs freshly-resolved + # third-party test code should not carry it. + permissions: + contents: read + uses: ./.github/workflows/run_tests.yml + + report: + needs: [tests] + if: always() + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + # One issue reconciler at a time: the read at `gh issue list` and the write + # below are not atomic, so an overlapping dispatch could file a duplicate. + concurrency: + group: drift-issue-${{ github.repository }} + cancel-in-progress: false + steps: + - name: Create, update, or close the deduped drift issue + env: + GH_TOKEN: ${{ github.token }} + RESULT_TESTS: ${{ needs.tests.result }} + FORCE_FAIL: ${{ inputs.force_fail }} + run: | + set -euo pipefail + KEY="drift-key: LANfactory-scheduled" + TITLE="drift: LANfactory scheduled checks failing" + RUN_URL="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" + + FAILED_JOBS="" + if [ "$RESULT_TESTS" = "failure" ]; then + FAILED_JOBS=" tests" + fi + if [ "$FORCE_FAIL" = "true" ]; then + FAILED_JOBS="$FAILED_JOBS (force_fail rehearsal)" + fi + + EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --label drift \ + --state open --search "in:body \"$KEY\"" \ + --json number --jq '.[0].number // empty') + + # Only a genuine success closes the issue. `report` runs under + # `if: always()`, which includes cancellation, and a cancelled or + # never-started test job is no evidence of health — treating it as + # green would silently retire a real drift issue. + if [ -z "$FAILED_JOBS" ]; then + if [ "$RESULT_TESTS" = "success" ]; then + if [ -n "$EXISTING" ]; then + gh issue close "$EXISTING" --repo "$GITHUB_REPOSITORY" \ + --comment "Scheduled drift checks green again: $RUN_URL" + fi + else + echo "::notice::Inconclusive run (tests: $RESULT_TESTS) —" \ + "drift issue state left unchanged." + fi + exit 0 + fi + + if [ -n "$EXISTING" ]; then + gh issue comment "$EXISTING" --repo "$GITHUB_REPOSITORY" \ + --body "Still failing (${FAILED_JOBS# }): $RUN_URL" + else + gh label create drift --repo "$GITHUB_REPOSITORY" \ + --description "scheduled drift detection failure" \ + --force 2>/dev/null || true + BODY=$(printf '%s\n' \ + "" \ + "Scheduled drift run failed in:${FAILED_JOBS}" \ + "Run: $RUN_URL" \ + "" \ + "This re-runs the existing test workflow against a fresh dependency resolve of an unchanged main — a failure usually means an upstream or toolchain release, not a code change." \ + "Triage via the spine's audit-drift skill.") + gh issue create --repo "$GITHUB_REPOSITORY" --label drift \ + --title "$TITLE" --body "$BODY" + fi diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 23e1d2a..5d0b27f 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -2,6 +2,7 @@ name: Run tests on: pull_request: + workflow_call: # Called by drift.yml (scheduled drift detection) jobs: run_tests: @@ -27,22 +28,19 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v7 with: - version: "0.6.5" - # enable-cache: true - # cache-dependency-glob: "pyproject.toml pdm.lock" - - - name: Clear all caches - run: | - rm -rf ~/.cache/pip - rm -rf ~/.cache/uv - rm -rf ~/.cache/conda - rm -rf ~/.cache/npm - + version: "latest" + enable-cache: true + cache-dependency-glob: "pyproject.toml" + + # (a cache-nuking block used to sit here: rm -rf of every cache plus + # `uv sync --reinstall` with PIP_NO_CACHE_DIR — every PR run cold- + # downloaded the multi-GB torch/bayesflow/keras/sbi stack, and uv was + # pinned to the ancient 0.6.5. It was added during an undiagnosed hour + # of trial and error in June 2025; the SIGILL it was flailing at came + # from ssm-simulators' `-march=native` wheels and is pinned out by + # `ssm-simulators>=0.13.1`. Don't reinstate it on a hunch.) - name: Install package - run: uv sync --all-groups --reinstall - env: - # UV_CACHE_DIR: "" - PIP_NO_CACHE_DIR: "1" + run: uv sync --all-groups - name: Run pytest run: uv run pytest @@ -75,7 +73,9 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v7 with: - version: "0.6.5" + version: "latest" + enable-cache: true + cache-dependency-glob: "pyproject.toml" - name: Install package (with notebook + backend deps) run: uv sync --all-groups