Skip to content

feat: stewardship api unmock (CM-1218)#4195

Merged
ulemons merged 11 commits into
mainfrom
feat/stewardship-api-unmock
Jun 11, 2026
Merged

feat: stewardship api unmock (CM-1218)#4195
ulemons merged 11 commits into
mainfrom
feat/stewardship-api-unmock

Conversation

@ulemons

@ulemons ulemons commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Removes all mock implementations from the 4 packages public API endpoints and replaces them with real queries against the packages DB. Also lands the stewardship schema migration and a backfill script to seed one unassigned stewardship row per critical package.

Changes

  • Stewardship schema — migration V1781094067__stewardship-tables.sql creates stewardships, stewardship_stewards, stewardship_activity, stewardship_assessments, stewardship_findings, stewardship_remediation_actions; in v1 only stewardships is populated
  • Backfill script — packages_worker/src/bin/stewardship-backfill.ts + runStewardshipBackfill — cursor-based idempotent batch job that inserts one unassigned row per critical package; re-checks is_critical at insert time, safe to re-run
  • DAL — new data-access-layer/src/osspckgs/api.ts with 5 query functions (getPackageMetrics, getPackagesByStewardshipPurls, listPackagesForApi, getPackageDetailByPurl, getAdvisoriesByPackageId); includes LATERAL join on package_repos+repos for scorecard/security/repo fields and subquery on downloads_last_30d
  • Backend config — CROWD_PACKAGES_DB_* env vars mapped in custom-environment-variables.json; lazy promise based singleton in backend/src/db/packagesDb.ts (race-safe: caches the in-flight Promise, not the resolved value)
  • API endpoints — all 4 endpoints (GET /packages/metrics, GET /packages, POST /packages:batch-stewardship, GET /packages/detail) now read from the real packages DB; v2 fields (healthScore, lifecycle, maintainerBusFactor, openVulns, stewards) remain null by design until future enrichers populate them
  • Field notes — packages.impact is the renamed criticality_score (migration V1780589607); packages.dependent_count is the renamed dependent_packages_count (migration V1780394591)

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Performance improvement
  • Chore / dependency update
  • Documentation

JIRA ticket

ticket


Note

Medium Risk
Public API responses now depend on packages DB availability and real data semantics; advisory patched/open logic uses lexicographic version comparison, which can misclassify semver ordering.

Overview
Wires the public packages API to the packages database instead of in-memory mocks, via CROWD_PACKAGES_DB_* config and a lazy getPackagesQx() connection helper.

Four endpoints now use new DAL queries in osspckgs/api.ts: metrics counts critical packages, list supports pagination/filters (ecosystem, stale, unstewarded) and DB-side sort, batch stewardship resolves by PURL, and package detail assembles impact, repo/scorecard fields, stewardship status, and advisories with open/patched resolution. Response shape is preserved but several fields are intentionally null until later work (health, lifecycle, stewards, transitive reach); list still accepts lifecycle / busFactor1Only query params but those filters are not applied in the DAL yet; sortBy=health falls back to name.

getPackage validates pkg: PURLs with Zod instead of a manual BadRequestError.

Reviewed by Cursor Bugbot for commit eefb9ed. Bugbot is set up for automated code reviews on this repo. Configure here.

ulemons added 2 commits June 10, 2026 16:17
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
Copilot AI review requested due to automatic review settings June 10, 2026 18:37
@ulemons ulemons self-assigned this Jun 10, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conventional Commits FTW!

if (unstewardedOnly && p.stewardship !== 'unassigned') return false
if (staleOnly) {
const lastRelease = MOCK_DETAILS[p.purl]?.general.riskSignals.lastRelease
if (!lastRelease || new Date(lastRelease) >= staleThreshold) return false

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignored list query filters

Medium Severity

GET /packages still validates and echoes lifecycle and busFactor1Only, but those values are no longer passed into listPackagesForApi, so results stay unfiltered while the response filters object implies they were applied.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2d2aa85. Configure here.

Comment thread services/libs/data-access-layer/src/osspckgs/api.ts
@ulemons ulemons changed the title Feat/stewardship api unmock feat: stewardship api unmock (CM-1218) Jun 10, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR wires the public v1 Packages/Stewardship API endpoints to the real packages-db (instead of in-memory mocks) by introducing a packages DB connection helper in the backend and a new DAL module (osspckgs/api) that queries package metrics, listings, detail, advisories, and batch stewardship rows.

Changes:

  • Add packagesDb config/env wiring (CROWD_PACKAGES_DB_*) and a lazy getPackagesQx() connection helper.
  • Add new DAL query module services/libs/data-access-layer/src/osspckgs/api.ts and export it through DAL entrypoints.
  • Update public API handlers (/packages, /packages/metrics, /packages/detail, /packages:batch-stewardship) to fetch from the packages DB and return v1 responses (with several v2 fields intentionally null).

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
services/libs/data-access-layer/src/osspckgs/index.ts Re-export new OSS packages DAL API module.
services/libs/data-access-layer/src/osspckgs/api.ts Introduce SQL queries backing metrics, list, detail, advisories, and batch stewardship lookup.
services/libs/data-access-layer/src/index.ts Export new DAL API module from the package root.
backend/src/db/packagesDb.ts Add lazy, singleton-style packages DB QueryExecutor initializer.
backend/src/conf/index.ts Add PACKAGES_DB_CONFIG configuration entry.
backend/src/api/public/v1/packages/listPackages.ts Replace mock listing logic with DAL-backed pagination and mapping.
backend/src/api/public/v1/packages/getPackagesMetrics.ts Replace mock metrics with DAL-backed metrics query.
backend/src/api/public/v1/packages/getPackage.ts Replace mock detail response with DB-backed package + advisories fetch and response mapping.
backend/src/api/public/v1/packages/batchGetStewardship.ts Replace mock batch stewardship with DB-backed lookup keyed by PURL.
backend/config/custom-environment-variables.json Map packagesDb config to CROWD_PACKAGES_DB_* environment variables.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +44 to 53
const qx = await getPackagesQx()
const { rows, total } = await listPackagesForApi(qx, {
page,
pageSize,
ecosystem,
staleOnly,
unstewardedOnly,
sortBy,
sortDir,
})
openVulns: null,
stewardship: (r.stewardshipStatus ?? 'unassigned') as StewardshipStatus,
stewards: null,
}))
Comment thread services/libs/data-access-layer/src/osspckgs/api.ts Outdated
Comment thread backend/src/db/packagesDb.ts
general: {
healthScore: null,
impact: {
impactScore: pkg.criticalityScore != null ? Math.round(Number(pkg.criticalityScore)) : null,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest to * 100 and then do a Math.round

ok(res, detail)
const advisories = await getAdvisoriesByPackageId(qx, pkg.id)

ok(res, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why transitiveReach, maintainerBusFactor, resolution, securityContacts (this one we should check with Mouad how he is storing these) are null?

Comment on lines +62 to +63
maintainerBusFactor: null,
openVulns: null,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could get both of these

Comment on lines +11 to +12
COUNT(*) AS total,
COUNT(*) FILTER (WHERE has_critical_vulnerability = true) AS critical

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should use `has_critical_vulnerability for this the critical ones. I'm a bit unsure what Nuno meant by this, but for now we could use the ones where Health is critical. Let's add a todo here to confirm this with product, and we can update later.

ulemons and others added 2 commits June 11, 2026 10:30
… filter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
@ulemons ulemons force-pushed the feat/backfill-stewardship-script branch from 399b9ba to a739c9f Compare June 11, 2026 08:33
ulemons added 3 commits June 11, 2026 10:35
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
@ulemons ulemons force-pushed the feat/stewardship-api-unmock branch from 2d2aa85 to 3066140 Compare June 11, 2026 10:39
impact: r.criticalityScore != null ? Math.round(Number(r.criticalityScore) * 100) : null,
lifecycle: null,
maintainerBusFactor: null,
openVulns: r.openVulns,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List openVulns wrong response shape

High Severity

GET /packages now puts a plain integer in openVulns, but the public contract and OpenVulns type expect an object with low, medium, high, and critical counts (or null). Clients that read severity fields from the list response will get wrong or missing data.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3066140. Configure here.

SELECT
COUNT(*) AS total,
-- TODO: confirm with product whether "critical" here means health=critical, not has_critical_vulnerability
COUNT(*) FILTER (WHERE has_critical_vulnerability = true) AS critical

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metrics critical count wrong criterion

Medium Severity

getPackageMetrics sets criticalPackages using has_critical_vulnerability, while product review on this PR indicated that metric should reflect critical health (with confirmation pending), not the vulnerability flag. Dashboard totals can disagree with intended stewardship semantics.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3066140. Configure here.

WHEN ar.fixed_version IS NULL AND ar.last_affected IS NULL THEN FALSE
WHEN ar.fixed_version IS NOT NULL AND p.latest_version >= ar.fixed_version THEN TRUE
WHEN ar.fixed_version IS NOT NULL THEN FALSE
WHEN ar.last_affected IS NOT NULL AND p.latest_version > ar.last_affected THEN TRUE

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Advisory resolution uses string compare

Medium Severity

getAdvisoriesByPackageId marks advisories patched or open by comparing latest_version to range bounds with SQL >= and >. That is lexicographic, not semver or Maven ordering, so patched vs open can be wrong for typical version strings.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3066140. Configure here.

pkg.downloadsLast30d != null ? parseInt(pkg.downloadsLast30d, 10) : null,
dependentPackages: pkg.dependentPackagesCount ?? null,
dependentRepos: pkg.dependentReposCount ?? null,
transitiveReach: pkg.transitiveReach,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

transitiveReach wrong API format

Medium Severity

Package detail returns transitiveReach as a numeric percentile rank from SQL, while the public schema and previous mock responses use a human-readable string such as Top 0.4%. Consumers expecting the documented string format will misinterpret or fail to render the field.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3066140. Configure here.

@ulemons ulemons force-pushed the feat/backfill-stewardship-script branch from a739c9f to 4e73636 Compare June 11, 2026 11:34
Base automatically changed from feat/backfill-stewardship-script to main June 11, 2026 11:43
Copilot AI review requested due to automatic review settings June 11, 2026 12:08
},
provenance: {
repositoryMapping: {
declaredRepo: pkg.repoUrl ?? pkg.repositoryUrl ?? pkg.declaredRepositoryUrl ?? null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declared repo uses mapped URL

Medium Severity

provenance.repositoryMapping.declaredRepo prefers repoUrl from the best-confidence repo join before declaredRepositoryUrl, so the field can expose an inferred mapping instead of the package’s declared repository.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8a2b724. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Comment on lines +62 to +67
health: null,
impact: r.criticalityScore != null ? Math.round(Number(r.criticalityScore) * 100) : null,
lifecycle: null,
maintainerBusFactor: null,
openVulns: r.openVulns,
stewardship: (r.stewardshipStatus ?? 'unassigned') as StewardshipStatus,
Comment on lines +43 to +47
pkg.downloadsLast30d != null ? parseInt(pkg.downloadsLast30d, 10) : null,
dependentPackages: pkg.dependentPackagesCount ?? null,
dependentRepos: pkg.dependentReposCount ?? null,
transitiveReach: pkg.transitiveReach,
},
Comment on lines +116 to +121
p.latest_release_at AS "latestReleaseAt",
s.status AS "stewardshipStatus",
(SELECT COUNT(*)::int FROM advisory_packages ap WHERE ap.package_id = p.id) AS "openVulns",
COUNT(*) OVER() AS total
FROM packages p
LEFT JOIN stewardships s ON s.package_id = p.id
Comment on lines +206 to +215
-- percentile rank within ecosystem (0=least, 1=most transitive reach)
(
SELECT r.prank
FROM (
SELECT purl, PERCENT_RANK() OVER (PARTITION BY ecosystem ORDER BY transitive_dependent_count ASC NULLS FIRST) AS prank
FROM packages
WHERE ecosystem = p.ecosystem
) r
WHERE r.purl = p.purl
) AS "transitiveReach"
ulemons added 2 commits June 11, 2026 16:49
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
Copilot AI review requested due to automatic review settings June 11, 2026 14:51

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 8 total unresolved issues (including 6 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit dc97258. Configure here.

LEFT JOIN stewardships s ON s.package_id = p.id
LEFT JOIN LATERAL (
SELECT COUNT(*)::int AS cnt FROM advisory_packages WHERE package_id = p.id
) ap_counts ON true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openVulns counts all advisories

Medium Severity

The list query labels the lateral count as openVulns, but it counts every advisory_packages row for the package with no check for patched or open resolution. Sorting and display by openVulns therefore reflect total linked advisories, not open vulnerabilities.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit dc97258. Configure here.

LIMIT 1
) pr ON true
LEFT JOIN repos r ON r.id = pr.repo_id
WHERE p.purl = $(purl)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Detail omits critical package scope

Medium Severity

Package detail loads any row by purl, while the list API restricts to is_critical = true. Non-critical packages in the same table can return full detail even though they never appear in the stewardship list.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit dc97258. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Comment on lines +62 to +67
health: null,
impact: r.criticalityScore != null ? Math.round(Number(r.criticalityScore) * 100) : null,
lifecycle: null,
maintainerBusFactor: null,
openVulns: r.openVulns,
stewardship: (r.stewardshipStatus ?? 'unassigned') as StewardshipStatus,
Comment on lines 19 to 29
const querySchema = z.object({
page: z.coerce.number().int().min(1).default(1),
pageSize: z.coerce.number().int().min(1).max(MAX_PAGE_SIZE).default(DEFAULT_PAGE_SIZE),
ecosystem: z.string().trim().optional(),
lifecycle: z.enum(lifecycleValues).optional(),
busFactor1Only: booleanQueryParam,
lifecycle: z.enum(lifecycleValues).optional(), // TODO: filter not yet implemented in DAL
busFactor1Only: booleanQueryParam, // TODO: filter not yet implemented in DAL
staleOnly: booleanQueryParam,
unstewardedOnly: booleanQueryParam,
sortBy: z.enum(['name', 'health', 'impact', 'openVulns']).default('name'),
sortDir: z.enum(['asc', 'desc']).default('asc'),
})
Comment on lines 81 to 83
},
sort: { by: sortBy, dir: sortDir },
sort: { by: effectiveSortBy, dir: sortDir },
packages,
Comment on lines +242 to +245
SELECT
a.osv_id AS "osvId",
LOWER(a.severity) AS severity,
CASE
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
@ulemons ulemons merged commit 730d5cf into main Jun 11, 2026
12 checks passed
@ulemons ulemons deleted the feat/stewardship-api-unmock branch June 11, 2026 15:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants