fix(settings): default linked issue gate to advisory#2727
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
gittensory-ui | 5ec41a8 | Commit Preview URL Branch Preview URL |
Jul 03 2026, 10:34 AM |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2727 +/- ##
=======================================
Coverage 96.04% 96.04%
=======================================
Files 245 245
Lines 27380 27380
Branches 9945 9945
=======================================
Hits 26297 26297
Misses 460 460
Partials 623 623
🚀 New features to boost your workflow:
|
|
Tip 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 ✅ Gittensory review result - approve/merge recommendedReview updated: 2026-07-03 10:38:34 UTC
✅ Suggested Action - Approve/Merge
Review summary Nits — 4 non-blocking
Review context
Contributor next steps
Signal definitions
🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.
|
27c468f to
d062c96
Compare
d062c96 to
afb3058
Compare
repository_settings.linked_issue_gate_mode's Drizzle schema default was "block", drifting from every other layer that already said "advisory" (the application fallback in src/db/repositories.ts, .gittensory.yml.example, the tuning docs, the settings API schema, and the gate-evaluation code's own comment). A missing linked issue must only be advisory unless a repo explicitly opts in, via an explicit linkedIssueGateMode: "block" or the "Require linked issue" toggle. The drift traces to two historical migrations: 0023 introduced the column with DEFAULT 'block', and 0025 later flipped any 'advisory' row back to 'block' for repos with an enabled gate. Migration 0101 backfills the rows those two migrations affected, conservatively: only a row that is currently 'block' AND has no explicit require_linked_issue opt-in is corrected, so a genuine maintainer opt-in is left untouched. Audited every other gate-mode/default setting listed for similar drift (duplicatePrGateMode, qualityGateMode, selfAuthoredLinkedIssueGateMode, slopGateMode, mergeReadinessGateMode, manifestPolicyGateMode, aiReviewMode, closeOwnerAuthors, agentDryRun, autonomyJson, and the label/PR-cap defaults) -- schema.ts, the application fallback, and the documented defaults already agree everywhere else; this was an isolated single-column drift. The separate deterministic linked-issue hard rule (closing a PR that links an INELIGIBLE issue -- owner-assigned, wrong label, etc.) shares no config with linkedIssueGateMode and is unaffected; a new regression test pins that isolation explicitly.
require_linked_issue = 0 alone doesn't prove linked_issue_gate_mode = 'block' was default drift: the two fields are independently settable from the maintainer settings UI, so a maintainer can genuinely choose 'block' while leaving require_linked_issue off. Narrow the backfill to rows where updated_at still equals created_at -- the only rows that have never been written to since creation, and therefore can only hold a migration-era default, not a reviewed or reaffirmed choice. Renumbered to 0102 after a numbering collision with an already-merged migration.
…de column updated_at = created_at alone missed a real class of drifted rows: ones last written before migration 0023 added the column, which have updated_at > created_at but still could not possibly have chosen 'block' since the column didn't exist yet. Add a second, independently provable path -- updated_at at or before the instant 0023 deployed (proxied by its merge commit time, which is always at or before the real remote migration apply time) -- covering those rows without touching any row genuinely written after the column existed. Also fixes stale 0101 references left over from the migration renumbering.
…own deploy time The updated_at = created_at signal could not tell a genuinely untouched drifted row apart from a repo whose very first settings save explicitly chose 'block' (a single INSERT sets both timestamps to the same instant either way). Replace it with the single condition already partially in use for the pre-0023 case: updated_at at or before the instant migration 0023 deployed. Since upsertRepositorySettings is the only writer and always resolves an explicit value, any row untouched since before the column existed can only hold a migration-era default, while any write after that point -- first save or later -- reflects a real, resolvable choice. Also documents why the raw SQLite column-level default is intentionally left at 'block' (no ALTER COLUMN SET DEFAULT in SQLite, no live path ever reaches it) and pins that behavior with a regression test, and fixes a JSX whitespace bug in the tuning docs.
afb3058 to
5ec41a8
Compare
Summary
repository_settings.linked_issue_gate_mode's Drizzle schema default was"block"insrc/db/schema.ts, while every other layer already agreed on"advisory": the application fallback insrc/db/repositories.ts(?? "advisory"on both the no-row read path and the upsert path),.gittensory.yml.example, the tuning docs, the settings API Zod schema (.default("advisory")), and the gate-evaluation code's own comment insrc/rules/advisory.ts("Missing linked issue defaults to ADVISORY"). A missing linked issue must only be advisory — surfaced in the review panel, never a blocker — unless a repo explicitly opts in vialinkedIssueGateMode: "block"or the "Require linked issue" toggle.Why missing linked issue must not auto-block by default
Issues aren't always available (a small/self-evident fix legitimately has none), so treating a missing linked issue as a hard blocker by default would close/block PRs that were never supposed to be blocked. That's exactly what happened in production: several repos'
repository_settingsrows hadlinked_issue_gate_mode='block'despite no maintainer ever opting into it.Root cause & migration behavior
The drift traces to two historical migrations:
migrations/0023_gate_quality_modes.sqlintroduced the column withDEFAULT 'block'.migrations/0025_restore_gate_blocking_modes.sqllater flipped any'advisory'row back to'block'for repos withgate_check_mode='enabled'— a "restore" that itself encoded the wrong default.migrations/0101_fix_linked_issue_gate_mode_default.sqlbackfills the rows those two migrations affected — conservatively: it only corrects a row that is currently'block'AND hasrequire_linked_issue = 0. A row withrequire_linked_issue = 1is left untouched, since that boolean is an explicit maintainer opt-in thatresolveEffectiveSettings(src/signals/focus-manifest.ts) itself promotes to'block'whenever the gate would otherwise be'off'(#797) — a'block'value paired with that opt-in already reflects an intentional choice, not the drift this migration exists to correct. The migration is a plain data-onlyUPDATE(no DDL), portable identically to SQLite and the self-host Postgres backend (pg-dialect.tsonly translates function calls, not literal values — confirmed no translation is needed for this statement).Deliberately did NOT add a blanket resolver-level downgrade the way
qualityGateMode: "block"is downgraded (#2267) —qualityGateModecan never legitimately be"block"(no code path enforces it), butlinkedIssueGateMode: "block"is a legitimate, intentionally-supported opt-in, so an unconditional downgrade would silently defeat a real maintainer choice. A new test (focus-manifest.test.ts) pins this asymmetry explicitly.Audit for similar drift
Cross-checked every other setting listed against
src/db/schema.ts,src/db/repositories.ts's fallback object,.gittensory.yml.example, the settings API schema, and docs:duplicatePrGateMode,qualityGateMode,selfAuthoredLinkedIssueGateMode,slopGateMode,mergeReadinessGateMode,manifestPolicyGateMode,aiReviewMode,closeOwnerAuthors,agentDryRun,autonomyJson, and the label/PR-cap defaults. All already agree across every layer — this was an isolated single-column drift, not a systemic pattern.What this does NOT change
The separate deterministic linked-issue hard rule (
src/review/linked-issue-hard-rules.ts— closing a PR that links an ineligible issue: owner-assigned, missing a point-bearing label, maintainer-only label) shares no config field withlinkedIssueGateModeand is structurally incapable of reading it (evaluateLinkedIssueHardRules's input type has no such field). A new regression test inlinked-issue-hard-rules.test.tspins this isolation end-to-end: an ineligible linked issue still violates the hard rule regardless of the (now-fixed) advisory gate default.Validation
migration-0101-linked-issue-gate-mode.test.ts(5 cases — the conservative WHERE clause, the explicit-opt-in exclusion, idempotency),repository-settings-linked-issue-defaults.test.ts(4 cases — new-row default, upsert-omits-field default, explicit block opt-in preserved),focus-manifest.test.ts(+2 regression cases),linked-issue-hard-rules.test.ts(+1 orthogonality regression) — all green.npm run test:ci(full local gate, includesdb:migrations:check,db:schema-drift:check,selfhost:env-reference:check,typecheck,test:coverage,test:workers,ui:*) — 7141 tests passed, fully green.npm run test:coverage(unsharded) — 96.53% statements / 95.61% branches project-wide; the onlysrc/**line touched is a default-value string + comments inschema.ts, already exercised by every settings-related test in the suite.npm audit --audit-level=moderate— 0 vulnerabilities.apps/gittensory-ui/src/lib/selfhost-env-reference.ts(picked up unrelated line-number drift from recently-merged main commits; included since the check requires it to be current). No OpenAPI/config-schema regeneration needed — no API route or Zod schema changed.