R2D2-MERIDIAN/.github/AGENTS.md
Joshua Belke 7c630a61cf feat(ci): refuse the two ReductStore Zenoh omissions that work when wrong
From v1.19 the bytes tier joins a Zenoh network natively — a subscriber for
writes, a queryable for reads — configured entirely by `RS_ZENOH_*`. Two
one-token omissions in that environment each convert it into something
DIAGRAM.md refuses by name, and neither goes red:

- Point `RS_ZENOH_SUB_KEYEXPRS` or `RS_ZENOH_QUERY_KEYEXPRS` anywhere that
  overlaps `meridian/v1/**` and one string buys a second durable log AND a
  second read path — R2 and R3, the arrival R9 names.
- Leave `mode=` out of `RS_ZENOH_CONFIG` and it inherits Zenoh's own default
  of `peer`, so a storage box becomes a routing peer transiting between
  communities — R4, where Law 2 answers "Leaves. Always."

Records land and queries answer in both cases, which is why this is a gate
rather than a review item, and why it lands before any Zenoh wiring exists
to guard.

Overlap, not string match. `**`, `meridian/**`, `meridian/*/**`, `**/k/9`
and `meridian/v1` all reach the event key space without spelling it, so the
core does chunk-wise keyexpr intersection over `**`/`*`/`$*`;
`meridian/artifacts/**` and `meridian/v2/**` pass. `RS_ZENOH_CONFIG_PATH` is
resolved and read, an unresolvable path being a finding rather than a skip,
and an unreadable value (a Helm template, a `${VAR}` with no literal
default) is reported as `unverifiable-value` — explicit denial, never a
silent empty result.

Nothing in tree sets `RS_ZENOH_*` yet, so the guard would pass vacuously.
The contract test therefore pins its *reach* as well as its verdicts: the
scan set still contains `docker-compose.yml`,
`docker-compose.override.yml.example` and `.env.example`, still reaches
`deploy/` and `REMAPPING/cicd/`, and the enumerate-read-refuse path joins up
end to end in a throwaway repo.

One bug found by staging rather than by running: the guard scans tracked
files via `git ls-files`, and its own contract test carries both refused
values inline as the assertions that give it teeth. Untracked, the test was
invisible to the scan and everything passed; committed, the guard would have
eaten its own test corpus and failed on first run. Excluded by exact path —
not by a `scripts/` prefix, so a real config under `scripts/` is still
scanned. The "default scan of the tree is clean" assertion the test already
carried is what will catch this class next time; it simply could not fire
before the file was tracked.

Markdown is deliberately not scanned: prose legitimately names these
variables while forbidding a value, so the guard is assignment-anchored and
config-file-scoped.

Verified: four bad fixtures exit 1 and the good fixture exits 0 individually;
`node --test` core suite 41/41; contract test 22 assertions; `just
check-reductstore-zenoh` exits 0 with the guard staged, so the post-commit
state is what was measured. Reachable from `just ci` via `ci -> check ->
check-reductstore-zenoh` (confirmed with `just --dry-run ci`, which emits all
three guard lines; the full multi-minute `just ci` was not run).

Bead: meridian-f26n
Signed-off-by: Joshua Belke <joshua@innovationhub-act.org>
2026-08-20 18:33:55 -04:00

6 KiB

.github/ — CI & Repo Automation

Purpose

The required checks, release automation, and repo templates. This directory decides whether a PR can merge, so a change here changes the contract for every other tree.

Ownership

Workflow Runs
ci.yml The main gate: changes (dorny/paths-filter plus unconditional repo-contract guard steps — release/mobile/desktop-dev/compose contracts, file-size ratchet, relay E2E inventory, frozen migrations, legacy namespaces, relay capacity, architecture map, unwrap budget, ReductStore Zenoh config) → Rust Lint, Unit Tests, Isolated DB Gate, Desktop Core, Desktop Smoke E2E (sharded), Desktop E2E Relay, Desktop E2E Integration (sharded), Backend Integration (relay e2e), Relay E2E, Web, Mobile, Security, Dead Token Reference Guard, Server Cross-Compile, Windows Rust, Desktop Build (macOS). Every job needs: changes, so a failing guard step blocks the whole gate. A guard step's inputs must be self-contained in the commit (no hand-synchronised pairs — check-gauntlet's passive beads export disqualified it, finding F027); the rollback for a guard step is deleting it, justified per the do-not-weaken rule — it stays runnable via its just recipe
docker.yml Relay image build
helm-chart.yml, push-gateway-helm-chart.yml Chart lint + helm-unittest + render matrix, gated kind install
control-plane.yml Control-plane chart contract + fmt/clippy/tests against a Postgres service
release.yml, auto-tag-on-release-pr-merge.yml Release + tagging automation
mobile-release-candidate.yml Mobile RC publishing
signed-macos-canary.yml, linux-canary.yml, windows-canary.yml Platform canaries
meridian-harness.yml Meridian Harness harness bundle
benchmark-harbor.yml benchmarks/harbor-meridian-orchestra run

Also: CODEOWNERS, PULL_REQUEST_TEMPLATE.md, ISSUE_TEMPLATE/ (bug-report.md, feature-request.md, config.yml).

Local Contracts

  • ci.yml's changes filter groups and lefthook.yml's globs mirror each other — keep both in sync. lefthook.yml's header documents the deliberate deviations (CI-workflow-only edits skip local runs; desktop TS checks don't trigger on Rust changes; deletion-only changes don't trigger local hooks). A new deviation must be added to that comment or it reads as drift.
  • A new source tree needs a filter entry, or its changes silently run no jobs.
  • A filter entry also needs a matching line in the changes job's outputs: block. The two are separate lists and nothing connects them: filters: defines the pattern, outputs: is what downstream jobs can read. Add only the filter and needs.changes.outputs.<name> evaluates to the empty string, so an if: of the usual shape collapses to github.event_name == 'push' — the job runs on pushes and never on a pull request, which is where the gate was supposed to catch the change before merge. It fails silently in the direction that looks green. Caught this way while adding the admin-web job (G21). To check all three lists agree, parse the workflow and compare filters:, outputs:, and every needs.changes.outputs.X reference — they must be equal sets.
  • Actions are pinned to a commit SHA with the version in a trailing comment (@<sha> # v4.0.2). Never pin to a mutable tag or branch.
  • DCO Check is required. Every commit needs a Signed-off-by trailer — commit with git commit -s; git rebase/git cherry-pick need --signoff. The commit-msg hook installed by just hooks adds it for locally created commits.
  • Contract scripts run in CI and must stay green: scripts/test-release-ref-contract.sh (release workflow source), scripts/test-mobile-release-contract.sh, and the guard-core node --test suites. If you change a release script, change its contract test.
  • Do not weaken a required check to land a PR. Deleting or continue-on-erroring a job to get green is a contract change that needs its own justification.
  • A slow or hang-prone suite gets a timeout-minutes, not an indefinite withholding. Isolated DB Gate sat unwired for exactly that reason — its cost was recorded as ~2.5h and nobody could confirm the number without running it somewhere, which is circular. A bounded job answers the question on the first run and fails legibly if the fear was real. Give the recipe an entry point with no service bootstrap (test-db-run beside test-db) so CI and a developer's box share one definition of the selection rather than two.
  • Quarantine a flaky test by name in the just recipe, never in the workflow. A gate that reds on unrelated work teaches people to re-run until green, so a measured flake is excluded rather than tolerated — but the exclusion belongs where both CI and a developer read it, each entry carries the bead that owns it, and an env-var escape hatch keeps the full selection runnable (MERIDIAN_TEST_DB_ALL=1). An exclusion with no bead is a deleted test wearing a comment.
  • Release tags are immutable: never force-move a published tag. A broken release gets the next patch version. Tag only a verified-live commit (see RELEASING.md).

Work Guidance

  • Reproduce a CI failure locally with the matching just recipe before editing the workflow — just ci is the local equivalent of the main gate, and just check/just test-unit map to Rust Lint/Unit Tests.
  • Prefer sharding an existing E2E job over adding a new workflow; the desktop E2E jobs already have a shard matrix.
  • scripts/summarize-flaky-tests.mjs (in REMAPPING/meridian-desktop/scripts/) exists for triaging flaky E2E output — use it instead of retry-looping a job.

Verification

just ci        # local equivalent of the main gate
just check
just test-unit
scripts/test-release-ref-contract.sh

A workflow edit is verified by a CI run on the PR — there is no local runner for these files.

Child STELLAR Index

None — .github/ is governed by this file.