Skip to content
Merged
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
15 changes: 15 additions & 0 deletions .github/ci-tools/audit/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# The Desktop dependency audit's tool, locked apart from ../ because it needs
# Python 3.11 or newer: pip-audit pulls filelock through cachecontrol, and the
# filelock releases that fix GHSA-v4x8-3wr5-3wq8 dropped Python 3.10. The
# audit runs on 3.12 only (.github/workflows/desktop-dependency-audit.yml).
[project]
name = "openadapt-ci-audit"
version = "0"
requires-python = ">=3.11"
dependencies = []

[dependency-groups]
audit = ["pip-audit>=2.10.1"]

[tool.uv]
package = false
613 changes: 613 additions & 0 deletions .github/ci-tools/audit/uv.lock

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions .github/ci-tools/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Locked tools for CI jobs that test no package of their own: the launcher's
# Python-support and platform-manifest tests, wheel builds and the package
# release build. Each group is one job's set. The Desktop audit's pip-audit
# has its own lock in audit/, because it needs Python 3.11.
# Jobs install a group with scripts/ci/locked_install.py (hashes from
# uv.lock) or `uv run --locked --only-group`, so no tool version floats.
# Dependabot proposes updates to the lock weekly (.github/dependabot.yml).
[project]
name = "openadapt-ci-tools"
version = "0"
requires-python = ">=3.10"
dependencies = []

[dependency-groups]
test = ["pytest>=9.1.1", "packaging>=25.0"]
build = ["build>=1.6.1", "pip>=25.3"]
pip = ["pip>=25.3"]
lint = ["ruff==0.17.0"]
release = ["build>=1.6.1", "twine>=7.0.0"]

[tool.uv]
package = false
906 changes: 906 additions & 0 deletions .github/ci-tools/uv.lock

Large diffs are not rendered by default.

75 changes: 34 additions & 41 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,61 +1,54 @@
# SOC 2: automated dependency updates (OA-C-23).
#
# Each ecosystem and directory opens at most one version-update pull request a
# week: every update is in one group (`patterns: ["*"]`). CI runner time is
# the scarce resource (the org is on the free GitHub Actions plan), and one
# grouped PR runs CI once instead of once per dependency. Security updates are
# not grouped and still open as they are published.
# Each entry opens at most one version-update pull request per group a week.
# CI runner time is the scarce resource (the org is on the free GitHub Actions
# plan), and one grouped PR runs CI once instead of once per dependency.
# Security updates are not grouped and still open as they are published.
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
labels:
- "dependencies"
groups:
pip-root:
patterns: ["*"]
# packages/capture keeps its own uv.lock. The uv ecosystem updates it with
# pyproject.toml, so the capture lanes' `uv sync --locked` accepts the bump.
# Every uv.lock except Desktop's, in one entry. CI installs from these locks
# (scripts/ci/locked_install.py, `uv sync --locked`), so a test tool moves
# only when a lock changes. Two weekly pull requests at most, each across
# all of these directories, so every package moves to the same version:
# the test tools in one, everything else in the other.
- package-ecosystem: "uv"
directory: "/packages/capture"
directories:
- "/"
- "/packages/types"
- "/packages/flow"
- "/packages/agent"
- "/packages/capture"
- "/packages/privacy"
- "/packages/tray"
- "/.github/ci-tools"
- "/.github/ci-tools/audit"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
labels:
- "dependencies"
groups:
uv-capture:
# Listed first: a dependency joins the first group it matches.
test-tools:
patterns:
- "pytest"
- "pytest-*"
- "hypothesis"
- "coverage"
- "ruff"
- "mypy"
- "playwright"
- "build"
- "pip"
- "pip-audit"
- "twine"
python:
patterns: ["*"]
ignore:
# Keep PyGObject >=3.46,<3.51 so the Linux extra installs on GLib 2.64
# distros; 3.51+ needs newer GLib. See packages/capture/docs/DESIGN.md.
- dependency-name: "pygobject"
versions: [">=3.51"]
# packages/privacy keeps its own uv.lock, like packages/capture.
- package-ecosystem: "uv"
directory: "/packages/privacy"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
labels:
- "dependencies"
groups:
uv-privacy:
patterns: ["*"]
# packages/tray keeps its own uv.lock, like packages/capture.
- package-ecosystem: "uv"
directory: "/packages/tray"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
labels:
- "dependencies"
groups:
uv-tray:
patterns: ["*"]
# packages/desktop: its uv.lock, the frontend's package-lock.json and the
# Tauri shell's Cargo.lock, each in one weekly group.
- package-ecosystem: "uv"
Expand Down
128 changes: 91 additions & 37 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@ The org is on GitHub's free Actions plan: 20 concurrent jobs, 5 of them
macOS. Every pull request and every queue group competes for those runners.
To keep pull requests fast, CI runs in three tiers:

- **PR tier** (`pull_request`): one fast Linux lane. That's one Python
version (3.12) and only the packages a change touches, including Flow's
unit suite. The checkout is the pull request merged with main, so this tier
already tests the change on top of main.
- **PR tier** (`pull_request`): one required check, `ci` (ci.yml), that
finishes in under ten minutes. ci.yml calls packages.yml and main.yml on
this tier: lint, the oldest-Python syntax check, and the Linux tests of the
packages a change touches, on Python 3.12. Flow's unit shards leave out the
slow tests listed in `packages/flow/tests/nightly_only.txt`. Those are tests
over about 5 s on CI, except the realism tests that guard safety, which stay.
Flow's critical tests run in two parallel steps, end-to-end and unit. A smoke test
(`scripts/ci/smoke.py`) installs every package from its lock, imports it
and runs its command with `--help`, whatever changed. The checkout is the
pull request merged with main, so this tier tests the change on top of main.
- **Queue tier** (`merge_group`), when the merge queue is on (ruleset
25486718; it is off as of 2026-10-25, so a pull request merges once its
PR-tier checks pass and the PR tier is the only gate before main): a lean re-check of the group on top of the
Expand All @@ -25,47 +31,63 @@ To keep pull requests fast, CI runs in three tiers:
(including the native installers) ran, so no Desktop release ships without
this tier.

- **Nightly** (`nightly.yml`, daily at 07:23 UTC and on dispatch): calls
Packages (with the Desktop test and build workflows) and the launcher CI as
a scheduled run, so every package's full suite runs against main on every
platform its CI covers, whatever changed. The merge queue's push and a pull
request run only the packages a change touches; this catches a package
nobody touched breaking on main. When a job fails, the run opens the
`main is red` issue (label `main-is-red`), or updates it if one is open. A
passing run closes it. The nightly run is not a required check.

**Drafts run nothing.** Every job in a workflow that runs on pull requests
checks `github.event.pull_request.draft != true`, and `ready_for_review` is a
trigger, so marking a draft ready starts CI. `packages-gate` fails on a draft
trigger, so marking a draft ready starts CI. `ci` fails on a draft
with "draft PR: CI runs when marked ready for review", so a draft never shows
a stale green. A draft can't merge anyway.

Nothing that used to run before merge was dropped: every job removed from the
PR and queue tiers runs in the post-merge tier.
Nothing that used to run before merge was dropped: every job or test removed
from the PR and queue tiers runs in the post-merge tier and nightly.

## Required checks

Branch protection requires these five checks, both on pull requests and in the
merge queue:
`ci` is the check a pull request needs, with `gitleaks` and
`dependency-review`:

| Check | Workflow | On a PR | In the queue |
|---|---|---|---|
| `ci` | ci.yml | runs; fails on a draft | runs |
| `gitleaks` | secret-scan.yml | runs | runs |
| `dependency-review` | dependency-review.yml | runs | runs |
| `Analyze (python)` | codeql.yml | reports; analyzes when Python files change | reports; analyzes when the group changes Python files |
| `run-ci (ubuntu-latest, 3.12)` | main.yml | runs (skips the suite for package-only changes) | runs |
| `packages-gate` | packages.yml | runs; fails on a draft | runs |

`packages-gate` needs every job in packages.yml. It fails when a job fails. It
also fails when a job the change selects was skipped, except on a tier where
that job doesn't run:

- Post-merge-only jobs (`capture-macos`, `capture-windows`) may be skipped on
a pull request and in the queue, and must run and pass on main and on
dispatch.
- `flow-unit` may be skipped in the queue only. A pull request can't enter
the queue until `packages-gate` passed with it green.

The `ci` job needs Packages, the launcher CI and the smoke test. It fails
unless each one passed. Inside Packages, `packages-gate` still requires every
job the change selects to run and pass, except on a tier where that job
doesn't run:

- Post-merge-only jobs (`capture-macos`, `capture-windows`,
`flow-native-census`) may be skipped on a pull request and in the queue,
and must run and pass on main and on dispatch.
- `flow-unit` may be skipped in the queue only.
- `desktop-release-intent` runs on pull requests only.

Inside the reusable desktop workflows, the native installers and the macOS and
Windows legs run only post-merge. The gate sees each reusable workflow as one
result, so a tier-skipped job inside it is never "skipped although selected".
packages.yml and main.yml no longer trigger on pull requests or the merge
queue themselves. ci.yml calls them, so each job runs once per pull request.

Jobs in other workflows that run only post-merge, such as main.yml's macOS
and older-Python legs and the macOS Intel audit, are not required checks.
Adding their check names to branch protection would block every pull request,
because they never report there.
**Switching branch protection.** Until branch protection names `ci`, ci.yml
also reports the two old required names, `run-ci (ubuntu-latest, 3.12)` and
`packages-gate`, with `ci`'s result. CodeQL keeps reporting
`Analyze (python)` from codeql.yml, as a non-required check after the
switch. An admin switches it with:

```bash
gh api -X PATCH repos/OpenAdaptAI/OpenAdapt/branches/main/protection/required_status_checks --input - <<'EOF'
{"strict": false, "checks": [{"context": "ci", "app_id": 15368}, {"context": "gitleaks", "app_id": 15368}, {"context": "dependency-review", "app_id": 15368}]}
EOF
```

Then remove the `legacy-run-ci` and `legacy-packages-gate` jobs from ci.yml.
App 15368 is GitHub Actions, the app the current checks are pinned to.

## Jobs by tier

Expand All @@ -75,7 +97,9 @@ because they never report there.
| main.yml | `unsupported-python-guard` | no | no | Python 3.13 and 3.14 |
| packages.yml | `types`, `agent` | Python 3.12 | Python 3.12 | Python 3.10 and 3.12 |
| packages.yml | `flow-lint`, `flow-critical` | yes | yes | yes |
| packages.yml | `flow-unit` | 2 xdist shards | no | 2 xdist shards |
| ci.yml | `smoke`, `ci` | yes | yes | no |
| packages.yml | `flow-unit` | 2 xdist shards, fast lane (without `nightly_only.txt`) | no | 2 xdist shards |
| packages.yml | `flow-native-census` | no | no | Windows and macOS |
| packages.yml | `capture-linux` | Python 3.12 | Python 3.12 | Python 3.10 (ubuntu-22.04), 3.11, 3.12 |
| packages.yml | `capture-macos`, `capture-windows` | no | no | yes |
| packages.yml | `capture-contract`, `capture-core-record` | yes | yes | yes |
Expand Down Expand Up @@ -129,11 +153,9 @@ Three kinds of workflow keep their own groups:

## Caches

The Flow and agent jobs install with `uv pip install --system` into the
`actions/setup-python` interpreter, with the uv cache keyed on the pyproject
files they resolve; uv takes seconds where pip took two to three minutes. The
other pip jobs use `cache: pip`. Every other `setup-uv` step sets
`enable-cache: true` and keys the cache on its package's own `uv.lock`.
Every `setup-uv` step sets `enable-cache: true` and keys the cache on the
`uv.lock` files its job installs from; uv takes seconds where pip took two to
three minutes.
npm uses `cache: npm`. The Tauri builds use `Swatinem/rust-cache`, and the
Rust audit caches its `cargo-audit` binary. The release workflows keep caching
off on purpose: release builds start from a clean cache.
Expand All @@ -152,7 +174,39 @@ are branch-protection settings, so an admin changes them. How GitHub fills a
group at those values hasn't been tested here; check the result on the next
few queue runs.

## Locked installs

Every job installs Python packages from a uv lockfile, test tools included, so
nothing changes version between two runs of the same commit. A floating
pytest 9.2 once broke flow-unit on every pull request at once.

- Jobs that own a package directory run `uv sync --locked`, then
`uv run --no-sync` or `uv run --locked`.
- Jobs that install into the setup-python interpreter run
`scripts/ci/locked_install.py`. It exports each named lock with
`uv export --locked` and installs it with `--require-hashes --no-deps`.
In-tree packages then install editable without dependencies, and
`uv pip check` fails the job if one of them needs something no lock has.
Where an in-tree sibling needs newer dependencies than the published copy
another lock names, the job adds the sibling's own lock after it. For
example, flow-unit installs Flow's lock, then Capture's.
- Jobs that test no package of their own (Python support, platform manifest,
wheel builds, release builds) use the groups in `.github/ci-tools/uv.lock`.
The Desktop audit's pip-audit has its own lock in `.github/ci-tools/audit/`,
because the fixed filelock it pulls in needs Python 3.11.

When a job fails with "The lockfile at `uv.lock` needs to be updated", run
`uv lock` in that directory and commit the lock.
`tests/test_ci_locked_installs.py` fails on any workflow install that resolves
at run time. The few that do on purpose are listed there with their reasons:
the launcher's check of what a user's pip picks, the uv bootstrap and the
Harness² commit pin.

## Dependabot

Each ecosystem and directory opens at most one grouped version-update pull
request a week (`.github/dependabot.yml`). Security updates are not grouped.
`.github/dependabot.yml` opens grouped version-update pull requests weekly.
One uv entry covers every lock except Desktop's and opens at most two pull
requests a week, each across all of those directories: `test-tools` (pytest
and its plugins, ruff, mypy, Playwright, build, pip, pip-audit, twine) and
`python` (everything else). Desktop's uv, npm and cargo locks keep their own
entries. Security updates are not grouped.
Loading
Loading