Skip to content

CI execution policy

Routine CI uses representative combinations instead of a Cartesian version/OS matrix.

Trigger Runtime validation
Documentation-only PR Documentation and policy checks; no runtime suite or CUBRID provisioning
Ordinary code PR One Ubuntu/Python 3.12 offline smoke lane; no full coverage claim
High-risk PR Full offline regressions without coverage on Python 3.11 and 3.15, plus Python 3.14/CUBRID 11.4; targeted additional lanes where relevant
Code push to main Full offline suite on Ubuntu/Python 3.11 and 3.15, each with the existing 95% coverage floor; live endpoints Python 3.14/CUBRID 11.4 and Python 3.11/CUBRID 10.2
Monday 03:00 UTC Same policy as a main push (including the Python 3.11/3.15 offline suite), comparing changes in the previous seven days; unchanged/docs-only history does not select runtime tests, and lanes a successful push run of the same SHA already ran are not repeated (see Event tiers and cost evidence)
Explicit full dispatch or release Existing full Python 3.11–3.15 × CUBRID 10.2/11.0/11.2/11.4 integration workflow and mandatory release lanes, plus the Python 3.11/3.15 offline suite with the 95% floor, lint, type checking, the public API baseline and the repository tooling tests

The PR smoke suite is deliberately bounded. Contributors are encouraged to run regression checks relevant to their change locally and record actual commands/results or reasons for any skipped checks. Required GitHub CI remains the authoritative PR merge validation. Passing smoke is not evidence that the whole offline suite or coverage floor ran. make test and make integration remain available without changing their scope.

Change selection is in ci.yml's detect-changes job. Non-documentation paths are code by default, so new source/configuration files do not silently become docs. All driver and test paths, including new modules, plus dependency, build and script changes and ci.yml itself conservatively select representative pre-merge integration and the existing offline regression suite on the oldest and newest supported Python (see Offline endpoint versions). Other workflow changes select the repository-tooling lane instead (see Workflow change impact). Other code changes retain the bounded smoke suite. Repository tooling tests run in one Linux lane when tooling changes. Every test module with repo_tooling tests (such as tests/test_official_fixture_setup.py and tests/test_upstream_scenario_ledger.py) and the files only those tests read (such as tests/fixtures/upstream_scenarios.csv, agent-evals/**, THIRD_PARTY_LICENSES.md, release-please-config.json and api-baseline.json) explicitly select that same tooling lane, and a guard in tests/test_workflow_path_impact.py, which the always-on lint job also runs, fails when such a module does not; these marker-based checks are excluded from the driver offline lane. The static lint job continues on all events, including generated documentation checks.

The aggregate required-check name stays stable and includes change detection. Selected jobs must succeed: skipped, failed or cancelled selected checks fail the gate. Only intentionally unselected jobs may skip. Keep branch protection on the aggregate gate; do not require every old matrix cell name after reducing matrices. Branch protection must be inspected before merge. No protection-setting change is part of this PR.

Full verification has no automatic nightly schedule. integration-full.yml retains manual dispatch and the release workflow_call with the immutable candidate SHA. Both manual workflows require a full sha input matching the dispatched branch commit. Supply pr_number when collecting PR evidence: preflight and the final gate reject a closed PR, an API failure or a superseded head. Every checkout uses the immutable requested/run SHA, which the guard reports in the summary. Routine CI manual dispatch forces all representative runtime/tooling lanes, even with an empty default-branch diff; the full workflow forces the full matrix. No release publisher/generator is changed.

Concurrency isolates event/ref groups and still cancels superseded PR runs. Main pushes and PR merge refs are not assumed to have identical SHAs. Weekly change selection uses the previous seven days, not a persisted last-success cache; a failed weekly run must be rerun or followed by manual validation rather than treated as successful evidence. The weekly run then drops a lane family only when a successful push run of the exact same SHA already ran it (#750).

GitHub charges have not been attributed to a workflow or runner SKU. Reduced job counts establish less repeated work; they are not a measured monetary saving. Compare subsequent Actions jobs/runner minutes and the actual billing category before making a cost claim.

The deep bug-hunt workflow runs weekly (Thursday 04:00 UTC) with one Python 3.12/CUBRID 11.4 cell, skipping unchanged weeks and retaining its mutation/performance/downstream checks and wide Hypothesis profile. TLS, EUC-KR and official differential jobs are selected by related paths on PRs; they remain available in main, changed-weekly and full release validation.

Event tiers and cost evidence

#750 gives each expensive event one purpose. Pull requests stay fast and representative. main pushes carry the endpoint evidence. The weekly schedules cover what a push run did not already prove at the same SHA, plus the deep bug hunt. The release runs the full matrix and fails closed. Changes were made only where another lane makes the same assertions on the same SHA.

Measurements

Measured with the GitHub REST API (actions/workflows/<file>/runs and actions/runs/<id>/jobs?filter=latest) on 2026-10-09, before this change.

  • Runner minutes: the sum of completed_at - started_at over each executed job (latest attempt).
  • Rounded minutes: each job rounded up to a whole minute, which is how GitHub meters. The repository is public on standard runners, so neither is a billed charge.
  • Wall minutes: the first job start to the last job end.
  • Executed job: conclusion neither skipped nor empty. Runs where no job executed are left out. These are 120 pull-request runs waiting for approval (action_required) and 4 that failed at startup.
  • Failed / cancelled: run conclusions. Cancelled runs are listed apart: every one in the sample was superseded by a newer run in the same concurrency group.

The window starts at 2026-10-03 06:04 UTC, after a65360a introduced the current representative tiers. The weekly and push samples predate #745's 3.11 and 3.14 offline cells (landed 2026-10-09 02:06 UTC): the 2026-10-05 weekly run had a single offline-tests (3.12) cell, and only 2 of the 51 code push runs are post-#745. The release sample uses its current workflow shape. The ci.yml runs are classified as follows:

  • Pull requests: a risk PR executed any of integration-tests, integration-tls, integration-charset or official-differential, or an offline-tests cell on Python 3.11. Otherwise an ordinary code PR executed offline-tests. Any other PR is docs-only.
  • Pushes: a code push executed offline-tests.
  • Releases: publish-pypi.yml push runs in which the Full compatibility matrix jobs ran.
Category Window (runs) Median jobs Median runner min Median rounded min Median wall min Failed / cancelled
Docs-only PR 2026-10-03..09 (24) 8 1.0 8 0.7 0 / 0
Ordinary code PR 2026-10-03..08 (10) 11.5 2.3 11.5 1.0 3 (lint) / 1
Risk PR 2026-10-03..09 (91) 15 7.5 18 3.6 4 (lint, one also typecheck) / 8
Main push, code 2026-10-03..09 (50) 17 10.7 23 4.0 0 / 5
Main push, no code 2026-10-03..09 (17) 8 1.0 8 0.7 0 / 0
Weekly ci.yml (Monday 03:00) 2026-10-05 (1) 18 11.4 24 4.2 0 / 0
Weekly bug-hunt.yml (Monday 04:00) 2026-10-05 (1) 7 149.4 152 83.0 1 (mutation: runner shutdown after 82.8 min) / 0
bug-hunt.yml dispatch 2026-10-04 (2) 7 110.1 112.5 74.6 0 / 1
integration-full.yml dispatch 2026-10-04 (3) 27 40.6 53 8.4 0 / 0
Release (publish-pypi.yml with integration-full.yml) 2026-10-04..09 (2) 34 41.8 60.5 10.2 1 (cookbook verification after publish) / 0
  • No defect findings in the scheduled lanes. No live lane, official differential, TLS or charset job failed on any push, weekly, dispatch or release run in the window. Every PR failure was lint, plus one typecheck failure.
  • The weekly bug-hunt.yml failure was infrastructure. Its only failure was a runner shutdown during mutation testing.
  • Earlier bug-hunt shape. The nightly 15-cell bug-hunt shape (2026-09-18 to 10-02, 15 runs) had a median of 739 runner minutes. Every run failed, in downstream dogfooding and the mutmut 2.x configuration, both since fixed.
  • Samples are small. The weekly samples are one run each, because both workflows moved to the weekly shape in early October. The release sample has two runs.

Median per-job runner minutes on successful code pushes:

  • integration-tests: 1.9 (3.14/11.4) and 1.5 (3.11/10.2)
  • offline-tests: 1.7 (3.12, before #745), 1.5 (3.11), 1.4 (3.14)
  • integration-tls: 1.4
  • official differential: 1.2
  • integration-charset: 0.9
  • repo-tooling-tests: 0.7
  • typecheck, packaging and lint: 0.3 each
  • public API check: 0.2

Coverage ownership

Coverage Owner (workflow / job) Event tier
Offline suite, 95% floor ci.yml offline-tests; integration-full.yml offline-endpoints Ordinary PR: 3.12 smoke. Risk PR: 3.11 + 3.15 without coverage. Main push, weekly, dispatch: 3.11 + 3.15. Release and full dispatch: 3.11 + 3.15
Live endpoints ci.yml integration-tests; integration-full.yml integration-full (5 × 4) Risk PR: 3.14/11.4. Main push, weekly, dispatch: plus 3.11/10.2. Release and full dispatch: all 20 cells
TLS ci.yml integration-tls (3.14/11.4); integration-full.yml integration-tls (3.11 + 3.14 + 3.15) TLS-path PR, non-PR code; release and full dispatch
EUC-KR charset ci.yml and integration-full.yml integration-charset Charset-path PR, non-PR code; release and full dispatch
Official CUBRIDdb differential ci.yml and integration-full.yml official-differential Official-path PR, non-PR code; release and full dispatch
CUBRID version differential integration-full.yml version-differential Release and full dispatch
Property, protocol, state-machine, fault, chaos, soak (wide profile) bug-hunt.yml property-and-fault Thursday 04:00 UTC with changes in the previous 7 days, and dispatch
Mutation testing bug-hunt.yml mutation Same as bug hunt
Benchmark trend bug-hunt.yml perf-trend Same as bug hunt
Downstream corpus (advisory) bug-hunt.yml downstream-corpus Same as bug hunt
Lint, format, Bandit, docs-reason examples (make check without typecheck), CHANGELOG structure, translation structure, llms-full.txt sync ci.yml lint; integration-full.yml lint (same steps) Every event; release and full dispatch
Type checking (the only mypy run in each workflow) ci.yml typecheck; integration-full.yml typecheck (same steps) Every code event; release and full dispatch
Public API baseline ci.yml compat-check; integration-full.yml compat-check (same steps) Every code event; release and full dispatch
Packaging ci.yml packaging-smoke-test; publish-pypi.yml consistency and build Risk PR, non-PR code; release
Repository tooling ci.yml repo-tooling-tests; integration-full.yml repo-tooling-tests (same steps) Tooling-path events; release and full dispatch
Python 3.15 packaging check (advisory) python-canary.yml Manual only

Consolidation

  • The weekly ci.yml run reuses same-SHA push evidence. The one weekly run in the window (2026-10-05, c2a4f1db) re-ran, at the same SHA, the 17 jobs that the push run had passed five hours earlier, plus repo-tooling-tests.
  • Why the runs match. Both are non-PR events, so they select the same matrix cells and run the same commands from the same ci.yml.
  • What the weekly run now does. Its detect-changes job (with read-only actions: read) looks up push runs of the exact SHA. It drops the code lanes when one successful push run executed offline-tests, and the repository-tooling lane when one executed repo-tooling-tests, with no failed or skipped cell.
  • When it still runs everything. A docs-only head commit, a head push run that failed or was cancelled, or a failed lookup selects the lanes exactly as before.
  • Why the backstop stays. Since 2026-09-01, 3 of 16 cancelled code push runs were superseded by a run that selected no code lanes, so the 7-day diff stays the backstop for a cancelled push.
  • Drift is not this lane's job. The weekly run never owned dependency or runner-image drift, because quiet weeks already select nothing.
  • bug-hunt.yml moves from Monday 04:00 UTC to Thursday 04:00 UTC. It no longer stacks on the Monday ci.yml, CodeQL, SBOM, security and maintenance schedules. A later cron start does not mean that the earlier run finished.
  • Activity guard. It is unchanged: a schedule runs only with changes in the previous 7 days.
  • No duplicate to remove. Its integration and not slow and not tls session uses Python 3.12 and the wide Hypothesis profile. That makes it a different cell and profile from ci.yml's 3.11/3.15 cells, so it stays.
  • The release-merge overlap is kept.
  • The overlap. On a release commit, the ci.yml push run's live lanes (integration-tests 3.14/11.4 and 3.11/10.2, TLS 3.14/11.4, charset, official differential) are a subset of integration-full.yml on the same SHA, with identical commands. That is about 6.9 runner minutes per release (two releases in the window).
  • Why it stays. Skipping them would make the push run's evidence depend on a release detection that can stop before the matrix (consistency failure). A code commit would then have no live evidence anywhere.
  • Pull requests and main pushes are unchanged.

Expected effect, computed from the measured runs above:

Category Jobs before → after Runner min before → after Rounded min before → after
Ordinary code PR, risk PR, main push unchanged unchanged unchanged
Weekly ci.yml, head push run green with code lanes 19 → 9 about 13 → about 1.8 about 26 → 9
Weekly ci.yml, head push run not green or docs-only 19 → 19 about 13 → about 13 about 26 → about 26
Weekly bug-hunt.yml 7 → 7 (Thursday) 149.4 → 149.4 152 → 152
Release (with the release-run copies of lint, typecheck, public API and repository tooling) 34 → 40 41.8 → about 46.2 60.5 → about 68.5

These are expectations. Compare them with the next weeks of Actions data before claiming a saving.

Release evidence

publish-pypi.yml calls integration-full.yml at the release SHA through workflow_call. build and publish need that matrix job, so a failed, cancelled or skipped matrix stops publication. full-matrix-result fails unless every dependency reports success.

The release does not read any ci.yml run. Before #750, the Python 3.11/3.14 offline suite with the 95% floor (#745) at the release commit came only from the ci.yml push run.

  • Nothing required that run. No step in the release path checked it.
  • A later merge could cancel it. The ci-push-refs/heads/main concurrency group cancels an older push run when a newer push arrives. That happened to 16 push runs since 2026-09-01.

integration-full.yml now runs offline-endpoints. It uses the same Python versions as ci.yml (3.11 and 3.15, the oldest and newest supported) with the same install and pytest command as ci.yml offline-tests, at the exact SHA. full-matrix-result requires it. This adds about 2.9 runner minutes per release.

The same gap covered the other main push evidence, and the maintainer chose to close it the same way (2026-10-11, as in sqlalchemy-cubrid #737). integration-full.yml also runs copies of ci.yml lint, typecheck, compat-check (the public API baseline) and repo-tooling-tests, with the same Python version, timeout and steps, at the exact SHA. Three things differ from ci.yml, and tests/test_ci_policy.py keeps everything else equal:

  • No changed-path selection. Each copy needs only validate-target, so it runs on every manual dispatch and release call. A skipped copy can only follow a failed validate-target, which already fails the gate.
  • Checkout. The checkout sets persist-credentials: false, like the other integration-full.yml jobs.
  • Cache mode. setup-uv uses the auto cache mode of this workflow.

full-matrix-result lists all four in needs and fails on any result other than success, so a failed, cancelled, skipped or missing copy blocks publication. publish-pypi.yml is unchanged, because it is kept identical across cubrid-lab repositories. The alternative, making main push runs non-cancellable and having the publisher require them, would have changed it. From the per-job medians above, the copies add about 1.5 runner minutes (4 rounded minutes) per release or full dispatch.

Offline endpoint versions

The offline-tests job picks its Python matrix from the event (#745), so the full offline suite runs on the oldest (3.11) and newest (3.15) supported Python wherever it already ran in full, while ordinary PRs stay on one cheap cell.

Event offline-tests cells Suite
Ordinary code PR (risk not selected) Python 3.12 Representative smoke tests
Risk-selected PR Python 3.11 and 3.15 Full offline suite, no coverage
Push to main, Monday schedule, manual dispatch Python 3.11 and 3.15 Full offline suite with the 95% coverage floor
Release and full dispatch (integration-full.yml offline-endpoints) Python 3.11 and 3.15 Same install and command, required by full-matrix-result

Every cell keeps the not integration and not repo_tooling selection, the 15-minute timeout and the immutable inputs.sha || github.sha checkout. In ci.yml, coverage reports are written per Python version (coverage-py<version>.xml), uploaded as the offline-coverage-py<version> artifact and sent to Codecov with the offline-py<version> flag, so the two cells never overwrite each other. The integration-full.yml offline-endpoints cells upload nothing.

The weekly schedule is owned by ci.yml itself: integration-full.yml has no schedule (it repeats the two cells only for a release or a full dispatch), and bug-hunt.yml runs targeted property and fault suites, so no other weekly lane duplicates this evidence (#750). Python 3.11 here is the latest 3.11 patch release from actions/setup-python; it does not exercise 3.11.0–3.11.2, which keep their targeted regression (#744).

ci-gate sees one aggregate offline-tests result. A failed or cancelled cell makes that result non-success, and a skipped job is accepted only when no code changed, or, on the weekly schedule, when a successful push run of the same SHA already ran it, so a missing or failed endpoint cell cannot turn the gate green.

Workflow change impact

Changed-path selection follows a per-workflow impact table (#761). Only ci.yml selects the live PR lanes (risk, tls, charset, official), because it defines and runs them. Every other workflow under .github/ selects the repository-tooling lane, whose policy and workflow tests parse and check it; the live lanes of ci.yml do not execute another workflow's jobs, so running them adds no detection.

Changed workflow PR validation
ci.yml All lanes it defines, plus tooling
integration-full.yml Tooling, plus a manual workflow_dispatch of integration-full.yml on the PR head, linked in the PR
publish-pypi.yml, release-please.yml Tooling (release workflow tests)
bug-hunt.yml, python-canary.yml Tooling; dispatch when the run itself changes
Other workflows Tooling; pr-title.yml and docs-sync.yml also run themselves on the PR

tests/test_workflow_path_impact.py evaluates the filters against every workflow file and against representative source, test and documentation paths.

Parallel live lanes

integration-tests, integration-charset, integration-tls and official-differential depend only on validate-target and detect-changes (#760), so they start alongside lint, type checking and offline tests instead of after them. validate-target stays a dependency because it verifies the manual dispatch SHA against the PR head. ci-gate still needs every job, so a lint, type or offline failure keeps the gate red even when the live lanes pass. The trade-off is that a PR that fails lint can still spend live-lane runner time.

Job timeouts

Every executing job sets an integer timeout-minutes (GitHub's default is 360 minutes), so a hung container, socket or install fails within its budget instead of holding a runner for six hours (#758). Short jobs get roughly 3–5× their observed maximum Actions duration with a floor: 5 minutes for gates and small jobs, 10–15 for lint/type/offline/tooling and 20–30 for live integration lanes. The two long weekly jobs are explicit exceptions with smaller multipliers. Property/fault/soak gets 120 (about 1.9× its observed 63; its soak step is separately capped at 90). Mutation testing runs as four shards of roughly equal mutant counts (#750): about 1,550–1,850 of the 6,812 mutants each, so about 30 minutes per shard on the 120-minute single-job baseline. Each shard gets 90, which retires the former 300-minute exception; no job is above the 180-minute cap. tests/test_mutation_shards.py checks that every only_mutate module is in exactly one shard. Aggregate gates (ci-gate, full-matrix-result) run with if: always() and a short timeout; a timed-out dependency reports a non-success result (cancelled/failure), which the gate treats as a failure.

Jobs that call a reusable workflow cannot set timeout-minutes. Repo-local callees (publish-pypi.yml → integration-full.yml) are covered through their own jobs. The externally owned callees are an explicit allowlist in tests/test_workflow_timeouts.py: the shared doc-lint and codeql workflows in cubrid-lab/.github, and the cookbook smoke test. That test parses every workflow and fails when an executing job lacks a bounded timeout, or when a new external caller is not allowlisted.

Dependency installation

ci.yml, integration-full.yml and docs.yml use commit-pinned setup-uv and uv 0.12.17. Jobs export their selected dependency groups from uv.lock with --locked --no-build --no-emit-project, preserving applicable runtime dependencies for jobs that install the project. Export also refuses source metadata builds before a stale lock is rejected. Third-party installs require the exported hashes and --only-binary :all:, so registry source-build hooks cannot run. A manifest change without an updated lock fails. Update the lock with the dependency declarations.

The locked build group supplies the backend before a separate first-party editable install with --no-index --no-deps --no-build-isolation. Package builds use python -m build --no-isolation. Wheel/sdist smoke tests retain plain pip in fresh environments, with dependencies already installed and downloads disabled. These trusted checkout builds may execute first-party build code. Python 3.15 uses a derived pure Python PyYAML wheel whose hashes, licence and reproduction steps are in the CI wheel provenance.

setup-uv receives setup-python's python-version and exports UV_PYTHON for the selected interpreter. Caches include pyproject.toml, uv.lock and CI wheels, with a per-job suffix and Python minor version. CI and docs always cache; integration-full.yml retains auto. Cache contents cannot replace the required artifact hashes. Jobs log installed versions with uv pip freeze --system. python-canary.yml retains pip for its separate fresh-install probe. tests/test_workflow_installs.py checks these install and cache contracts and exercises export/installation source-hook, stale-lock and tampered-hash rejection offline.

Pinned documentation tools and scan concurrency

docs.yml exports --only-group docs from the lock. The docs group in pyproject.toml pins the site tools, and uv.lock fixes their transitive versions and hashes. Dependabot's root uv entry updates the manifest and lock together for every dependency group, including the documentation tools (#750, #784). The updater uses its own installed uv version; CI's pinned uv 0.12.17 must accept the resulting lock and the site build must pass before merge. The conditional Python 3.15 PyYAML wheel source uses a single mapping so the updater can fetch the checked-in wheel. Its Python marker and wheel hash remain part of the locked installation contract.

ci.yml also builds the site on pull requests (#786), so a bump or a docs edit that breaks mkdocs build --strict fails before merge instead of later on main. The docs-build job is selected by the site path filter: docs/** (content and nav assets), pyproject.toml (the docs group), uv.lock, .github/ci-wheels/**, mkdocs.yml, scripts/generate_llms_full.py, .github/workflows/docs.yml and .github/workflows/ci.yml. Root *.md files are not site inputs (the changelog is linked, not staged), so they do not select it, and workflow_dispatch forces it like the other lanes. The job runs the docs.yml build steps (install the docs group, scripts/generate_llms_full.py, mkdocs build --strict) with read-only permissions, pinned actions, a 10-minute timeout and persist-credentials: false; it never uploads a Pages artifact or deploys. The CI Gate expects docs-build to succeed exactly when site is selected and accepts a skip otherwise, so the required CI Gate check blocks a broken docs build. tests/test_ci_policy.py and tests/test_workflow_path_impact.py enforce this.

Documentation defects that a script can verify also block the merge (#927). The docs-check job runs scripts/check_docs_links.py and scripts/check_agents_docs.py on every tracked Markdown file. It is selected by the docs filter (**/*.md, docs/**) or the tooling filter (scripts/**, Makefile, ...), because a removed script or make target strands the documents that name it. It fails, printing file:line and a suggested fix when one is close, on:

  • a relative Markdown or HTML (<a href>, <img src>, <source src>) link or image whose target is missing, absolute or outside the repository;
  • an #anchor that is not a heading (its GitHub or site slug, or {#id}) or an <a id>/<a name> of the target Markdown file;
  • a reference link [text][label] without a definition, and an unclosed code fence;
  • a paragraph line under docs/ that starts with # and a digit, such as a wrapped issue number, also as a list item or in a block quote: the MkDocs site renders it as a heading (#938, #941), so write \#750;
  • a make goal written in inline code or a fenced block that the Makefile does not define, and a scripts/ path that does not exist. Release history (CHANGELOG.md, RELEASE_CHANGELOG.md) keeps the commands of its time, so only its links are checked.

Markdown is parsed as CommonMark (markdown-it-py, pinned in the docs group) and HTML with the standard library parser, so links in code are examples, brackets in a URL, a title or an HTML comment are not references, and quoted text and redirections are not make goals (#939). A reviewed false positive, such as raw HTML written relative to the site URL, is marked with <!-- docs-check: ignore --> on the reported line or the line before it. The job never opens a network connection and the check takes about one second. The CI Gate expects docs-check to succeed exactly when either filter is selected and accepts a skip otherwise, so a docs-only pull request still skips the CUBRID runtime lanes. tests/test_check_docs_links.py holds the passing and failing fixtures.

The shared doc-lint workflow stays advisory (fail-on-issue: false): markdownlint style, the Mermaid syntax scan and the shared link checker. Their findings on the #936 head (2026-10-15) are why: markdownlint reported 2,605 style findings, 2,014 of them table style (MD060); the Mermaid scan reported 4 on one quoted label that renders correctly; and lychee exited on its --exclude-mail argument without checking a link. Setting fail-on-issue: true would gate all of these, so it stays off. External links are not checked on pull requests. docs-sync, the Korean translation check, mkdocs build --strict and the public API check keep their own gates; docs-check does not repeat them.

codeql.yml and security.yml declare caller-level concurrency with group ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }} and cancel-in-progress: ${{ github.event_name == 'pull_request' }} (#783): a new push to a pull request cancels the superseded scan, while every other event gets a unique per-run group, because within one group GitHub replaces a pending run even when cancel-in-progress is false, so main pushes and scheduled runs are never cancelled or dropped. security.yml installs bandit[toml]==1.9.4, the version pinned in pyproject.toml. tests/test_workflow_pins.py enforces all three.

Artifact retention and caches

Every actions/upload-artifact step sets retention-days, so no artifact falls back to the repository default of 90 days. Debugging and evidence artifacts keep 14 or 30 days, and release artifacts keep 14 days. anchore/sbom-action runs with upload-artifact: false, because the workflow uploads the SBOM itself; otherwise the action would store a second copy with the default retention. tests/test_workflow_pins.py::test_every_artifact_upload_sets_a_retention_period pins both rules.

Review on 2026-10-11 (#750): the Actions cache held 7.6 GB in 162 entries, against GitHub's 10 GB repository limit. 3.0 GB of it was setup-python pip caches that no current workflow creates, left over from before the uv installs; they were last used on 2026-10-08 and GitHub evicts caches after 7 days without use. The rest are the per-job setup-uv caches. No cache change was needed.

Dependabot grouping

Dependabot used to open one PR per package for pip (/), pip (/docs) and GitHub Actions, and each one ran the full PR tier. .github/dependabot.yml now groups minor and patch version updates into one PR per ecosystem and directory (dev-tools, docs-tools, github-actions), so a weekly batch costs one CI run per group (#750). These stay in their own PRs:

  • Major updates are excluded from every group (update-types is only minor and patch), and dependabot-auto-merge.yml still holds them for human review.
  • Security updates stay separate: the groups set applies-to: version-updates.
  • The runtime dependency tzdata is excluded from dev-tools.

Grouping does not bypass anything. A grouped PR runs the same required checks, and auto-merge completes only after they pass. dependabot/fetch-metadata reports the highest semver change of a grouped PR as its update-type, so a group can never carry a major update into auto-merge. One failing package blocks the whole group: fix it in the group PR, or, if it needs separate work, add it to the group's exclude-patterns through a reviewed PR so Dependabot opens it on its own. tests/test_workflow_pins.py enforces the group settings.

Python 3.15 preview preparation

python-canary.yml is manual-only: supply the full SHA and dispatch the branch at that commit. One Ubuntu/standard-GIL lane selects Python 3.15 with prereleases allowed, prints the actual interpreter/dependency versions, runs full offline regressions and validates fresh wheel/sdist installs. Failed setup/install/tests fail the run normally. It is separate from required PR checks and release gates; there is no new schedule, PR matrix cell or CUBRID provisioning. This lane alone does not establish official support, live database or free-threaded compatibility.

Python 3.15 is now supported (see the support matrix). Its support evidence comes from the full compatibility matrix (live Python 3.15 × four CUBRID versions and TLS), the 3.15 offline endpoint cells and this lane's fresh wheel/sdist installs; the lane is kept as a manual packaging check. The 3.11 offline endpoint is unchanged; the representative live cells stay on Python 3.14/CUBRID 11.4 and Python 3.11/CUBRID 10.2, and Python 3.14 is still exercised live on every high-risk PR and in the full matrix.