docs/ci.md
Rama main · réplica de lectura del repositorio original.
# CI qualification
PR checks gate ordinary correctness. Release qualification gates publication.
Nightly qualification detects regressions before release day. All results
refer to one exact commit. A skipped check is not a passing check.
## Coverage contract
| Check | Ordinary PR | Relevant PR or full override | Nightly and release |
|-------|-------------|------------------------------|---------------------|
| Format, Clippy, rustdoc, roundtrip, security, lockfile | Required for applicable changes | Required | Required |
| Script/workflow lint, workflow hardening, typos | Required | Required | Required |
| Linux all-feature and no-default offline suites | Required for code changes | Required | Required |
| Scenario catalogs and safety-contract checks | Required for applicable changes | Required | Required |
| Windows/macOS builds and platform smoke | Required for code changes | Required | Required |
| Full Windows/macOS offline suites | Required when Rust tests change | Required for platform-sensitive changes | Required |
| Compact copies/reseed safety fixture | Part of ordinary suite | Required | Required |
| 300-writer copies stress | Not required | Required for scale-sensitive changes | Required |
| Instrumented coverage | Not merge-critical | Available through full qualification | Required report, not a percentage threshold |
| Nightly unused-dependency analysis | Not required | Full qualification | Required |
| Fuzz build and bounded seeded fuzz runs | Not required | Full qualification | Required |
| Optimized offline regression tests | Not required | Full qualification | Required |
| Five release archives and extracted-binary smoke | Not required | Full qualification | Required |
| Both container architectures, entrypoint and service smoke | Relevant smoke only | Full qualification | Required |
| Live provider, live shell, release-binary import, real host service | Never on untrusted PRs | Protected maintainer qualification only | Separate exact-commit evidence for releases |
Full qualification must enumerate ignored tests. Live tests remain opt-in and
single-threaded. Do not substitute an unfiltered `--ignored` run for this
inventory. Cross-zone fixtures and unavailable host platforms need an explicit
applicability decision, not a silent skip.
The local `just gate` and `just full-test` commands retain their existing
contracts. CI reuses their owners and test routes; it does not claim that an
offline workflow performed live provider or real host-service validation.
## Local work
Before opening a PR, run focused tests for the changed behavior plus formatting
and lint checks. Do not replay the complete offline gate locally. PR CI owns
that evidence. Use local live-provider and real service checks only when the
change needs them and the isolated fixtures are approved. `just gate` remains
available for diagnosis or work without hosted CI.
## PR routing
Draft validation events skip routing, quality checks, qualification, and native
service smoke, including when labeled `ci:full`. The required `CI` job still runs
as a small failing marker: skipped required checks count as success on GitHub.
Marking the PR ready for review starts validation for that event's exact merge
commit, even if the head has not changed. A new head starts a new run.
Converting a ready PR back to draft starts the failing marker and uses the
existing PR concurrency group to supersede the older run. Marking it ready again
requires a fresh successful `CI`; draft skips never satisfy that check.
Unrelated label additions/removals and title/body edits use a run-specific
concurrency group and a different check name, `CI metadata (ignored)`. They
neither cancel active validation nor create a skipped or successful `CI`.
Adding or removing `ci:full` and editing the base branch start validation in the
normal PR group. Retargets use that event's base and merge commit. Main
pushes, manual qualification, nightly runs, and releases keep their existing
routes. The repository does not currently enable a merge queue.
GitHub does not guarantee concurrency ordering during rapid transitions. If a
ready run is superseded, retrigger validation for the current head; cancelled or
missing work cannot pass the required check.
The required check stays `CI`. A failed, cancelled, missing, or unexpectedly
skipped required job fails that check. The `ci:full` label runs full
qualification. Cargo/build inputs, fuzz inputs, container code, artifact and
qualification/release workflows, the core `ci.yml`/`checks.yml` orchestrators,
and unknown inputs also select it. Their heavy branches need execution coverage.
Known notification/advisory clients and their workflows, routing/gate helpers, and
`scripts/ci/test_*.py` keep the always-on Python tests, workflow hardening,
script/workflow lint, cheap safety-contract validation, and typos without
building Rust. Architecture-only edits therefore validate the contract catalog.
New unclassified helpers still select full qualification.
Recognized Rust test files keep the Linux all-feature and no-default suites
and full Windows/macOS offline suites. Other code changes keep Linux suites with
macOS/Windows build and CLI/media/service smoke; filesystem, state,
configuration, and service changes select full platform tests. Narrow
scenario/roundtrip/stress runners keep the ordinary Rust matrix without
selecting release artifacts or fuzzing. The contract checker itself runs in the
always-on workflow-hardening job.
The large copies stress test follows its runtime owners:
download, state, Photos provider, sync, configuration, retry, selection,
filesystem/shared types, command service types, media fixtures, and its own
queue-projection fixture. CLI entrypoints, unrelated commands, authentication,
and other unit-test modules do not select it by themselves. The compact
copies/reseed fixtures remain in ordinary offline tests.
Routing uses filenames, not PR descriptions or semantic diff parsing. Changes
inside shared runtime files still select stress even when a particular change
is diagnostic. For example, #984 retains stress because it changes Photos
hydration and session code; #985's reset command and unrelated test modules
skip stress while keeping the full platform tests selected by its entrypoints.
New files within stress runtime owners select stress conservatively. Renames
consider both old and new paths. Full/heavy overrides, unknown top-level
inputs, main pushes, and scheduled qualification retain stress. Its runner and
Rust check orchestration also select stress. The broad-PR rollout fallback adds
platform suites, coverage and unused-dependency checks for Rust changes, but
cannot make an unrelated unit-test module or notification script select stress.
Docs-only PRs retain workflow/script lint, safety-contract validation and typos.
Native release-binary service smoke is routed through the required `CI` graph
for service, service/status command, CLI, config and shared entrypoint changes.
A selected missing, skipped, cancelled or failed smoke fails `CI`. Heavy
qualification owns the same smoke internally; ordinary CI calls it only when
qualification is off. The reusable service workflow has no independent PR
trigger, so each validation runs its native smoke matrix at most once.
For the exact file sets in #1000, #997, #991 and #1001, routing now selects
ordinary Rust checks for #1000's Rust tests and only the always-on checks for the
advisory-only PRs. All four skip full qualification and copies stress. These
path-matrix cases, positive build/runtime controls and missing/skipped aggregate
controls are covered by `scripts/ci/test_policy.py` and `test_draft_pr.py`.
## Rollout
Keep the existing PR workload until the replacement qualification route,
publication guards, and nightly run have passed. Activate lighter PR routing
only after that evidence exists. `CI_LIGHT_PR_ENABLED` defaults to unset, which
retains the broad PR checks. After merge, run Nightly qualification on main,
verify its complete artifact receipt, then enable the variable with value
`true`. Observe the first scheduled run and freshness check. Record cold and
warm timings separately. Clear the variable to restore broad PR checks.
Release publication must consume the artifacts that passed qualification.
GitHub release assets, GHCR version/latest tags, and Homebrew updates all depend
on that result. Nightly and manual rehearsals have no publishing permissions.
## Local release prerequisites
When a maintainer explicitly requests a release, run these local prerequisites
before starting release CI. Verify the configured isolated test account, selected
fixtures, and an unused host-service slot. Ask for input if those prerequisites
are unavailable or ambiguous. Never use an existing media/state tree as
disposable test data, and never replace an existing service. An ordinary PR or
CI rehearsal does not authorize credential-bearing checks.
1. Check out the final release commit with a clean worktree. Do not change it
after qualification starts.
2. Run `KEI_FULL_TEST_REAL_SERVICE=1 just release-local` on the prepared Linux host.
This builds the live CLI/container fixtures, then runs provider, shell,
release-binary, import, and real service checks. It does not repeat the
hosted offline suite. It requires working credentials, Docker, and an unused
user-service slot. Retain the finalized report and private logs.
3. Run `python3 scripts/ci/local_release.py record --repo rhoopr/kei --sha COMMIT --report REPORT`.
The validator rejects missing, skipped, rate-limited, failed, or dirty-head
evidence. All discovered shell suites and the real host-service phase must
pass. Cross-zone testing remains an explicit fixture-dependent extension.
4. Post only the redacted summary and report digest to the release PR. Do not
upload private logs, credentials, provider identifiers, or local account paths.
5. As part of the authorized release, repeat the record command with
`--publish --evidence-url COMMENT_URL`. The release gate accepts only the latest
repository-owner status for that exact commit, no older than seven days.
6. Start the release only after those prerequisites pass. A tag starts heavy
qualification; manual dispatch of Release rehearses it without publication.
The attestation is a maintainer trust boundary, not independent proof that local
commands ran. Keep the underlying evidence. A new commit requires a new run.
Nightly results do not replace this requirement.
## Nightly operation
`Nightly qualification` runs on main at 07:23 UTC each day (03:23 EDT or
02:23 EST). It runs even when the commit is unchanged, since dependencies and
runner images can change. It has no publication permission. Manual dispatch on
main uses the same route; manual Heavy qualification can test a feature branch
without publishing or posting notifications.
`Nightly freshness` checks at 19:23 UTC. A missing, failed, cancelled, or stale
completed run opens one incident issue. Incidents carry `bug`, `@infra`, `@test`,
and `nightly fail` labels. Missing labels are added to an existing incident
without replacing its other labels; the reporter creates `nightly fail` if needed.
A failure identity is the workflow run and its attempt. The reporter and the
freshness check use the same identity, and message text does not identify a
failure. An incident body records each observed finding. Both checks read open
and closed bot incidents that start with the trusted marker. Closing an
incident acknowledges its recorded findings, so the check does not refile them.
Only one incident stays open. Later findings are recorded on it without a
comment, and closing it acknowledges those findings too. An issue body changes
only when a finding is missing, and repeated checks do not add comments.
Missing and stale qualification have their own identities, separate from failed
runs. Acknowledging an old failure does not clear a later freshness alert.
After a qualification run has completed, the missing identity includes that run,
so closing a missing incident before a later recovery does not hide a new
missing episode.
Suppression changes notifications only; the checks still report failure.
Recovery adds a comment and closes the incident. A completed run must have
started within 36 hours. Both notification jobs share the `nightly-incident`
concurrency group, so incident updates stay serialized. Scheduled Actions can be
delayed or disabled; this second schedule detects missing qualification runs but
is not an external availability monitor for GitHub itself.
Reports and candidate artifacts are retained for 14 days. PR, nightly, and
release concurrency groups are separate. Nightly work cannot cancel a release.
## Advisory OpenCode review
[OpenCode advisory review](opencode-review.md) documents the optional bot review,
secret setup, provider data transmission, limits, and security boundary. It is
advisory and does not contribute to the required CI gate.
## Resume a partially published release
The Release workflow accepts optional `publication_run` and `publication_tag`
inputs together when dispatched from main. This mode publishes retained container
artifacts and updates Homebrew; it does not rebuild, republish archives, or move
the release tag. The tag must be the latest public stable release, and the original
tag-push run must have successful validation, heavy qualification and archive
publication for that exact commit.
The resume path checks artifact checksums and provenance again. After loading each
container, it verifies the exact config digest and ordered uncompressed layer
digests from a fresh Docker save, rather than comparing storage-backend-specific
image IDs. Missing, ambiguous or mismatched evidence stops publication.