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_atover 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
skippednor 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-charsetorofficial-differential, or anoffline-testscell on Python 3.11. Otherwise an ordinary code PR executedoffline-tests. Any other PR is docs-only. - Pushes: a code push executed
offline-tests. - Releases:
publish-pypi.ymlpush runs in which theFull compatibility matrixjobs 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.ymlfailure 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.9repo-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.ymlrun 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, plusrepo-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-changesjob (with read-onlyactions: read) looks up push runs of the exact SHA. It drops the code lanes when one successful push run executedoffline-tests, and the repository-tooling lane when one executedrepo-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.ymlmoves from Monday 04:00 UTC to Thursday 04:00 UTC. It no longer stacks on the Mondayci.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 tlssession uses Python 3.12 and the wide Hypothesis profile. That makes it a different cell and profile fromci.yml's 3.11/3.15 cells, so it stays. - The release-merge overlap is kept.
- The overlap. On a release commit, the
ci.ymlpush run's live lanes (integration-tests3.14/11.4 and 3.11/10.2, TLS 3.14/11.4, charset, official differential) are a subset ofintegration-full.ymlon 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 (
consistencyfailure). 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/mainconcurrency 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 failedvalidate-target, which already fails the gate. - Checkout. The checkout sets
persist-credentials: false, like the otherintegration-full.ymljobs. - Cache mode. setup-uv uses the
autocache 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
#anchorthat 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
makegoal written in inline code or a fenced block that the Makefile does not define, and ascripts/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-typesis onlyminorandpatch), anddependabot-auto-merge.ymlstill holds them for human review. - Security updates stay separate: the groups set
applies-to: version-updates. - The runtime dependency
tzdatais excluded fromdev-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.