Skip to content

preparing version 0.4.0 #266

preparing version 0.4.0

preparing version 0.4.0 #266

Workflow file for this run

name: CI
on:
push:
branches:
- main
pull_request:
branches:
- "**"
jobs:
workflow-lint:
name: Workflow lint (actionlint)
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Lint GitHub workflow files
uses: rhysd/actionlint@v1.7.12
dependency-audit:
name: Dependency audit (uv audit)
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
with:
python-version: "3.12"
enable-cache: true
- name: Audit dependencies for vulnerabilities
run: |
uv audit
docs-touch-gate:
name: Docs touch gate
runs-on: ubuntu-latest
if: ${{ github.event_name == 'pull_request' }}
permissions:
pull-requests: read
steps:
- name: Detect code/docs file changes
id: changes
uses: dorny/paths-filter@v4
with:
filters: |
code:
- 'src/**'
docs:
- 'docs/**'
- 'README.md'
- 'CHANGELOG.md'
- 'CONTRIBUTING.md'
- name: Enforce docs update for code changes
uses: actions/github-script@v9
env:
CODE_CHANGED: ${{ steps.changes.outputs.code }}
DOCS_CHANGED: ${{ steps.changes.outputs.docs }}
with:
script: |
// Labels must be fetched via the REST API. Re-runs and the event
// payload still reflect labels as-of the original `pull_request`
// event, so `no-docs` added after the first run would otherwise
// never be seen.
const pr = context.payload.pull_request;
if (!pr) {
core.setFailed("Expected pull_request payload.");
return;
}
const { data } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
});
const labels = (data.labels ?? []).map((l) => l.name);
const hasSkipLabel = labels.includes("no-docs") || labels.includes("internal-only");
const codeChanged = process.env.CODE_CHANGED === "true";
const docsChanged = process.env.DOCS_CHANGED === "true";
if (!codeChanged) {
core.info("No src/ changes detected; docs-touch gate passes.");
return;
}
if (docsChanged) {
core.info("Code and docs changed together; docs-touch gate passes.");
return;
}
if (hasSkipLabel) {
core.info(
"No docs changes, but PR has no-docs/internal-only label; docs-touch gate passes."
);
return;
}
core.setFailed(
"This PR changes src/ without docs updates. " +
"Update docs/README/CHANGELOG/CONTRIBUTING, or apply the " +
"`no-docs`/`internal-only` label with reviewer approval."
);
newsfragment-naming:
name: News fragment naming (ID + KAC type)
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Validate all newsfragments
run: |
python scripts/validate_newsfragments.py
- name: On PR, changed fragments must match this PR number
if: github.event_name == 'pull_request'
run: |
python scripts/validate_newsfragments.py \
--pr "${{ github.event.number }}" \
--diff "${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
towncrier-fragment-gate:
name: Changelog fragment gate (Towncrier)
runs-on: ubuntu-latest
if: ${{ github.event_name == 'pull_request' }}
permissions:
pull-requests: read
steps:
- name: Detect source and fragment changes
id: changes
uses: dorny/paths-filter@v4
with:
filters: |
code:
- 'src/**'
- '.github/workflows/**'
- 'pyproject.toml'
fragment:
- 'newsfragments/*.added.md'
- 'newsfragments/*.changed.md'
- 'newsfragments/*.deprecated.md'
- 'newsfragments/*.removed.md'
- 'newsfragments/*.fixed.md'
- 'newsfragments/*.security.md'
changelog:
- 'CHANGELOG.md'
- name: Enforce changelog fragment for code changes
uses: actions/github-script@v9
env:
CODE_CHANGED: ${{ steps.changes.outputs.code }}
FRAGMENT_CHANGED: ${{ steps.changes.outputs.fragment }}
CHANGELOG_CHANGED: ${{ steps.changes.outputs.changelog }}
with:
script: |
const pr = context.payload.pull_request;
if (!pr) {
core.setFailed("Expected pull_request payload.");
return;
}
const { data } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
});
const labels = (data.labels ?? []).map((l) => l.name);
const hasSkipLabel = labels.includes("skip-changelog") || labels.includes("internal-only");
const codeChanged = process.env.CODE_CHANGED === "true";
const fragmentChanged = process.env.FRAGMENT_CHANGED === "true";
const changelogChanged = process.env.CHANGELOG_CHANGED === "true";
if (!codeChanged) {
core.info("No relevant code/workflow changes; fragment gate passes.");
return;
}
if (fragmentChanged || changelogChanged) {
core.info("Fragment/changelog update detected; fragment gate passes.");
return;
}
if (hasSkipLabel) {
core.info(
"No changelog fragment, but PR has skip-changelog/internal-only label; gate passes."
);
return;
}
core.setFailed(
"This PR changes code/workflows without a changelog fragment. " +
"Add newsfragments/<PR>.(added|changed|deprecated|removed|fixed|security).md, update CHANGELOG.md, " +
"or apply `skip-changelog`/`internal-only` with reviewer approval."
);
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
with:
python-version: "3.12"
enable-cache: true
- name: Sync dependencies
run: |
uv sync --locked --dev
- name: Lint
run: |
uv run ruff check .
- name: Format
run: |
uv run ruff format --check .
type-check:
name: Type Check
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
with:
python-version: "3.12"
enable-cache: true
- name: Sync dependencies
run: |
uv sync --locked --dev
- name: Type check
run: |
uv run ty check .
test:
name: Tests (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
- os: windows-latest
- os: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
with:
python-version: "3.12"
enable-cache: true
- name: Sync dependencies
run: |
uv sync --locked --dev
# PySide6 links libEGL even for offscreen; ubuntu-latest images omit it by default.
- name: Install Qt EGL/XCB dependencies (Ubuntu only)
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libegl1 \
libgl1 \
libxkbcommon0 \
libdbus-1-3
- name: Run pytest
env:
QT_QPA_PLATFORM: offscreen
run: |
uv run pytest --cov-fail-under=48
prose-lint:
name: Prose lint (codespell + doc8 + interrogate)
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
with:
python-version: "3.12"
enable-cache: true
- name: Run pre-commit (prose hooks)
run: |
uv run pre-commit run --all-files codespell
uv run pre-commit run --all-files doc8
uv run pre-commit run --all-files interrogate
docs:
name: Docs (build + linkcheck)
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
with:
python-version: "3.12"
enable-cache: true
# autosummary imports dbs_annotator modules that transitively touch
# PySide6; ubuntu-latest images omit the EGL/XCB libs that PySide6 needs
# even under QT_QPA_PLATFORM=offscreen.
- name: Install Qt EGL/XCB dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libegl1 \
libgl1 \
libxkbcommon0 \
libdbus-1-3
- name: Sync docs dependency group
run: |
uv sync --locked --group docs --no-dev
- name: Verify generated TSV schema docs are up to date
run: |
uv run python scripts/generate_tsv_schema_docs.py --check
- name: Build HTML docs (warnings are errors)
env:
QT_QPA_PLATFORM: offscreen
MPLBACKEND: Agg
run: |
uv run sphinx-build -b html -W --keep-going docs docs/_build/html
- name: Link check (non-blocking)
continue-on-error: true
env:
QT_QPA_PLATFORM: offscreen
MPLBACKEND: Agg
run: |
uv run sphinx-build -b linkcheck docs docs/_build/linkcheck
- name: Upload rendered HTML artifact
uses: actions/upload-artifact@v7
with:
name: docs-html-${{ github.sha }}
path: docs/_build/html
if-no-files-found: error
- name: Upload linkcheck report
if: always()
uses: actions/upload-artifact@v7
with:
name: docs-linkcheck-${{ github.sha }}
path: docs/_build/linkcheck
if-no-files-found: warn
# Briefcase smoke test. Intentionally runs on Ubuntu (not Windows) and
# skips `briefcase package`, because:
# - ~80% of Briefcase regressions (bad [tool.briefcase] config, missing
# runtime deps, stale icon/stub paths, undeclared resources) are
# platform-agnostic and caught by `create` + `build`.
# - Packaging is the slowest step and rarely fails when build succeeds.
# - The tagged-release workflow (release.yml) fully exercises
# `create + build + package` for Windows MSI, macOS DMG, and Linux
# system packages, so platform-specific packaging regressions cannot
# reach a release without being caught there.
# The result is a PR-gated smoke test that runs in ~1-2 min instead of
# ~3-4 min on a Windows runner.
briefcase-smoke:
name: Briefcase smoke test (Linux)
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
with:
python-version: "3.12"
enable-cache: true
# PySide6 links libEGL even for offscreen; ubuntu-latest images omit it by default.
- name: Install Qt EGL/XCB dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libegl1 \
libgl1 \
libxkbcommon0 \
libdbus-1-3
- name: Sync dependencies (incl. build / Briefcase)
run: |
uv sync --locked --dev --group build
# Mirror release.yml so Briefcase installs from the same lock-derived
# constraints the real release build uses.
- name: Export lock-derived constraints for Briefcase
run: |
uv export --locked --format requirements.txt --no-dev --no-hashes --no-emit-project --no-emit-workspace --output-file constraints-briefcase.txt
- name: Briefcase create + build (linux system)
env:
QT_QPA_PLATFORM: offscreen
PIP_INDEX_URL: https://pypi.org/simple
PIP_EXTRA_INDEX_URL: ""
PIP_NO_CACHE_DIR: "1"
run: |
uv run briefcase create linux system --no-input
uv run briefcase build linux system --no-input