Skip to content

Fix nullable correlation handling (#486) #752

Fix nullable correlation handling (#486)

Fix nullable correlation handling (#486) #752

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
schedule:
- cron: "0 3 * * *"
workflow_dispatch:
# Only a newer push to the same PR cancels its superseded run. Every other run
# (each merge to main, the nightly schedule, manual dispatches) is grouped by
# commit: a group keeps at most one queued run and a newer queued run replaces
# it even without cancel-in-progress, so a per-ref group still dropped the CI of
# back-to-back merges. Per-commit groups keep a complete result on every commit.
concurrency:
group: ci-${{ github.event_name }}-${{ github.event_name == 'pull_request' && github.ref || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
# Least privilege by default; jobs that need more (nightly alert issue,
# coverage-badge push) declare their own elevated permissions.
permissions:
contents: read
# Dependency pinning: the gating jobs install with `-c constraints/ci.txt`
# (exported from uv.lock), so an upstream release cannot turn CI red overnight.
# test-matrix deliberately floats to the latest compatible releases: it is the
# early warning for upstream breakage. Regenerate with `make constraints`.
jobs:
lockfile:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- name: uv.lock and constraints/ci.txt are up to date
run: |
python -m pip install "uv==0.12.10"
uv lock --check
uv export --frozen --no-hashes --all-extras --no-emit-project --quiet -o constraints/ci.txt
if ! git diff --exit-code -- constraints/ci.txt; then
echo "::error::constraints/ci.txt is stale; run 'make constraints' and commit the result"
exit 1
fi
quality-fast:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
- name: Install
run: |
python -m pip install --upgrade pip
pip install -c constraints/ci.txt -e ".[dev,ml]"
- name: Lint
run: ruff check .
- name: Test (required fast lane)
run: pytest -m "not online and not large"
- name: Typecheck
# mypy targets Python 3.10; NumPy 2.5+ stubs require Python 3.12, so the
# type check runs against numpy<2.5 after the tests used the locked pin.
run: |
pip install "numpy<2.5"
mypy src/freshdata
truthbench:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
- name: Install
run: |
python -m pip install --upgrade pip
pip install -c constraints/ci.txt -e ".[dev,ml]"
- name: TruthBench PR ratchet
# PRs must not regress any gate that passes in the committed
# baseline.json; the known-red gates are release blockers and are
# enforced absolutely by the release workflow (make truthbench-release),
# so they are reported here as KNOWN-RED without failing every
# unrelated PR. Infrastructure errors and partial runs still fail.
run: make truthbench-pr
- name: Upload run artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: truthbench-results
path: benchmarks/truthbench/results/
test-matrix:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
include:
- python-version: "3.9"
pandas: "pandas>=1.5,<2"
numpy: "numpy<2"
- python-version: "3.13"
pandas: "pandas"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}
cache: pip
- name: Install
# Intentionally unpinned (no constraints file): this matrix tracks the
# latest releases each Python version resolves to.
run: |
python -m pip install --upgrade pip
pip install -e ".[dev,ml]"
if [ -n "${{ matrix.pandas }}" ]; then pip install "${{ matrix.pandas }}"; fi
if [ -n "${{ matrix.numpy }}" ]; then pip install "${{ matrix.numpy }}"; fi
- name: Test (fast marker set)
run: pytest -m "not online and not large"
nightly-online-large:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 45
# NO continue-on-error here: when this lane runs, a failure is a real
# failure. Visibility is guaranteed by the alert step below (a pinned
# issue), so a red nightly can't scroll by unnoticed.
permissions:
issues: write
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
- name: Install
run: |
python -m pip install --upgrade pip
pip install -c constraints/ci.txt -e ".[dev,ml]"
- name: Nightly online/large drift checks
# --no-cov: this lane deliberately runs ~20 tests, so the repo-wide
# --cov-fail-under=93 inherited from addopts can never be met here.
# The coverage gate is enforced by the full fast lane (quality-fast).
run: pytest -m "online or large or tier1" --no-cov
- name: Open/refresh alert issue on failure
if: failure()
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const title = "nightly large-data lane failing";
const body = `The scheduled online/large test lane failed.\n\n` +
`Run: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}` +
`/actions/runs/${context.runId}\n\n` +
`This lane does not run on push/PR — a failure here is only ` +
`visible through this issue and the Actions tab.`;
const open = await github.rest.issues.listForRepo({
...context.repo, state: "open", labels: "nightly-failure",
});
const existing = open.data.find(i => i.title === title);
if (existing) {
await github.rest.issues.createComment({
...context.repo, issue_number: existing.number, body,
});
} else {
await github.rest.issues.create({
...context.repo, title, body, labels: ["nightly-failure"],
});
}
# Runs on every push/PR: makes the *existence and status* of the large-data
# lane visible where a green checkmark would otherwise hide it. Fails if the
# large/online markers stop collecting any tests (a silently-rotted lane).
large-data-status:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
- name: Install
run: |
python -m pip install --upgrade pip
pip install -c constraints/ci.txt -e ".[dev,ml]"
- name: Assert the large/online lane still collects tests
run: |
N=$(pytest -m "online or large or tier1" --collect-only -q -o addopts="" \
2>/dev/null | grep -c "::" || true)
echo "large/online/tier1 tests collected: $N"
if [ "$N" -eq 0 ]; then
echo "::error::the large/online test lane collects zero tests — it has rotted"
exit 1
fi
{
echo "## Large-data test lane"
echo ""
echo "**$N** large/online/tier1 test(s) exist. They do **not** run on this"
echo "push/PR — they run on the nightly schedule (see the *nightly-online-large*"
echo "job in scheduled runs; failures open a pinned \`nightly-failure\` issue)."
} >> "$GITHUB_STEP_SUMMARY"
build:
needs: quality-fast
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- name: Build distributions
run: |
python -m pip install build twine
python -m build
twine check dist/*
freshcore-native:
# Builds the optional FreshCore Rust extension (crates/freshcore) and runs
# the FreshCore tests against it. No other job installs the extension, so
# the native parity tests skip everywhere else.
runs-on: ubuntu-latest
timeout-minutes: 10
env:
RUSTUP_TOOLCHAIN: "1.98.1"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
- name: Install Rust toolchain
run: rustup toolchain install "$RUSTUP_TOOLCHAIN" --profile minimal
- name: Install
# maturin develop installs into a virtualenv, so create one in the
# workspace and put it first on PATH for the remaining steps.
run: |
python -m venv .venv
echo "VIRTUAL_ENV=$PWD/.venv" >> "$GITHUB_ENV"
echo "$PWD/.venv/bin" >> "$GITHUB_PATH"
.venv/bin/python -m pip install --upgrade pip
.venv/bin/pip install -c constraints/ci.txt -e ".[dev,freshcore]"
- name: Rust unit tests
run: cargo test --locked --manifest-path crates/freshcore/Cargo.toml
- name: Build the extension
run: maturin develop --manifest-path crates/freshcore/Cargo.toml --features extension-module
- name: FreshCore tests
# The coverage gate in addopts applies to the full suite, not this subset.
run: |
python -c "import freshdata_freshcore"
pytest tests/test_execution -k freshcore -o addopts="--strict-markers" -q
coverage-badge:
# Publish a self-hosted shields endpoint badge to the `badges` branch
# (no third-party account needed). Runs only on main.
if: github.ref == 'refs/heads/main'
needs: quality-fast
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
- name: Install
run: |
python -m pip install --upgrade pip
pip install -c constraints/ci.txt -e ".[dev,ml]"
- name: Compute coverage and write badge JSON
run: |
pytest -m "not online and not large and not tier1" \
--cov=freshdata --cov-report=json -o addopts=""
python - <<'PY'
import json
pct = round(json.load(open("coverage.json"))["totals"]["percent_covered"])
color = ("brightgreen" if pct >= 90 else "green" if pct >= 80
else "yellow" if pct >= 70 else "orange" if pct >= 60 else "red")
json.dump(
{"schemaVersion": 1, "label": "coverage", "message": f"{pct}%", "color": color},
open("coverage.json", "w"),
)
print("coverage badge:", pct, "%", color)
PY
- name: Push badge to `badges` branch
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p /tmp/badges && cp coverage.json /tmp/badges/coverage.json
cd /tmp/badges
git init -q
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -q -b badges
git add coverage.json
git commit -q -m "Update coverage badge"
# Back-to-back merges run this job concurrently on different commits,
# and a force push can fail transiently (ref lock, network). Retry with
# backoff so a badge update never turns main red.
for attempt in 1 2 3 4; do
if git push -q --force \
"https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" badges; then
exit 0
fi
echo "badge push attempt ${attempt} failed; retrying"
sleep $((attempt * 10))
done
echo "::warning::coverage badge push failed after 4 attempts"
exit 1