Skip to content

Commit 4f6f192

Browse files
authored
Merge branch 'main' into chore/update-pvtr-version
2 parents aeccd3e + b14d417 commit 4f6f192

22 files changed

Lines changed: 349 additions & 103 deletions

File tree

backend/.env.dist.local

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ OSSPCKGS_GCP_CREDENTIALS_B64=e30=
199199
# maven/all.zip 404s). The allowlist check and DB storage normalize to lowercase
200200
# internally per ADR-0001 §OSV "Ecosystem normalization", so downstream stays lowercase.
201201
OSV_BULK_BASE_URL=https://osv-vulnerabilities.storage.googleapis.com
202-
OSV_ECOSYSTEMS=npm,Maven,cargo,NuGet
202+
OSV_ECOSYSTEMS=npm,Maven,cargo,NuGet,RubyGems,Go
203203
OSV_TMP_DIR=/tmp/osv
204204
OSV_BATCH_SIZE=500
205205
OSV_DERIVE_BATCH_SIZE=1000

backend/src/api/public/v1/packages/getPackage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export async function getPackage(req: Request, res: Response): Promise<void> {
130130
},
131131
provenance: {
132132
repositoryMapping: {
133-
declaredRepo: pkg.repoUrl ?? pkg.repositoryUrl ?? pkg.declaredRepositoryUrl ?? null,
133+
declaredRepo: pkg.repoUrl ?? pkg.repositoryUrl ?? null,
134134
mappingConfidence,
135135
mappingLabel: repoMappingLabel(mappingConfidence),
136136
lastCommitAt: pkg.repoLastCommitAt ? pkg.repoLastCommitAt.toISOString() : null,

docs/adr/0004-go-nuget-transitive-dependent-counts.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,14 @@ methodology. We chose exact over an HLL-sketch approximation, accepting the high
9797
- **Weekly trigger.** No new schedule — the `go`/`nuget` variants are already child workflows of
9898
`bootstrapOsspckgs`, which the existing `osspckgs-bootstrap-weekly` schedule runs incrementally.
9999

100+
## Addendum: RUBYGEMS (2026-07-13)
101+
RubyGems is also absent from deps.dev's `Dependents` reverse index (verified via BQ 2026-07-13:
102+
`Dependents` contains only NPM/MAVEN/PYPI/CARGO) and has no resolved/transitive graph table, only
103+
the raw manifest `RubyGemsRequirementsLatest` — same shape as GO/NUGET. It was added as a third
104+
`isScript` closure variant (`dependent_counts_rubygems`), reusing this ADR's design unchanged:
105+
same semi-naive fixpoint, same `isScript` pipeline integration, same guard/monitor pattern. Corpus
106+
is NUGET-scale (1.7M pkgs / 4.5M runtime edges), so cost/runtime are expected to track NUGET, not
107+
GO. This ADR's title and body still say "GO/NUGET" for the original decision; treat this addendum
108+
as extending scope to a third ecosystem rather than superseding the analysis above.
109+
100110
Related: ADR-0003 (deps BQ table selection — same root cause: GO/NUGET absent from the resolved-graph tables).

pnpm-lock.yaml

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

services/apps/packages_worker/src/deps-dev/README.md

Lines changed: 20 additions & 19 deletions
Large diffs are not rendered by default.

services/apps/packages_worker/src/deps-dev/activities/checkDependentCountsGuard.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,33 @@ import { getPackagesDb } from '../../db'
77
export interface CheckDependentCountsGuardInput {
88
currentRowCount: number
99
snapshotDate: string
10-
// Which dependent-counts job kind to baseline against. The three ways each guard against their
11-
// own history: 'dependent_counts' (edges) | 'dependent_counts_go' | 'dependent_counts_nuget'.
10+
// Which dependent-counts job kind to baseline against. The four ways each guard against their
11+
// own history: 'dependent_counts' (edges) | 'dependent_counts_go' | 'dependent_counts_nuget'
12+
// | 'dependent_counts_rubygems'.
1213
jobKind?: OsspckgsJobKind
1314
}
1415

16+
// Source table to point operators at in the Slack alert, keyed by job kind — the edge variant reads
17+
// deps.dev's `Dependents` reverse index, while GO/NUGET/RUBYGEMS invert their own manifest table
18+
// (they're absent from `Dependents`, see ADR-0004). A hardcoded "check Dependents" message would send
19+
// on-call to the wrong table for those three.
20+
const GUARD_SOURCE_TABLE: Partial<Record<OsspckgsJobKind, string>> = {
21+
dependent_counts: 'Dependents',
22+
dependent_counts_go: 'GoRequirementsLatest',
23+
dependent_counts_nuget: 'NuGetRequirementsLatest',
24+
dependent_counts_rubygems: 'RubyGemsRequirementsLatest',
25+
}
26+
27+
// `Dependents` is snapshotted per-date, so "for this snapshot date" is actionable there. The
28+
// GO/NUGET/RubyGems `*RequirementsLatest` views have no snapshot history (see bootstrapOsspckgs.ts,
29+
// RETENTION_DAYS_BY_KIND) — telling an operator to check "this snapshot date" on those is misleading.
30+
const GUARD_HAS_SNAPSHOT_HISTORY: Partial<Record<OsspckgsJobKind, boolean>> = {
31+
dependent_counts: true,
32+
dependent_counts_go: false,
33+
dependent_counts_nuget: false,
34+
dependent_counts_rubygems: false,
35+
}
36+
1537
export interface CheckDependentCountsGuardOutput {
1638
ok: boolean
1739
prevRowCount: number | null
@@ -49,7 +71,7 @@ export async function checkDependentCountsGuard(
4971
},
5072
{
5173
title: 'Action',
52-
text: 'Ingest aborted — existing `dependent_count` values preserved. Check deps.dev `Dependents` table for this snapshot date.',
74+
text: `Ingest aborted — existing \`dependent_count\` values preserved. Check deps.dev \`${GUARD_SOURCE_TABLE[jobKind] ?? 'Dependents'}\` table${(GUARD_HAS_SNAPSHOT_HISTORY[jobKind] ?? true) ? ' for this snapshot date' : ''}.`,
5375
},
5476
],
5577
)

services/apps/packages_worker/src/deps-dev/queries/dependentCountsSql.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,12 @@ GROUP BY pm.purl
4747
`
4848
}
4949

50-
// GO/NUGET reverse-dependent counts. deps.dev publishes NO reverse-dependent or transitive graph for
51-
// these two ecosystems (absent from Dependents/Dependencies/DependencyGraphEdges — see ADR-0004), so
52-
// we compute the EXACT reverse transitive closure ourselves from the direct manifests it DOES publish
53-
// (GoRequirementsLatest / NuGetRequirementsLatest). The "Latest" tables hold the highest release per
54-
// package, matching the DependentIsHighestReleaseWithResolution semantics of the edge query.
50+
// GO/NUGET/RUBYGEMS reverse-dependent counts. deps.dev publishes NO reverse-dependent or transitive
51+
// graph for these three ecosystems (absent from Dependents/Dependencies/DependencyGraphEdges — see
52+
// ADR-0004), so we compute the EXACT reverse transitive closure ourselves from the direct manifests
53+
// it DOES publish (GoRequirementsLatest / NuGetRequirementsLatest / RubyGemsRequirementsLatest). The
54+
// "Latest" tables hold the highest release per package, matching the
55+
// DependentIsHighestReleaseWithResolution semantics of the edge query.
5556
//
5657
// Unlike the edge query (a single SELECT), this is a multi-statement BQ SCRIPT: a semi-naive fixpoint
5758
// over session-scoped TEMP tables, run via bqExportToGcs's isScript mode. It ends by creating
@@ -68,7 +69,7 @@ const MAX_CLOSURE_ITERATIONS = 60
6869
// Builds the full closure script for one ecosystem. `edgesSql` must SELECT DISTINCT a `dep` (requirer)
6970
// and `subj` (depended-upon) name column from that ecosystem's manifest table.
7071
function buildClosureScript(
71-
system: 'GO' | 'NUGET',
72+
system: 'GO' | 'NUGET' | 'RUBYGEMS',
7273
edgesSql: string,
7374
snapshotDate: string,
7475
): string {
@@ -194,3 +195,16 @@ UNNEST(grp.Dependencies) AS dp
194195
WHERE dp.Name NOT LIKE '%>%' AND n.Name NOT LIKE '%>%' AND n.Name != dp.Name`
195196
return buildClosureScript('NUGET', edgesSql, snapshotDate)
196197
}
198+
199+
// RubyGems has no reverse-dependent/transitive graph in deps.dev either (Dependents contains only
200+
// {NPM, MAVEN, PYPI, CARGO} — verified via BQ 2026-07-13), so — like GO/NUGET — the exact reverse
201+
// transitive closure is computed from the runtime-dependency manifest (RubyGemsRequirementsLatest),
202+
// matching the RuntimeDependencies-only scope used for the forward package_dependencies ingest.
203+
export function buildRubygemsDependentCountsSql(snapshotDate: string): string {
204+
const edgesSql = `
205+
SELECT DISTINCT r.Name AS dep, d.Name AS subj
206+
FROM \`bigquery-public-data.deps_dev_v1.RubyGemsRequirementsLatest\` r,
207+
UNNEST(r.RuntimeDependencies) AS d
208+
WHERE d.Name NOT LIKE '%>%' AND r.Name NOT LIKE '%>%' AND r.Name != d.Name`
209+
return buildClosureScript('RUBYGEMS', edgesSql, snapshotDate)
210+
}

services/apps/packages_worker/src/deps-dev/workflows/bootstrapOsspckgs.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@ type JobKind =
3535
| 'dependent_counts'
3636
| 'dependent_counts_go'
3737
| 'dependent_counts_nuget'
38+
| 'dependent_counts_rubygems'
3839

3940
// deps.dev retains weekly snapshots for ~3 years; 1095 days (3 years) gives comfortable headroom.
4041
// advisories/advisory_packages use AdvisoriesLatest (no partition history) → effectively unlimited.
41-
// dependent_counts_go/_nuget read *RequirementsLatest (no partition history) → effectively unlimited.
42+
// dependent_counts_go/_nuget/_rubygems read *RequirementsLatest (no partition history) → effectively
43+
// unlimited.
4244
const RETENTION_DAYS_BY_KIND: Record<JobKind, number> = {
4345
packages: 1095,
4446
repos: 1095,
@@ -50,6 +52,7 @@ const RETENTION_DAYS_BY_KIND: Record<JobKind, number> = {
5052
dependent_counts: 1095,
5153
dependent_counts_go: 999_999,
5254
dependent_counts_nuget: 999_999,
55+
dependent_counts_rubygems: 999_999,
5356
}
5457

5558
// Kinds whose incremental diff is driven by a BQ partition snapshot date.
@@ -204,13 +207,14 @@ export async function bootstrapOsspckgs(opts: {
204207
}
205208
}
206209
}
207-
// GO/NUGET reverse-dependent counts: separate kinds, manifest-sourced (GoRequirementsLatest /
208-
// NuGetRequirementsLatest), computed via the exact reverse transitive closure script. The manifests
210+
// GO/NUGET/RUBYGEMS reverse-dependent counts: separate kinds, manifest-sourced (GoRequirementsLatest /
211+
// NuGetRequirementsLatest / RubyGemsRequirementsLatest), computed via the exact reverse transitive
212+
// closure script. The manifests
209213
// are *Latest views (no resolution needed); `today` is the snapshot_at stamp AND the anchor for the
210214
// dependent_repos partition window (latest PackageVersionToProject snapshot within 60 days). Each
211215
// guards against its own history and merges a disjoint purl space, so an edge-snapshot corruption
212216
// that aborts `dependent_counts` never blocks these.
213-
for (const variant of ['go', 'nuget'] as const) {
217+
for (const variant of ['go', 'nuget', 'rubygems'] as const) {
214218
const kind = `dependent_counts_${variant}` as const
215219
if (!runs(kind)) continue
216220
try {

services/apps/packages_worker/src/deps-dev/workflows/ingestDependentCounts.ts

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
buildDependentCountsSql,
88
buildGoDependentCountsSql,
99
buildNugetDependentCountsSql,
10+
buildRubygemsDependentCountsSql,
1011
} from '../queries/dependentCountsSql'
1112

1213
const { bqExportToGcs } = proxyActivities<typeof depsDevActivities>({
@@ -40,18 +41,19 @@ const { setJobStep } = proxyActivities<typeof depsDevActivities>({
4041
retry: { maximumAttempts: 3 },
4142
})
4243

43-
// The dependent-counts ingest runs three independent ways, one per source of reverse-dependent data:
44-
// - 'edges' : NPM/MAVEN/PYPI/CARGO from the deps.dev `Dependents` reverse index (single SELECT)
45-
// - 'go' : GO from the GoRequirementsLatest exact reverse transitive closure (BQ script)
46-
// - 'nuget' : NUGET from the NuGetRequirementsLatest exact reverse transitive closure (BQ script)
47-
// All three produce the same three columns (dependent_count, transitive_dependent_count,
44+
// The dependent-counts ingest runs four independent ways, one per source of reverse-dependent data:
45+
// - 'edges' : NPM/MAVEN/PYPI/CARGO from the deps.dev `Dependents` reverse index (single SELECT)
46+
// - 'go' : GO from the GoRequirementsLatest exact reverse transitive closure (BQ script)
47+
// - 'nuget' : NUGET from the NuGetRequirementsLatest exact reverse transitive closure (BQ script)
48+
// - 'rubygems' : RUBYGEMS from the RubyGemsRequirementsLatest exact reverse transitive closure (BQ script)
49+
// All four produce the same three columns (dependent_count, transitive_dependent_count,
4850
// dependent_repos_count) — deps.dev gives the edge systems their transitive graph directly, while
49-
// GO/NUGET compute it via the semi-naive closure script (isScript, see ADR-0004 + dependentCountsSql).
50-
// Each way is its own job kind, staging table, guard baseline, and purl-disjoint merge — so a corrupt
51-
// edge snapshot aborts only the edge way and the GO/NUGET counts (manifest-sourced, unaffected) still
52-
// update. 'edges' keeps the original `dependent_counts` kind so existing job history / monitor / guard
53-
// baseline stay continuous.
54-
export type DependentCountsVariant = 'edges' | 'go' | 'nuget'
51+
// GO/NUGET/RUBYGEMS compute it via the semi-naive closure script (isScript, see ADR-0004 +
52+
// dependentCountsSql). Each way is its own job kind, staging table, guard baseline, and purl-disjoint
53+
// merge — so a corrupt edge snapshot aborts only the edge way and the manifest-sourced counts
54+
// (unaffected) still update. 'edges' keeps the original `dependent_counts` kind so existing job
55+
// history / monitor / guard baseline stay continuous.
56+
export type DependentCountsVariant = 'edges' | 'go' | 'nuget' | 'rubygems'
5557

5658
interface VariantConfig {
5759
jobKind: OsspckgsJobKind
@@ -61,10 +63,11 @@ interface VariantConfig {
6163
pgColumns: string[]
6264
maxBytesGb: number
6365
buildSql: (snapshotDate: string) => string
64-
// When true, buildSql returns a multi-statement BQ script (the GO/NUGET exact reverse transitive
65-
// closure) that ends by creating TEMP TABLE _export_data, rather than a single SELECT. The export
66-
// activity then appends only EXPORT DATA and enforces the byte ceiling via maximumBytesBilled
67-
// instead of a dry-run. See ADR-0004. Unset (single-SELECT) for the edges variant; set for GO/NUGET.
66+
// When true, buildSql returns a multi-statement BQ script (the GO/NUGET/RUBYGEMS exact reverse
67+
// transitive closure) that ends by creating TEMP TABLE _export_data, rather than a single SELECT.
68+
// The export activity then appends only EXPORT DATA and enforces the byte ceiling via
69+
// maximumBytesBilled instead of a dry-run. See ADR-0004. Unset (single-SELECT) for the edges
70+
// variant; set for GO/NUGET/RUBYGEMS.
6871
isScript?: boolean
6972
}
7073

@@ -73,8 +76,9 @@ interface VariantConfig {
7376
const EDGES_STAGING = 'staging.osspckgs_dependent_counts_raw'
7477
const GO_STAGING = 'staging.osspckgs_dependent_counts_go_raw'
7578
const NUGET_STAGING = 'staging.osspckgs_dependent_counts_nuget_raw'
79+
const RUBYGEMS_STAGING = 'staging.osspckgs_dependent_counts_rubygems_raw'
7680

77-
// All three ways now produce the same shape: purl + the three count columns. Each variant has its own
81+
// All four ways now produce the same shape: purl + the three count columns. Each variant has its own
7882
// staging table (parallel-safe) but identical DDL/merge; the merges touch disjoint purl spaces (one
7983
// per ecosystem), so they never collide.
8084
const PG_COLUMNS = [
@@ -116,11 +120,11 @@ const VARIANTS: Record<DependentCountsVariant, VariantConfig> = {
116120
maxBytesGb: 2000,
117121
buildSql: (snapshotDate) => buildDependentCountsSql(snapshotDate),
118122
},
119-
// GO/NUGET run the exact reverse transitive closure script (isScript). maxBytesGb is the
120-
// server-side maximumBytesBilled runaway cap, set well above the validated full-pipeline spend
121-
// (GO 2.31 TB incl. the all-depth repos aggregation; NUGET ~32 GB) so a normal week never trips
122-
// it — the iteration cap inside the script is the deterministic guard. Env-overridable via
123-
// BQ_DATASET_INGEST_DEPENDENT_COUNTS_GO_MAX_BQ_GB / _NUGET_.
123+
// GO/NUGET/RUBYGEMS run the exact reverse transitive closure script (isScript). maxBytesGb is
124+
// the server-side maximumBytesBilled runaway cap, set well above the validated full-pipeline
125+
// spend (GO 2.31 TB incl. the all-depth repos aggregation; NUGET ~32 GB) so a normal week never
126+
// trips it — the iteration cap inside the script is the deterministic guard. Env-overridable via
127+
// BQ_DATASET_INGEST_DEPENDENT_COUNTS_GO_MAX_BQ_GB / _NUGET_ / _RUBYGEMS_.
124128
go: {
125129
jobKind: 'dependent_counts_go',
126130
stagingTable: GO_STAGING,
@@ -141,6 +145,18 @@ const VARIANTS: Record<DependentCountsVariant, VariantConfig> = {
141145
buildSql: (snapshotDate) => buildNugetDependentCountsSql(snapshotDate),
142146
isScript: true,
143147
},
148+
// RubyGems: same shape as NUGET (manifest-sourced closure), similarly small corpus
149+
// (~1.7M pkgs / ~4.5M runtime edges — verified via BQ 2026-07-13).
150+
rubygems: {
151+
jobKind: 'dependent_counts_rubygems',
152+
stagingTable: RUBYGEMS_STAGING,
153+
stagingDdl: stagingDdl(RUBYGEMS_STAGING),
154+
mergeSql: dependentCountsMerge(RUBYGEMS_STAGING),
155+
pgColumns: PG_COLUMNS,
156+
maxBytesGb: 200,
157+
buildSql: (snapshotDate) => buildRubygemsDependentCountsSql(snapshotDate),
158+
isScript: true,
159+
},
144160
}
145161

146162
const ROWS_PER_CHUNK = 1_000_000

services/apps/packages_worker/src/osv/__tests__/versionCompare.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,56 @@ describe('compareVersion — nuget (semver)', () => {
111111
})
112112
})
113113

114+
describe('compareVersion — go (semver)', () => {
115+
it.each([
116+
['v1.2.3', 'v1.2.4', -1],
117+
['v1.2.4', 'v1.2.3', 1],
118+
['v1.2.3', 'v1.2.3', 0],
119+
['v1.10.0', 'v1.9.0', 1], // numeric, not lex
120+
['v1.0.0-alpha', 'v1.0.0', -1], // prerelease < release
121+
['v0.0.0-20220314234659-1baeb1ce4c0b', 'v0.0.0-20220315000000-2caec2d5d1c1', -1], // pseudo-versions
122+
])('compareVersion("go", %s, %s) sign = %s', (a, b, expected) => {
123+
expect(sign(compareVersion('go', a, b))).toBe(expected)
124+
})
125+
126+
it('returns null for unparseable go versions', () => {
127+
expect(compareVersion('go', 'not-a-version', 'v1.0.0')).toBeNull()
128+
})
129+
130+
it('rejects titlecase "Go" — production storage is always lowercase', () => {
131+
expect(compareVersion('Go', 'v1.0.0', 'v2.0.0')).toBeNull()
132+
})
133+
})
134+
135+
describe('compareVersion — rubygems (Gem::Version-style)', () => {
136+
it.each([
137+
['1.0.0', '1.0.1', -1],
138+
['1.0.1', '1.0.0', 1],
139+
['1.0.0', '1.0.0', 0],
140+
['1.10.0', '1.9.0', 1], // numeric, not lex
141+
['1.0', '1.0.0', 0], // missing trailing segment pads as 0
142+
['1.0.pre', '1.0', -1], // prerelease sorts below the corresponding release
143+
['1.0.0.rc1', '1.0.0.rc2', -1],
144+
// rack CVE-2022-30123 boundary
145+
['2.2.3', '2.2.3.1', -1],
146+
['1.0-1', '1.0.pre.1', 0], // hyphen is a prerelease marker, same as ".pre."
147+
['1.0.a10', '1.0.a9', 1], // letter+digit runs split: a10 -> a.10, a9 -> a.9
148+
['1.99999999999999999999999999999999', '1.99999999999999999999999999999998', 1], // beyond Number.MAX_SAFE_INTEGER — must not collapse to equal
149+
])('compareVersion("rubygems", %s, %s) sign = %s', (a, b, expected) => {
150+
expect(sign(compareVersion('rubygems', a, b))).toBe(expected)
151+
})
152+
153+
it('returns null for unparseable rubygems versions', () => {
154+
expect(compareVersion('rubygems', '', '1.0.0')).toBeNull()
155+
expect(compareVersion('rubygems', '---', '1.0.0')).toBeNull()
156+
expect(compareVersion('rubygems', '1..2', '1.0.0')).toBeNull()
157+
})
158+
159+
it('rejects titlecase "RubyGems" — production storage is always lowercase', () => {
160+
expect(compareVersion('RubyGems', '1.0.0', '2.0.0')).toBeNull()
161+
})
162+
})
163+
114164
describe('compareVersion — unsupported ecosystems', () => {
115165
it('returns null for ecosystems we have no comparator for', () => {
116166
expect(compareVersion('PyPI', '1.0.0', '2.0.0')).toBeNull()

0 commit comments

Comments
 (0)