From 4955bc6da6de3b19176ec1864ffa189ec56a7c0d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:49:42 +0200 Subject: [PATCH 1/2] ci: always report lint and test on pull requests The ruleset on main requires lint and test (3.11-3.14). #562 added a paths filter to the pull_request trigger, and a workflow that never triggers never reports its checks, so any PR touching only files outside flixopt/, tests/, pyproject.toml or .github/workflows/ is permanently blocked. Release PRs change exactly CHANGELOG.md, .release-please-manifest.json and CITATION.cff, so #740 cannot merge; #737 predates the filter and ran normally. The filter moves into a `changes` job that reads the PR's file list, and the install and pytest steps become conditional on it. The jobs still run and still report, but a docs-only PR costs a runner start instead of a full matrix. Detection failures fall back to running the tests, so a bad lookup wastes CI rather than skipping a real test run. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/tests.yaml | 42 ++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 04c17f536..ef861d119 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -8,14 +8,12 @@ on: - 'tests/**' - 'pyproject.toml' - '.github/workflows/**' + # No paths filter here: lint and test are required checks, and a workflow that + # never triggers never reports, leaving such PRs permanently blocked. The filter + # lives in the `changes` job below, which skips the expensive steps instead. pull_request: types: [opened, synchronize, reopened, ready_for_review] branches: ["**"] - paths: - - 'flixopt/**' - - 'tests/**' - - 'pyproject.toml' - - '.github/workflows/**' workflow_dispatch: concurrency: @@ -30,6 +28,32 @@ env: FLIXOPT_CI: false jobs: + changes: + name: Detect source changes + runs-on: ubuntu-24.04 + outputs: + source: ${{ steps.detect.outputs.source }} + steps: + - name: Detect source changes + id: detect + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + run: | + if [[ "${{ github.event_name }}" != 'pull_request' ]]; then + echo 'source=true' >> "$GITHUB_OUTPUT" + exit 0 + fi + paths=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json files --jq '.files[].path') + # Treat an unreadable or truncated listing as a source change, so a + # detection failure costs a test run rather than skipping one. + if [[ -z "$paths" ]] || grep -qE '^(flixopt/|tests/|pyproject\.toml$|\.github/workflows/)' <<< "$paths"; then + echo 'source=true' >> "$GITHUB_OUTPUT" + else + echo 'source=false' >> "$GITHUB_OUTPUT" + fi + lint: runs-on: ubuntu-24.04 steps: @@ -52,7 +76,7 @@ jobs: test: runs-on: ubuntu-24.04 timeout-minutes: 30 - needs: lint + needs: [lint, changes] strategy: fail-fast: false matrix: @@ -70,9 +94,11 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies + if: needs.changes.outputs.source == 'true' run: uv pip install --system .[dev] - name: Run tests + if: needs.changes.outputs.source == 'true' run: | if [[ "${{ github.event.pull_request.draft }}" == "true" ]]; then # Draft PR: skip examples, slow, and deprecated_api @@ -82,6 +108,10 @@ jobs: pytest -v --numprocesses=auto fi + - name: Report skipped tests + if: needs.changes.outputs.source != 'true' + run: echo 'No changes under flixopt/, tests/, pyproject.toml or .github/workflows/ - tests skipped.' + test-examples: runs-on: ubuntu-24.04 timeout-minutes: 45 From cef9476b5ae0de22635b0425d2251c0cb8ae75f6 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 27 Jul 2026 18:02:45 +0200 Subject: [PATCH 2/2] ci: fall back to running tests when PR file lookup fails Steps run under `bash -e`, so `paths=$(gh pr view ...)` aborted the step on a failed lookup rather than falling through to the empty-listing fallback. The changes job would fail, test would be skipped via needs, and the required checks would go unreported - the same block this workflow exists to prevent. Guarding the assignment lets the intended fallback run. Also declares least-privilege permissions for the workflow, matching release.yaml and dependabot-auto-merge.yaml; pull-requests: read is what the file-list lookup needs. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/tests.yaml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index ef861d119..fddad0374 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -16,6 +16,10 @@ on: branches: ["**"] workflow_dispatch: +permissions: + contents: read + pull-requests: read # `changes` reads the PR's file list via gh + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -45,9 +49,14 @@ jobs: echo 'source=true' >> "$GITHUB_OUTPUT" exit 0 fi - paths=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json files --jq '.files[].path') - # Treat an unreadable or truncated listing as a source change, so a - # detection failure costs a test run rather than skipping one. + # Treat a failed, empty or truncated listing as a source change, so a + # detection failure costs a test run rather than skipping one. The guard + # is required: steps run under `bash -e`, where a failing command + # substitution aborts the step instead of falling through. + if ! paths=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json files --jq '.files[].path'); then + echo 'source=true' >> "$GITHUB_OUTPUT" + exit 0 + fi if [[ -z "$paths" ]] || grep -qE '^(flixopt/|tests/|pyproject\.toml$|\.github/workflows/)' <<< "$paths"; then echo 'source=true' >> "$GITHUB_OUTPUT" else