Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .gittensory.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,9 @@ gate:
# comment, but never blocks; block = a confidence-floor-passing "unaddressed"
# verdict ALSO becomes a hard blocker (linked_issue_scope_mismatch).
# off | advisory | block. Default: off. DB-backed (dashboard-settable too);
# this overrides the stored value.
# this overrides the stored value. When unset, the review.linkedIssueSatisfaction
# knob further below is used as a fallback instead (#4149) -- setting either
# spelling has the same real effect; this one wins if both are set.
linkedIssueSatisfaction: off

# Gate-check dry-run. When true, the posted check conclusion remains the real
Expand Down Expand Up @@ -983,8 +985,8 @@ settings:
# finding_categories: false
# # How strictly a linked issue must actually be SATISFIED by the PR (distinct from linkedIssuePolicy,
# # which only checks a link EXISTS). off = not evaluated; advisory = surface a finding; block = can
# # become a hard blocker (confirmed-contributor-gated). This is the config knob only — parsed and
# # normalized here; the merge/close decision that reads it is a separate maintainer slice.
# # become a hard blocker (confirmed-contributor-gated). Fallback alias for gate.linkedIssueSatisfaction
# # above (#4149) -- used only when that field is unset; setting either spelling has the same real effect.
# # off | advisory | block. Default: off (byte-identical when unset).
# linkedIssueSatisfaction: off
# # Maintainer-declared DETERMINISTIC content assertions (title/description must contain a phrase, a
Expand Down
8 changes: 5 additions & 3 deletions config/examples/gittensory.full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,9 @@ gate:
# comment, but never blocks; block = a confidence-floor-passing "unaddressed"
# verdict ALSO becomes a hard blocker (linked_issue_scope_mismatch).
# off | advisory | block. Default: off. DB-backed (dashboard-settable too);
# this overrides the stored value.
# this overrides the stored value. When unset, the review.linkedIssueSatisfaction
# knob further below is used as a fallback instead (#4149) -- setting either
# spelling has the same real effect; this one wins if both are set.
linkedIssueSatisfaction: off

# Gate-check dry-run. When true, the posted check conclusion remains the real
Expand Down Expand Up @@ -996,8 +998,8 @@ settings:
# finding_categories: false
# # How strictly a linked issue must actually be SATISFIED by the PR (distinct from linkedIssuePolicy,
# # which only checks a link EXISTS). off = not evaluated; advisory = surface a finding; block = can
# # become a hard blocker (confirmed-contributor-gated). This is the config knob only — parsed and
# # normalized here; the merge/close decision that reads it is a separate maintainer slice.
# # become a hard blocker (confirmed-contributor-gated). Fallback alias for gate.linkedIssueSatisfaction
# # above (#4149) -- used only when that field is unset; setting either spelling has the same real effect.
# # off | advisory | block. Default: off (byte-identical when unset).
# linkedIssueSatisfaction: off
# # Maintainer-declared DETERMINISTIC content assertions (title/description must contain a phrase, a
Expand Down
9 changes: 9 additions & 0 deletions src/signals/focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,15 @@ export function resolveEffectiveSettings(
};
}
applyGateConfigOverrides(effective, manifest.gate);
// #4149: `review.linkedIssueSatisfaction` (#2173) is a near-identically-named but functionally distinct
// phantom field -- parsed, but until now never wired to the real DB-backed gate
// (linkedIssueSatisfactionGateMode), unlike every other typed `gate.*` alias. Fold it in as a fallback:
// `gate.linkedIssueSatisfaction` (applied above) always wins when set; otherwise an explicit
// `review.linkedIssueSatisfaction` takes effect instead of being silently discarded, so a self-hoster who
// sets either spelling gets the same real gate behavior.
if (manifest.gate.linkedIssueSatisfaction === null && manifest.review.linkedIssueSatisfaction !== null) {
effective.linkedIssueSatisfactionGateMode = manifest.review.linkedIssueSatisfaction;
}
// The dashboard "Require linked issue" toggle must not silently diverge from gate blocking: when the
// boolean is on but linkedIssueGateMode is still off, treat it as a block requirement (#797).
// #4618: the yml-only top-level `linkedIssuePolicy: required` knob gets the same promotion -- previously a
Expand Down
11 changes: 7 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -815,10 +815,13 @@ export type RepositorySettings = {
* satisfies its primary linked issue's intent never runs (byte-identical to today). `advisory` = it runs
* and renders as a collapsible section in the review comment, but never blocks. `block` = ALSO let a
* confidence-floor-passing "unaddressed" verdict become a gate blocker (`linked_issue_scope_mismatch`,
* confirmed-contributors only, like every other blocker). Distinct from the config-as-code-only
* `review.linkedIssueSatisfaction` manifest field (#2173) — this is the DB-backed, dashboard-settable
* gate-mode counterpart; `.gittensory.yml gate.linkedIssueSatisfaction` overrides it exactly like every
* other `gate:` field overrides its `RepositorySettings` counterpart. Default `off` — opt-in. */
* confirmed-contributors only, like every other blocker). This is the DB-backed, dashboard-settable
* counterpart; `.gittensory.yml gate.linkedIssueSatisfaction` overrides it exactly like every other
* `gate:` field overrides its `RepositorySettings` counterpart. The near-identically-named, config-as-
* code-only `review.linkedIssueSatisfaction` manifest field (#2173) is folded in as a fallback alias
* (#4149) when `gate.linkedIssueSatisfaction` is unset — see `resolveEffectiveSettings` in
* `signals/focus-manifest.ts` — so setting either spelling has the same real effect. Default `off` —
* opt-in. */
linkedIssueSatisfactionGateMode: GateRuleMode;
/** First-time-contributor grace (#552). RESERVED / currently INERT (#2266): parsed, clamped, and threaded
* end-to-end, but the gate evaluator never reads it — a genuine newcomer with a real blocker is still
Expand Down
31 changes: 31 additions & 0 deletions test/unit/focus-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3105,6 +3105,37 @@ describe("parseFocusManifest settings override + resolveEffectiveSettings", () =
expect(eff.linkedIssueGateMode).toBe("advisory");
});

describe("review.linkedIssueSatisfaction phantom-field alias (#4149)", () => {
it("an explicit review.linkedIssueSatisfaction takes effect when gate.linkedIssueSatisfaction is unset", () => {
const eff = resolveEffectiveSettings(
{ linkedIssueSatisfactionGateMode: "off" } as RepositorySettings,
parseFocusManifest({ review: { linkedIssueSatisfaction: "block" } }),
);
expect(eff.linkedIssueSatisfactionGateMode).toBe("block");
});

it("gate.linkedIssueSatisfaction always wins over review.linkedIssueSatisfaction when both are set", () => {
const eff = resolveEffectiveSettings(
{ linkedIssueSatisfactionGateMode: "off" } as RepositorySettings,
parseFocusManifest({ review: { linkedIssueSatisfaction: "block" }, gate: { linkedIssueSatisfaction: "advisory" } }),
);
expect(eff.linkedIssueSatisfactionGateMode).toBe("advisory");
});

it("falls through to the DB value when neither spelling is set", () => {
const eff = resolveEffectiveSettings({ linkedIssueSatisfactionGateMode: "block" } as RepositorySettings, parseFocusManifest(null));
expect(eff.linkedIssueSatisfactionGateMode).toBe("block");
});

it("review.linkedIssueSatisfaction: off is a real, distinct value from unset -- it still applies (not silently skipped)", () => {
const eff = resolveEffectiveSettings(
{ linkedIssueSatisfactionGateMode: "block" } as RepositorySettings,
parseFocusManifest({ review: { linkedIssueSatisfaction: "off" } }),
);
expect(eff.linkedIssueSatisfactionGateMode).toBe("off");
});
});

it("REGRESSION: downgrades a pre-existing DB qualityGateMode: block to advisory, even with no gate.readiness.mode override (#2267)", () => {
// Simulates a repo whose DB row already has quality_gate_mode = "block" from before the write-time guards
// (the settings.qualityGateMode parser, the settings-write API routes) existed — the dashboard/API path's
Expand Down