Count external issue links in vulnerability has_issues
What does this MR do and why?
This MR makes Vulnerabilities::ConsistencyChecks::TriggerParityCheck count external issue links (for example Jira) in vulnerability_reads.has_issues, the same rule VulnerabilityExternalIssueLinks::UpdateVulnerabilityRead already uses. It also stops that worker from clearing the value when other links remain.
Most writers of vulnerability_reads.has_issues count GitLab issue links only. That's the DB insert and delete triggers, the ingestion upsert (IngestVulnerabilityReads::Upsert) and VulnerabilityIssueLinks::DeleteService. VulnerabilityExternalIssueLinks::UpdateVulnerabilityRead also counts links to an external issue tracker such as Jira, which is the product intent (!146643 (merged), &12882).
Vulnerabilities::ConsistencyChecks::TriggerParityCheck#fix_has_issues_mismatches counted GitLab vulnerability_issue_links only. So the consistency check treated a correct, Jira-set has_issues value as drift and set it back to false. That drops Jira-linked vulnerabilities out of the vulnerability report's "Has issue" filter.
Sweeps during the partial rollout of the vulnerability_consistency_checks feature flag already cleared some of these values (#592117). On a production clone, 39,076 vulnerability_reads rows have has_issues = true from external links only, and a full rollout would have cleared them in every project the sweep reached. The feature flag is disabled on gprd (2026-10-02 08:56 UTC, #592117) until this fix ships.
I'll fix the other writers in #632267. Until then, on projects with turn_off_vulnerability_read_create_db_trigger_function enabled, ingestion still writes the value from GitLab issue links only, so the sweep may log has_issues fixes there that aren't application drift. Those logs are the parity signal for that flag, so #632267 blocks ramping it (#553939 moves the vulnerability_reads writes from DB triggers to application code).
Changes
fix_has_issues_mismatches: the expected value is nowEXISTS (vulnerability_issue_links) OR EXISTS (vulnerability_external_issue_links).create_missing_vulnerability_reads: same rule (issue_links.any? || external_issue_links.any?, withexternal_issue_linkspreloaded).UpdateVulnerabilityRead: removing an external link no longer setshas_issues: falseunconditionally. It keepstruewhen other GitLab or external issue links remain.
Once this is deployed and the feature flag is back on, projects get swept again after their next security scan, which restores the cleared values. So I don't think we need a separate data repair. Projects that don't scan again keep the cleared value until they do.
The check still finds real drift. On the same clone, 2,136 reads have has_issues = false despite a GitLab issue link. Those are still fixed, and where they come from is tracked in #631874.
Database
No schema change. All three tables are on gitlab_sec, and vulnerability_external_issue_links already has an index leading with vulnerability_id (idx_vulnerability_ext_issue_links_on_vulne_id_and_ext_issue).
These timings are for the expected subquery on one 1,000-read keyset window on a large project, from Database Lab gitlab-production-sec. The cold time is almost all the outer vulnerability_reads scan (heap fetches on a cold clone, 10.1 s of I/O), and this MR doesn't change that scan.
| Warm | Cold | |
|---|---|---|
| Before (internal links only, joe 508463) | 7.2 ms | n/a |
| After (internal OR external, joe 508459 warm / 508457 cold) | 34.9 ms | 10.4 s |
Plan (cold, joe 508457)
Index Only Scan using index_vulnerability_reads_on_project_id_and_vulnerability_id on public.vulnerability_reads vr (actual time=126.932..10388.871)
Index Cond: ((vr.project_id = <project>) AND (vr.vulnerability_id >= <min>) AND (vr.vulnerability_id <= <max>))
I/O Timings: read=10082.652
SubPlan 1
-> Index Only Scan using idx_vulnerability_issue_links_on_vulnerability_id_and_issue_id on public.vulnerability_issue_links vil (actual time=0.094..0.094)
Index Cond: (vil.vulnerability_id = vr.vulnerability_id)
SubPlan 4
-> Index Only Scan using idx_vulnerability_ext_issue_links_on_vulne_id_and_ext_issue on public.vulnerability_external_issue_links veil (actual time=1.531..64.688)
Time: 10.398 s (execution 10.390 s)The new probe is planned as a hashed subplan over the whole external-links index, about 65 ms cold and about 28 ms warm per window. If we want that cheaper, adding veil.project_id = <project> (indexed) would narrow the hash to the project's own links. I've left it out of this hotfix to keep it minimal, and it's tracked in #632193.
Tests
trigger_parity_check_spec.rb: Jira-only vulnerabilities keephas_issues = true, afalsevalue is corrected, and missing reads are created withhas_issues = true. The N+1 guard for missing reads now covers theexternal_issue_linkspreload.update_vulnerability_read_spec.rb: removing an external link keepstruewhen a GitLab issue link or another external link remains.- Locally: 57 examples, 0 failures. Negative control: against the old code, all 5 new examples fail.