Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/actions/gitleaks/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Gitleaks scan
description: Downloads a checksum-verified gitleaks release and scans the commits introduced by the current push or pull request for secrets. Extracted so this repo's own CI and monorepo's synced copy of this repo can call the identical steps locally -- see the parent engagement README's "Sharing CI with the monorepo" pattern, established for infrastructure and applied here.

inputs:
working-directory:
description: Directory whose git history and .gitleaks.toml config to scan
required: false
default: "."

runs:
using: composite
steps:
- name: Install gitleaks
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
set -euo pipefail
VER=8.21.2
curl -sSfL --max-time 60 -o gitleaks.tar.gz "https://github.com/gitleaks/gitleaks/releases/download/v${VER}/gitleaks_${VER}_linux_x64.tar.gz"
echo "5bc41815076e6ed6ef8fbecc9d9b75bcae31f39029ceb55da08086315316e3ba gitleaks.tar.gz" | sha256sum -c -
tar -xzf gitleaks.tar.gz gitleaks
rm gitleaks.tar.gz
./gitleaks version

- name: Scan new commits for secrets
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "pull_request" ]; then
RANGE="${{ github.event.pull_request.base.sha }}..${{ github.sha }}"
elif [ "${{ github.event.before }}" = "0000000000000000000000000000000000000000" ]; then
RANGE="-1 ${{ github.sha }}" # first push of a branch: scan tip commit
else
RANGE="${{ github.event.before }}..${{ github.sha }}"
fi
echo "Scanning commit range: $RANGE"
./gitleaks detect --source . --log-opts="$RANGE" \
--config .gitleaks.toml --redact --verbose
29 changes: 29 additions & 0 deletions .github/actions/test-minimal-install/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Test minimal install
description: Regression guard for the model_runner lazy-import bug -- a plain `pip install .`, with neither pandas nor pywr present, must still let create-synthetic-model run. Extracted so this repo's own CI and monorepo's synced copy of this repo can call the identical steps locally -- see the parent engagement README's "Sharing CI with the monorepo" pattern, established for infrastructure and applied here.

inputs:
working-directory:
description: Directory containing the pywr_utils package to test
required: false
default: "."

runs:
using: composite
steps:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- shell: bash
working-directory: ${{ inputs.working-directory }}
run: pip install .
- shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
pywr-utils create-synthetic-model --zones 2 --transfers 1 --output /tmp/synthetic.json --show-summary
test -f /tmp/synthetic.json
- name: confirm pandas and pywr are genuinely absent
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
! python -c "import pandas" 2>/dev/null
! python -c "import pywr" 2>/dev/null
41 changes: 41 additions & 0 deletions .github/actions/test/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Test
description: Installs the package with its run/dev extras, runs the pytest suite, and uploads a coverage report. Extracted so this repo's own CI and monorepo's synced copy of this repo can call the identical steps locally -- see the parent engagement README's "Sharing CI with the monorepo" pattern, established for infrastructure and applied here.

inputs:
working-directory:
description: Directory containing the pywr_utils package to test
required: false
default: "."

runs:
using: composite
steps:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- shell: bash
working-directory: ${{ inputs.working-directory }}
# pywr's lpsolve extension needs headers this runner doesn't have; glpk (also needed) is prebaked into the ci-runners image.
env:
PYWR_BUILD_LPSOLVE: "false"
run: pip install -e ".[run,dev]"
- shell: bash
working-directory: ${{ inputs.working-directory }}
run: pytest -v
- name: write coverage summary
# Fully native, no admin-gated feature or third-party action needed: coverage.py's own markdown formatter piped into GitHub Actions' own step-summary mechanism. Shows up on every run's summary page (and one click away from the PR checks list) regardless of whether Code Quality is ever enabled.
if: always()
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
echo '## Coverage' >> "$GITHUB_STEP_SUMMARY"
python -m coverage report --format=markdown >> "$GITHUB_STEP_SUMMARY"
- name: upload coverage report
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: actions/upload-code-coverage@v1
with:
file: ${{ inputs.working-directory }}/coverage.xml
language: Python
label: code-coverage/pytest
# TEMPORARY: Code Quality isn't enabled for this repo yet (Settings > Security > Code quality) — that's an org/repo admin action, and neither CI nor a push-only collaborator can do it. Without this, the upload 403s and fails the whole job. Remove fail-on-error once an admin enables Code Quality and this step succeeds for real.
fail-on-error: false
40 changes: 40 additions & 0 deletions .github/actions/typecheck/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Typecheck and lint
description: Runs mypy, ruff, vulture, bandit, and pip-audit against the Python package. Extracted so this repo's own CI and monorepo's synced copy of this repo can call the identical steps locally -- see the parent engagement README's "Sharing CI with the monorepo" pattern, established for infrastructure and applied here.

inputs:
working-directory:
description: Directory containing the pywr_utils package to check
required: false
default: "."

runs:
using: composite
steps:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- shell: bash
working-directory: ${{ inputs.working-directory }}
run: pip install -e ".[dev]"
- shell: bash
working-directory: ${{ inputs.working-directory }}
run: mypy pywr_utils/
- name: lint (ruff)
shell: bash
working-directory: ${{ inputs.working-directory }}
run: ruff check .
- name: dead-code scan (vulture)
shell: bash
working-directory: ${{ inputs.working-directory }}
run: vulture pywr_utils vulture_whitelist.py --min-confidence 80
- name: security scan (bandit)
shell: bash
working-directory: ${{ inputs.working-directory }}
run: bandit -r pywr_utils
- name: dependency vulnerability scan (pip-audit)
shell: bash
working-directory: ${{ inputs.working-directory }}
# pip itself is not a declared dependency, just whatever setup-python provisions; keep it current so a stale bundled pip doesn't produce a false CVE finding here.
run: |
pip install -U pip
pip-audit --skip-editable
64 changes: 10 additions & 54 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,77 +4,33 @@ on:
push:
branches: [main]
pull_request: {}
workflow_dispatch: {}

permissions:
contents: read
code-quality: write

jobs:
typecheck:
runs-on: ubuntu-latest
runs-on: gha-ci-runners
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -e ".[dev]"
- run: mypy pywr_utils/
- name: lint (ruff)
run: ruff check .
- name: dead-code scan (vulture)
run: vulture pywr_utils vulture_whitelist.py --min-confidence 80
- name: security scan (bandit)
run: bandit -r pywr_utils
- name: dependency vulnerability scan (pip-audit)
# pip itself is not a declared dependency, just whatever setup-python provisions; keep it current so a stale bundled pip doesn't produce a false CVE finding here.
run: |
pip install -U pip
pip-audit --skip-editable
- uses: ./.github/actions/typecheck

test:
runs-on: ubuntu-latest
env:
# pywr's lpsolve extension needs headers this runner doesn't have; glpk (also needed) is installed below via apt.
PYWR_BUILD_LPSOLVE: "false"
runs-on: gha-ci-runners
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: sudo apt-get update && sudo apt-get install -y libglpk-dev
- run: pip install -e ".[run,dev]"
- run: pytest -v
- name: write coverage summary
# Fully native, no admin-gated feature or third-party action needed: coverage.py's own markdown formatter piped into GitHub Actions' own step-summary mechanism. Shows up on every run's summary page (and one click away from the PR checks list) regardless of whether Code Quality is ever enabled.
if: always()
run: |
echo '## Coverage' >> "$GITHUB_STEP_SUMMARY"
python -m coverage report --format=markdown >> "$GITHUB_STEP_SUMMARY"
- name: upload coverage report
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: actions/upload-code-coverage@v1
with:
file: coverage.xml
language: Python
label: code-coverage/pytest
# TEMPORARY: Code Quality isn't enabled for this repo yet (Settings > Security > Code quality) — that's an org/repo admin action, and neither CI nor a push-only collaborator can do it. Without this, the upload 403s and fails the whole job. Remove fail-on-error once an admin enables Code Quality and this step succeeds for real.
fail-on-error: false
- uses: ./.github/actions/test

test-minimal-install:
# Regression guard for the model_runner lazy-import bug: a plain `pip install .`, with neither pandas nor pywr present, must still let create-synthetic-model run — only the `run` extra needs either.
runs-on: ubuntu-latest
runs-on: gha-ci-runners
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install .
- run: |
pywr-utils create-synthetic-model --zones 2 --transfers 1 --output /tmp/synthetic.json --show-summary
test -f /tmp/synthetic.json
- name: confirm pandas and pywr are genuinely absent
run: |
! python -c "import pandas" 2>/dev/null
! python -c "import pywr" 2>/dev/null
- uses: ./.github/actions/test-minimal-install
27 changes: 5 additions & 22 deletions .github/workflows/gitleaks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,18 @@ on:
pull_request:
push:
branches: [main]
workflow_dispatch: {}

permissions:
contents: read

jobs:
gitleaks:
runs-on: ubuntu-latest
# Self-hosted fleet: the org's billing block stops GitHub-hosted jobs, and the runner container has no sudo -- the binary runs from the workspace by path.
runs-on: gha-ci-runners
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Install gitleaks
run: |
VER=8.21.2
curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${VER}/gitleaks_${VER}_linux_x64.tar.gz" | tar -xz gitleaks
sudo install gitleaks /usr/local/bin/gitleaks
gitleaks version

- name: Scan new commits for secrets
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "pull_request" ]; then
RANGE="${{ github.event.pull_request.base.sha }}..${{ github.sha }}"
elif [ "${{ github.event.before }}" = "0000000000000000000000000000000000000000" ]; then
RANGE="-1 ${{ github.sha }}" # first push of a branch: scan tip commit
else
RANGE="${{ github.event.before }}..${{ github.sha }}"
fi
echo "Scanning commit range: $RANGE"
gitleaks detect --source . --log-opts="$RANGE" \
--config .gitleaks.toml --redact --verbose
- uses: ./.github/actions/gitleaks
Loading