|
| 1 | +import { Context } from '@temporalio/activity' |
| 2 | + |
| 3 | +import { logAuditFieldChanges } from '@crowd/data-access-layer/src/packages' |
| 4 | +import type { QueryExecutor } from '@crowd/data-access-layer/src/queryExecutor' |
| 5 | +import { getServiceChildLogger } from '@crowd/logging' |
| 6 | + |
| 7 | +import { getGoConfig } from '../config' |
| 8 | +import { getPackagesDb } from '../db' |
| 9 | + |
| 10 | +import { fetchStatus } from './pkgGoDevClient' |
| 11 | +import { fetchLatest } from './proxyClient' |
| 12 | +import { isFetchError } from './types' |
| 13 | + |
| 14 | +const log = getServiceChildLogger('go') |
| 15 | + |
| 16 | +const PROXY_SOURCE = 'go-proxy' |
| 17 | +const PKGGODEV_SOURCE = 'pkg-go-dev' |
| 18 | + |
| 19 | +// TODO: filter to critical packages once computed |
| 20 | +async function getGoBatch( |
| 21 | + qx: QueryExecutor, |
| 22 | + afterPurl: string, |
| 23 | + batchSize: number, |
| 24 | +): Promise<Array<{ purl: string; name: string }>> { |
| 25 | + return qx.select( |
| 26 | + `SELECT purl, name FROM packages |
| 27 | + WHERE ecosystem = 'go' AND purl > $(after) |
| 28 | + ORDER BY last_synced_at ASC NULLS FIRST, purl ASC |
| 29 | + LIMIT $(limit)`, |
| 30 | + { after: afterPurl, limit: batchSize }, |
| 31 | + ) |
| 32 | +} |
| 33 | + |
| 34 | +export async function enrichGoVersionsBatch( |
| 35 | + afterPurl: string, |
| 36 | + batchSize: number, |
| 37 | +): Promise<string | null> { |
| 38 | + const qx = await getPackagesDb() |
| 39 | + const rows = await getGoBatch(qx, afterPurl, batchSize) |
| 40 | + if (rows.length === 0) return null |
| 41 | + |
| 42 | + const { fetchTimeoutMs, proxyConcurrency } = getGoConfig() |
| 43 | + |
| 44 | + const enrichOne = async (row: { purl: string; name: string }): Promise<void> => { |
| 45 | + Context.current().heartbeat(row.purl) |
| 46 | + const result = await fetchLatest(row.name, fetchTimeoutMs) |
| 47 | + if (isFetchError(result)) { |
| 48 | + log.warn( |
| 49 | + { purl: row.purl, name: row.name, kind: result.kind, statusCode: result.statusCode }, |
| 50 | + 'go proxy fetch failed — skipping package', |
| 51 | + ) |
| 52 | + return |
| 53 | + } |
| 54 | + const changed = await qx.selectOne( |
| 55 | + `WITH old AS ( |
| 56 | + SELECT latest_version AS v, latest_release_at AS t, repository_url AS r |
| 57 | + FROM packages WHERE purl = $(purl) |
| 58 | + ), |
| 59 | + upd AS ( |
| 60 | + UPDATE packages p SET |
| 61 | + latest_version = $(version), |
| 62 | + latest_release_at = $(releaseAt), |
| 63 | + repository_url = COALESCE($(repoUrl), p.repository_url), |
| 64 | + last_synced_at = NOW() |
| 65 | + WHERE p.purl = $(purl) |
| 66 | + RETURNING latest_version AS v, latest_release_at AS t, repository_url AS r |
| 67 | + ) |
| 68 | + SELECT |
| 69 | + (SELECT v FROM old) IS DISTINCT FROM (SELECT v FROM upd) AS v_changed, |
| 70 | + (SELECT t FROM old) IS DISTINCT FROM (SELECT t FROM upd) AS t_changed, |
| 71 | + (SELECT r FROM old) IS DISTINCT FROM (SELECT r FROM upd) AS r_changed`, |
| 72 | + { |
| 73 | + version: result.version, |
| 74 | + releaseAt: result.releaseAt, |
| 75 | + repoUrl: result.repoUrl, |
| 76 | + purl: row.purl, |
| 77 | + }, |
| 78 | + ) |
| 79 | + const changedFields = [ |
| 80 | + changed?.v_changed ? 'packages.latest_version' : null, |
| 81 | + changed?.t_changed ? 'packages.latest_release_at' : null, |
| 82 | + changed?.r_changed ? 'packages.repository_url' : null, |
| 83 | + ].filter(Boolean) as string[] |
| 84 | + await logAuditFieldChanges(qx, PROXY_SOURCE, row.purl, changedFields) |
| 85 | + } |
| 86 | + |
| 87 | + for (let i = 0; i < rows.length; i += proxyConcurrency) { |
| 88 | + await Promise.all(rows.slice(i, i + proxyConcurrency).map(enrichOne)) |
| 89 | + } |
| 90 | + |
| 91 | + log.info({ count: rows.length, concurrency: proxyConcurrency }, 'Enriched go versions batch') |
| 92 | + return rows[rows.length - 1].purl |
| 93 | +} |
| 94 | + |
| 95 | +export async function enrichGoStatusBatch( |
| 96 | + afterPurl: string, |
| 97 | + batchSize: number, |
| 98 | +): Promise<string | null> { |
| 99 | + const qx = await getPackagesDb() |
| 100 | + const rows = await getGoBatch(qx, afterPurl, batchSize) |
| 101 | + if (rows.length === 0) return null |
| 102 | + |
| 103 | + const { fetchTimeoutMs } = getGoConfig() |
| 104 | + for (const row of rows) { |
| 105 | + const result = await fetchStatus(row.name, fetchTimeoutMs, () => |
| 106 | + Context.current().heartbeat(row.purl), |
| 107 | + ) |
| 108 | + if (isFetchError(result)) { |
| 109 | + log.warn( |
| 110 | + { purl: row.purl, name: row.name, kind: result.kind, statusCode: result.statusCode }, |
| 111 | + 'pkg.go.dev fetch failed — skipping package', |
| 112 | + ) |
| 113 | + continue |
| 114 | + } |
| 115 | + const changed = await qx.selectOne( |
| 116 | + `WITH old AS ( |
| 117 | + SELECT status AS s, versions_count AS vc FROM packages WHERE purl = $(purl) |
| 118 | + ), |
| 119 | + upd AS ( |
| 120 | + UPDATE packages p SET |
| 121 | + status = $(status), |
| 122 | + versions_count = COALESCE($(versionsCount), p.versions_count), |
| 123 | + last_synced_at = NOW() |
| 124 | + WHERE p.purl = $(purl) |
| 125 | + RETURNING status AS s, versions_count AS vc |
| 126 | + ) |
| 127 | + SELECT |
| 128 | + (SELECT s FROM old) IS DISTINCT FROM (SELECT s FROM upd) AS s_changed, |
| 129 | + (SELECT vc FROM old) IS DISTINCT FROM (SELECT vc FROM upd) AS vc_changed`, |
| 130 | + { status: result.status, versionsCount: result.versionsCount, purl: row.purl }, |
| 131 | + ) |
| 132 | + const changedFields = [ |
| 133 | + changed?.s_changed ? 'packages.status' : null, |
| 134 | + changed?.vc_changed ? 'packages.versions_count' : null, |
| 135 | + ].filter(Boolean) as string[] |
| 136 | + await logAuditFieldChanges(qx, PKGGODEV_SOURCE, row.purl, changedFields) |
| 137 | + } |
| 138 | + |
| 139 | + log.info({ count: rows.length }, 'Enriched go status batch') |
| 140 | + return rows[rows.length - 1].purl |
| 141 | +} |
0 commit comments