From e8efbdd4d9088f2487df5a1e7d1b589f3d80b68f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C5=A1per=20Grom?= Date: Fri, 28 Aug 2026 12:28:43 +0100 Subject: [PATCH 1/4] feat: add repo-filtered health score and lifecycle pipes (IN-1253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gašper Grom --- .../pipes/repo_health_score_v2_breakdown.pipe | 101 ++++++++++++++++++ .../tinybird/pipes/repo_lifecycle_v2.pipe | 42 ++++++++ 2 files changed, 143 insertions(+) create mode 100644 services/libs/tinybird/pipes/repo_health_score_v2_breakdown.pipe create mode 100644 services/libs/tinybird/pipes/repo_lifecycle_v2.pipe diff --git a/services/libs/tinybird/pipes/repo_health_score_v2_breakdown.pipe b/services/libs/tinybird/pipes/repo_health_score_v2_breakdown.pipe new file mode 100644 index 0000000000..04d01da73d --- /dev/null +++ b/services/libs/tinybird/pipes/repo_health_score_v2_breakdown.pipe @@ -0,0 +1,101 @@ +DESCRIPTION > + - `repo_health_score_v2_breakdown.pipe` is the live, request-time counterpart to + `project_insights_health_breakdown_copy.pipe`'s project-level Health Score v2 signal breakdown + rollup, scoped to a caller-supplied set of repo URLs instead of a project's full repo set. Powers + the Overview page's repo selector (IN-1253): when a subset of a project's repos is selected, the + Health breakdown box is recomputed live for just that subset's active repos. + - Per-column aggregation is copied 1:1 from `project_insights_health_breakdown_copy.pipe` — see + that pipe's DESCRIPTION for the full per-column methodology and rationale (coverage-filtered + `sumIf(score, available) / nullIf(countIf(available), 0)` for `*Available`-backed scores, `max()` + for counts/flags, `min()` for `daysSinceLatest`/`daysBetweenRecent`/`isGerrit`/`isExcluded`, + `avg()` for median-seconds columns and `commitActivityScore`). + - Join path: `repositories` (filtered by the `repos` param, `enabled = true AND excluded = false`) + -> `health_score_v2_signal_detail_ds` (matched on `repoUrl` = `rep.url`) — same population filter + as the project-level rollup, so archived/excluded repos in a mixed selection are dropped from the + aggregation. When every repo in `repos` is archived/excluded, this query returns zero rows. + - No `slug`/project grouping — this collapses to a single row for the given repo set, unlike the + project-level copy pipe which groups by project. + - Parameters: `repos` (required array of repo URLs). + +TOKEN "insights-app-token" READ + +TAGS "Insights, Widget", "Project", "Health" + +NODE repo_health_score_v2_breakdown_endpoint +SQL > + % + SELECT + sumIf(sd.busFactorScore, sd.busFactorAvailable) + / nullIf(countIf(sd.busFactorAvailable), 0) AS busFactorScore, + max(sd.busFactorAvailable) AS busFactorAvailable, + max(sd.busFactorCount) AS busFactorCount, + sumIf(sd.orgDiversityScore, sd.orgDiversityAvailable) + / nullIf(countIf(sd.orgDiversityAvailable), 0) AS orgDiversityScore, + max(sd.orgDiversityAvailable) AS orgDiversityAvailable, + max(sd.orgCount) AS orgCount, + sumIf(sd.responsivenessScore, sd.responsivenessAvailable) + / nullIf(countIf(sd.responsivenessAvailable), 0) AS responsivenessScore, + max(sd.responsivenessAvailable) AS responsivenessAvailable, + avg(sd.medianPrResponseS) AS medianPrResponseS, + avg(sd.medianIssueResponseS) AS medianIssueResponseS, + min(sd.isGerrit) AS isGerrit, + min(sd.isExcluded) AS isExcluded, + sumIf(sd.openVulnScore, sd.openVulnAvailable) + / nullIf(countIf(sd.openVulnAvailable), 0) AS openVulnScore, + max(sd.openVulnAvailable) AS openVulnAvailable, + max(sd.openCriticals) AS openCriticals, + max(sd.openHighs) AS openHighs, + max(sd.openModerates) AS openModerates, + sumIf(sd.scorecardScorePts, sd.scorecardAvailable) + / nullIf(countIf(sd.scorecardAvailable), 0) AS scorecardScorePts, + max(sd.scorecardAvailable) AS scorecardAvailable, + sumIf(toFloat64OrNull(sd.scorecardScore), sd.scorecardAvailable) + / nullIf(countIf(sd.scorecardAvailable), 0) AS scorecardScore, + sumIf(sd.securityPracticesScore, sd.securityPracticesAvailable) + / nullIf(countIf(sd.securityPracticesAvailable), 0) AS securityPracticesScore, + max(sd.securityPracticesAvailable) AS securityPracticesAvailable, + max(sd.securityPolicyEnabled) AS securityPolicyEnabled, + max(sd.branchProtectionEnabled) AS branchProtectionEnabled, + max(sd.branchProtectionRequiredReviews) AS branchProtectionRequiredReviews, + max(sd.branchProtectionRequiresStatusChecks) AS branchProtectionRequiresStatusChecks, + max(sd.branchProtectionAllowsForcePush) AS branchProtectionAllowsForcePush, + sumIf(sd.dependencyHealthScore, sd.dependencyHealthAvailable) + / nullIf(countIf(sd.dependencyHealthAvailable), 0) AS dependencyHealthScore, + max(sd.dependencyHealthAvailable) AS dependencyHealthAvailable, + max(sd.vulnerableDeps) AS vulnerableDeps, + sumIf(sd.releaseCadenceScore, sd.releaseCadenceAvailable) + / nullIf(countIf(sd.releaseCadenceAvailable), 0) AS releaseCadenceScore, + max(sd.releaseCadenceAvailable) AS releaseCadenceAvailable, + min(sd.daysSinceLatest) AS daysSinceLatest, + min(sd.daysBetweenRecent) AS daysBetweenRecent, + avg(sd.commitActivityScore) AS commitActivityScore, + max(sd.commitsLast6m) AS commitsLast6m, + max(sd.lastCommitAt) AS lastCommitAt, + sumIf(sd.issueResolutionScore, sd.issueResolutionAvailable) + / nullIf(countIf(sd.issueResolutionAvailable), 0) AS issueResolutionScore, + max(sd.issueResolutionAvailable) AS issueResolutionAvailable, + max(sd.closed12m) AS closed12m, + max(sd.opened12m) AS opened12m, + avg(sd.medianCloseS) AS medianCloseS, + sumIf(sd.prMergeScore, sd.prMergeAvailable) + / nullIf(countIf(sd.prMergeAvailable), 0) AS prMergeScore, + max(sd.prMergeAvailable) AS prMergeAvailable, + max(sd.merged12m) AS merged12m, + max(sd.closedUnmerged12m) AS closedUnmerged12m, + avg(sd.medianMergeS) AS medianMergeS, + sumIf( + coalesce(sd.commitsLast6m, 0) * coalesce(sd.busFactorScore, 0), + sd.busFactorAvailable + ) / nullIf( + sumIf( + coalesce(sd.commitsLast6m, 0), sd.busFactorAvailable AND sd.commitsLast6m IS NOT NULL + ), + 0 + ) AS busFactorScoreActivityWeightedMean + FROM repositories rep FINAL + INNER JOIN health_score_v2_signal_detail_ds sd ON sd.repoUrl = rep.url + WHERE + rep.url + IN {{ Array(repos, 'String', description="Selected repo URLs", required=True) }} + AND rep.enabled = true + AND rep.excluded = false diff --git a/services/libs/tinybird/pipes/repo_lifecycle_v2.pipe b/services/libs/tinybird/pipes/repo_lifecycle_v2.pipe new file mode 100644 index 0000000000..0650aceb49 --- /dev/null +++ b/services/libs/tinybird/pipes/repo_lifecycle_v2.pipe @@ -0,0 +1,42 @@ +DESCRIPTION > + - `repo_lifecycle_v2.pipe` is the live, request-time counterpart to `project_insights_copy.pipe`'s + `project_insights_copy_health_v2_project` lifecycle rollup, scoped to a caller-supplied set of repo + URLs instead of a project's full repo set. Powers the Overview page's repo selector (IN-1253): + when a subset of a project's repos is selected, Lifecycle is recomputed live for just that subset. + - Deliberately does NOT filter on `enabled`/`excluded` — `archived` is itself a valid + `lifecycleLabelV2` value (per the spec's decision tree: archived flag > abandoned > inert > + declining > stable > active, first match wins), so an all-archived/excluded selection must still + produce a real lifecycle label rather than being dropped from the rollup. + - Reads `health_score_v2_repo_copy_ds` directly, no join with `repositories`. + - Same best-state-wins precedence and `if(empty(groupArray(...)), NULL, ...)` NULL-guard as + `project_insights_copy_health_v2_project` (see that node's DESCRIPTION for the `arrayElement` on + an empty array returning `''` instead of NULL rationale, IN-1196). + - Parameters: `repos` (required array of repo URLs). + +TOKEN "insights-app-token" READ + +TAGS "Insights, Widget", "Project", "Health" + +NODE repo_lifecycle_v2_endpoint +SQL > + % + SELECT + if( + empty(groupArray(lifecycleLabelV2)), + NULL, + toNullable( + arrayElement( + arraySort( + x -> indexOf( + ['active', 'stable', 'declining', 'inert', 'abandoned', 'archived'], x + ), + groupArray(lifecycleLabelV2) + ), + 1 + ) + ) + ) AS lifecycleLabel + FROM health_score_v2_repo_copy_ds + WHERE + repoUrl + IN {{ Array(repos, 'String', description="Selected repo URLs", required=True) }} From b4e31fb67c6f585575e302def9e811a833dd2b29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C5=A1per=20Grom?= Date: Fri, 28 Aug 2026 12:54:38 +0100 Subject: [PATCH 2/4] fix: return zero rows for all-archived/excluded repo selection (IN-1253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gašper Grom --- .../pipes/repo_health_score_v2_breakdown.pipe | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/services/libs/tinybird/pipes/repo_health_score_v2_breakdown.pipe b/services/libs/tinybird/pipes/repo_health_score_v2_breakdown.pipe index 04d01da73d..325db23655 100644 --- a/services/libs/tinybird/pipes/repo_health_score_v2_breakdown.pipe +++ b/services/libs/tinybird/pipes/repo_health_score_v2_breakdown.pipe @@ -12,7 +12,8 @@ DESCRIPTION > - Join path: `repositories` (filtered by the `repos` param, `enabled = true AND excluded = false`) -> `health_score_v2_signal_detail_ds` (matched on `repoUrl` = `rep.url`) — same population filter as the project-level rollup, so archived/excluded repos in a mixed selection are dropped from the - aggregation. When every repo in `repos` is archived/excluded, this query returns zero rows. + aggregation. `HAVING count() > 0` ensures that when every repo in `repos` is archived/excluded, + this query returns zero rows rather than one row of NULL/NaN aggregates over an empty input. - No `slug`/project grouping — this collapses to a single row for the given repo set, unlike the project-level copy pipe which groups by project. - Parameters: `repos` (required array of repo URLs). @@ -82,16 +83,7 @@ SQL > max(sd.prMergeAvailable) AS prMergeAvailable, max(sd.merged12m) AS merged12m, max(sd.closedUnmerged12m) AS closedUnmerged12m, - avg(sd.medianMergeS) AS medianMergeS, - sumIf( - coalesce(sd.commitsLast6m, 0) * coalesce(sd.busFactorScore, 0), - sd.busFactorAvailable - ) / nullIf( - sumIf( - coalesce(sd.commitsLast6m, 0), sd.busFactorAvailable AND sd.commitsLast6m IS NOT NULL - ), - 0 - ) AS busFactorScoreActivityWeightedMean + avg(sd.medianMergeS) AS medianMergeS FROM repositories rep FINAL INNER JOIN health_score_v2_signal_detail_ds sd ON sd.repoUrl = rep.url WHERE @@ -99,3 +91,4 @@ SQL > IN {{ Array(repos, 'String', description="Selected repo URLs", required=True) }} AND rep.enabled = true AND rep.excluded = false + HAVING count() > 0 From 24c0c92806accb1d2e82bf14ca9c3718ef382812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C5=A1per=20Grom?= Date: Fri, 28 Aug 2026 13:00:24 +0100 Subject: [PATCH 3/4] fix: scope repo-filtered health/lifecycle pipes to calling project (IN-1253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gašper Grom --- .../pipes/repo_health_score_v2_breakdown.pipe | 7 ++++++- services/libs/tinybird/pipes/repo_lifecycle_v2.pipe | 12 +++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/services/libs/tinybird/pipes/repo_health_score_v2_breakdown.pipe b/services/libs/tinybird/pipes/repo_health_score_v2_breakdown.pipe index 325db23655..926e45d1ae 100644 --- a/services/libs/tinybird/pipes/repo_health_score_v2_breakdown.pipe +++ b/services/libs/tinybird/pipes/repo_health_score_v2_breakdown.pipe @@ -16,7 +16,10 @@ DESCRIPTION > this query returns zero rows rather than one row of NULL/NaN aggregates over an empty input. - No `slug`/project grouping — this collapses to a single row for the given repo set, unlike the project-level copy pipe which groups by project. - - Parameters: `repos` (required array of repo URLs). + - Additionally joins `insightsProjects` (matched on `repositories.insightsProjectId` = `id`) and + requires `insightsProjects.slug` = the caller's `slug` param, so a `repos` value from a different + project can't leak that project's repo-level data into this response. + - Parameters: `slug` (required, the calling project's slug), `repos` (required array of repo URLs). TOKEN "insights-app-token" READ @@ -86,9 +89,11 @@ SQL > avg(sd.medianMergeS) AS medianMergeS FROM repositories rep FINAL INNER JOIN health_score_v2_signal_detail_ds sd ON sd.repoUrl = rep.url + INNER JOIN insightsProjects ip FINAL ON ip.id = rep.insightsProjectId WHERE rep.url IN {{ Array(repos, 'String', description="Selected repo URLs", required=True) }} AND rep.enabled = true AND rep.excluded = false + AND ip.slug = {{ String(slug, description="Calling project's slug", required=True) }} HAVING count() > 0 diff --git a/services/libs/tinybird/pipes/repo_lifecycle_v2.pipe b/services/libs/tinybird/pipes/repo_lifecycle_v2.pipe index 0650aceb49..459afe64b9 100644 --- a/services/libs/tinybird/pipes/repo_lifecycle_v2.pipe +++ b/services/libs/tinybird/pipes/repo_lifecycle_v2.pipe @@ -11,7 +11,10 @@ DESCRIPTION > - Same best-state-wins precedence and `if(empty(groupArray(...)), NULL, ...)` NULL-guard as `project_insights_copy_health_v2_project` (see that node's DESCRIPTION for the `arrayElement` on an empty array returning `''` instead of NULL rationale, IN-1196). - - Parameters: `repos` (required array of repo URLs). + - Joins `repositories` (matched on `url` = `repoUrl`) to `insightsProjects` (matched on + `insightsProjectId` = `id`) and requires `insightsProjects.slug` = the caller's `slug` param, so a + `repos` value from a different project can't leak that project's repo-level data into this response. + - Parameters: `slug` (required, the calling project's slug), `repos` (required array of repo URLs). TOKEN "insights-app-token" READ @@ -36,7 +39,10 @@ SQL > ) ) ) AS lifecycleLabel - FROM health_score_v2_repo_copy_ds + FROM health_score_v2_repo_copy_ds hs + INNER JOIN repositories rep FINAL ON rep.url = hs.repoUrl + INNER JOIN insightsProjects ip FINAL ON ip.id = rep.insightsProjectId WHERE - repoUrl + hs.repoUrl IN {{ Array(repos, 'String', description="Selected repo URLs", required=True) }} + AND ip.slug = {{ String(slug, description="Calling project's slug", required=True) }} From c1828c9a7e2dc8121f3255fa21e356b3096f3661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C5=A1per=20Grom?= Date: Fri, 28 Aug 2026 18:45:17 +0100 Subject: [PATCH 4/4] fix: compute repo-scoped healthScoreV2/healthLabel in breakdown pipe (IN-1253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gašper Grom --- .../pipes/repo_health_score_v2_breakdown.pipe | 49 +++++++++++++++++-- .../tinybird/pipes/repo_lifecycle_v2.pipe | 3 +- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/services/libs/tinybird/pipes/repo_health_score_v2_breakdown.pipe b/services/libs/tinybird/pipes/repo_health_score_v2_breakdown.pipe index 926e45d1ae..b8f18090dc 100644 --- a/services/libs/tinybird/pipes/repo_health_score_v2_breakdown.pipe +++ b/services/libs/tinybird/pipes/repo_health_score_v2_breakdown.pipe @@ -19,13 +19,26 @@ DESCRIPTION > - Additionally joins `insightsProjects` (matched on `repositories.insightsProjectId` = `id`) and requires `insightsProjects.slug` = the caller's `slug` param, so a `repos` value from a different project can't leak that project's repo-level data into this response. + - `maintainerHealthScoreV2` / `securitySupplyChainScoreV2` / `developmentActivityScoreV2` (IN-1253): + same exact-median (`quantileExact(0.5)`) rollup of `health_score_v2_repo_copy_ds`'s per-repo category + scores that `project_insights_copy.pipe` uses at the project level, scoped here to the caller-supplied + `repos` set instead of a project's full repo set — kept consistent so a single-repo selection reduces + to that repo's own per-repo score. Joined via the same `repositories` (enabled/non-excluded, project- + scope-checked) population as the rest of this pipe, so a repo with no `health_score_v2_repo_copy_ds` + row (LEFT JOIN) contributes NULL rather than being silently dropped from the median. + - `healthScoreV2` / `healthLabel` (IN-1253): same exact-median rollup of `health_score_v2_repo_copy_ds`'s + per-repo total, with the label bands copied 1:1 from `project_insights_copy.pipe` + (>=85 excellent, >=70 healthy, >=50 fair, >=30 concerning, else critical). This pipe only computes the + value — whether the caller's UI actually displays a repo-scoped total (e.g. a dedicated single-repo + page) versus suppressing it (a multi-repo filter selection, where the total is intentionally hidden per + IN-1253 State 2) is a display decision made by the frontend, not this pipe. - Parameters: `slug` (required, the calling project's slug), `repos` (required array of repo URLs). TOKEN "insights-app-token" READ TAGS "Insights, Widget", "Project", "Health" -NODE repo_health_score_v2_breakdown_endpoint +NODE repo_health_score_v2_breakdown_category_totals SQL > % SELECT @@ -86,14 +99,42 @@ SQL > max(sd.prMergeAvailable) AS prMergeAvailable, max(sd.merged12m) AS merged12m, max(sd.closedUnmerged12m) AS closedUnmerged12m, - avg(sd.medianMergeS) AS medianMergeS + avg(sd.medianMergeS) AS medianMergeS, + quantileExact(0.5) + (hv2.maintainerHealthScoreV2) AS maintainerHealthScoreV2, + quantileExact(0.5) + (hv2.securitySupplyChainScoreV2) AS securitySupplyChainScoreV2, + quantileExact(0.5) + (hv2.developmentActivityScoreV2) AS developmentActivityScoreV2, + quantileExact(0.5) + (hv2.healthScoreV2) AS healthScoreV2Raw FROM repositories rep FINAL INNER JOIN health_score_v2_signal_detail_ds sd ON sd.repoUrl = rep.url INNER JOIN insightsProjects ip FINAL ON ip.id = rep.insightsProjectId + LEFT JOIN health_score_v2_repo_copy_ds hv2 ON hv2.repoUrl = rep.url WHERE - rep.url - IN {{ Array(repos, 'String', description="Selected repo URLs", required=True) }} + rep.url IN {{ Array(repos, 'String', description="Selected repo URLs", required=True) }} AND rep.enabled = true AND rep.excluded = false AND ip.slug = {{ String(slug, description="Calling project's slug", required=True) }} HAVING count() > 0 + +NODE repo_health_score_v2_breakdown_endpoint +SQL > + SELECT + * EXCEPT (healthScoreV2Raw), + toUInt8(round(healthScoreV2Raw)) AS healthScoreV2, + multiIf( + healthScoreV2Raw IS NULL, + NULL, + healthScoreV2Raw >= 85, + 'excellent', + healthScoreV2Raw >= 70, + 'healthy', + healthScoreV2Raw >= 50, + 'fair', + healthScoreV2Raw >= 30, + 'concerning', + 'critical' + ) AS healthLabel + FROM repo_health_score_v2_breakdown_category_totals diff --git a/services/libs/tinybird/pipes/repo_lifecycle_v2.pipe b/services/libs/tinybird/pipes/repo_lifecycle_v2.pipe index 459afe64b9..ffa15c207d 100644 --- a/services/libs/tinybird/pipes/repo_lifecycle_v2.pipe +++ b/services/libs/tinybird/pipes/repo_lifecycle_v2.pipe @@ -43,6 +43,5 @@ SQL > INNER JOIN repositories rep FINAL ON rep.url = hs.repoUrl INNER JOIN insightsProjects ip FINAL ON ip.id = rep.insightsProjectId WHERE - hs.repoUrl - IN {{ Array(repos, 'String', description="Selected repo URLs", required=True) }} + hs.repoUrl IN {{ Array(repos, 'String', description="Selected repo URLs", required=True) }} AND ip.slug = {{ String(slug, description="Calling project's slug", required=True) }}