Skip to content

Commit 73cd474

Browse files
rainxchzedoz-agent
andcommitted
feat: add pushedAt field (R5/R13) — pipe GitHub pushed_at through all layers
- V18 migration: add pushed_at_gh TIMESTAMPTZ column to repos - Expose pushedAt in RepoResponse (distinct from updatedAt/metadata change) - Persist in GitHubSearchClient ingest, upsertMetadataOnly, and Meili sync - Map in RepoRepository, SearchRepository, MeiliRepoHit, all route mappers - Add POST /internal/backfill-pushed-at to fill NULL rows on existing data Co-Authored-By: Oz <oz-agent@warp.dev>
1 parent 7e7ce72 commit 73cd474

11 files changed

Lines changed: 79 additions & 2 deletions

File tree

src/main/kotlin/zed/rainxch/githubstore/db/DatabaseFactory.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ object DatabaseFactory {
9696
"V15__license_info.sql",
9797
"V16__oauth_ephemeral.sql",
9898
"V17__signing_fingerprint_host.sql",
99+
"V18__pushed_at.sql",
99100
)
100101
for (migration in migrations) {
101102
val rawSql = this::class.java.classLoader

src/main/kotlin/zed/rainxch/githubstore/db/MeilisearchClient.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ data class MeiliRepoHit(
152152
val has_installers_linux: Boolean = false,
153153
val trending_score: Double? = null,
154154
val popularity_score: Double? = null,
155+
// R5/R13: last commit timestamp; piped from GitHub pushed_at.
156+
val pushed_at: String? = null,
155157
// Must be populated on every addDocuments() call — Meili's POST /documents
156158
// *replaces* the doc, so omitting this field wipes the SignalAggregationWorker's
157159
// most recent score. Null here is "no signal yet," not "no longer ranked."

src/main/kotlin/zed/rainxch/githubstore/db/RepoRepository.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ class RepoRepository {
9595
releasesUrl = "${this[Repos.htmlUrl]}/releases",
9696
updatedAt = this[Repos.updatedAtGh]?.toString(),
9797
createdAt = this[Repos.createdAtGh]?.toString(),
98+
pushedAt = this[Repos.pushedAtGh]?.toString(),
9899
latestReleaseDate = releaseDateStr,
99100
latestReleaseTag = this[Repos.latestReleaseTag],
100101
releaseRecency = recencyDays,

src/main/kotlin/zed/rainxch/githubstore/db/SearchRepository.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class SearchRepository {
5555
has_installers_android, has_installers_windows,
5656
has_installers_macos, has_installers_linux,
5757
trending_score, popularity_score, search_score,
58-
updated_at_gh, created_at_gh
58+
updated_at_gh, created_at_gh, pushed_at_gh
5959
FROM repos
6060
""".trimIndent()
6161
)
@@ -115,6 +115,7 @@ class SearchRepository {
115115
releasesUrl = "${rs.getString("html_url")}/releases",
116116
updatedAt = rs.getString("updated_at_gh"),
117117
createdAt = rs.getString("created_at_gh"),
118+
pushedAt = rs.getString("pushed_at_gh"),
118119
latestReleaseDate = releaseDateStr,
119120
latestReleaseTag = rs.getString("latest_release_tag"),
120121
releaseRecency = recencyDays,

src/main/kotlin/zed/rainxch/githubstore/db/Tables.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ object Repos : Table("repos") {
3636
val searchScore = float("search_score").nullable()
3737
val createdAtGh = timestampWithTimeZone("created_at_gh").nullable()
3838
val updatedAtGh = timestampWithTimeZone("updated_at_gh").nullable()
39+
// R5/R13: last default-branch commit (GitHub pushed_at), distinct from
40+
// updatedAtGh (last metadata change). Used by client Heartbeat animation.
41+
val pushedAtGh = timestampWithTimeZone("pushed_at_gh").nullable()
3942
val indexedAt = timestampWithTimeZone("indexed_at")
4043

4144
override val primaryKey = PrimaryKey(id)

src/main/kotlin/zed/rainxch/githubstore/ingest/GitHubSearchClient.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,9 @@ class GitHubSearchClient(
539539
it[hasInstallersLinux] = platforms["linux"] ?: false
540540
it[downloadCount] = r.downloadCount
541541
it[searchScore] = scoreToWrite
542+
it[pushedAtGh] = repo.pushedAt?.let {
543+
try { OffsetDateTime.parse(it) } catch (_: Exception) { null }
544+
}
542545
it[indexedAt] = OffsetDateTime.now()
543546
}
544547
scoredByRepoId[repo.id] = scoreToWrite.toDouble()
@@ -576,6 +579,7 @@ class GitHubSearchClient(
576579
has_installers_windows = r.platformFlags["windows"] ?: false,
577580
has_installers_macos = r.platformFlags["macos"] ?: false,
578581
has_installers_linux = r.platformFlags["linux"] ?: false,
582+
pushed_at = r.repo.pushedAt,
579583
// Meili's POST /documents replaces the whole doc. Omitting this
580584
// would wipe the SignalAggregationWorker's most recent score
581585
// on every passthrough/refresh until the next hourly cycle.
@@ -622,6 +626,7 @@ class GitHubSearchClient(
622626
releasesUrl = "${repo.htmlUrl}/releases",
623627
updatedAt = repo.updatedAt,
624628
createdAt = repo.createdAt,
629+
pushedAt = repo.pushedAt,
625630
latestReleaseDate = releaseDateStr,
626631
latestReleaseTag = release.tagName,
627632
releaseRecency = recencyDays,
@@ -682,6 +687,8 @@ data class GitHubRepo(
682687
val disabled: Boolean = false,
683688
@SerialName("updated_at") val updatedAt: String? = null,
684689
@SerialName("created_at") val createdAt: String? = null,
690+
// R5/R13: last default-branch commit, distinct from updated_at (metadata change).
691+
@SerialName("pushed_at") val pushedAt: String? = null,
685692
)
686693

687694
@Serializable

src/main/kotlin/zed/rainxch/githubstore/model/RepoResponse.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ data class RepoResponse(
4949
val releasesUrl: String?,
5050
val updatedAt: String?,
5151
val createdAt: String?,
52+
// R5/R13: last commit timestamp (GitHub pushed_at = default-branch HEAD).
53+
// Distinct from updatedAt (last metadata change). Null for Meili-served
54+
// search results until meili_sync.py backfills the field.
55+
val pushedAt: String? = null,
5256
val latestReleaseDate: String? = null,
5357
val latestReleaseTag: String? = null,
5458
val releaseRecency: Int? = null,

src/main/kotlin/zed/rainxch/githubstore/routes/InternalRoutes.kt

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import kotlinx.coroutines.launch
1616
import kotlinx.serialization.Serializable
1717
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
1818
import org.jetbrains.exposed.sql.SqlExpressionBuilder.isNull
19+
import java.time.OffsetDateTime
1920
import org.jetbrains.exposed.sql.selectAll
2021
import org.jetbrains.exposed.sql.transactions.TransactionManager
2122
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
@@ -160,6 +161,54 @@ fun Route.internalRoutes(
160161
)
161162
}
162163

164+
// One-shot backfill for pushed_at_gh (V18). Iterates every repo
165+
// where pushed_at_gh IS NULL, re-fetches from GitHub, and writes
166+
// the field. Self-terminates once all rows are filled. Shares the
167+
// same backfillRunning gate as /backfill-stale so the two never
168+
// run concurrently and don't race the rotation pool.
169+
post("/backfill-pushed-at") {
170+
if (!authorized(call, adminToken)) {
171+
return@post respondNotFound(call)
172+
}
173+
val limit = call.request.queryParameters["limit"]
174+
?.toIntOrNull()
175+
?.coerceIn(1, 10_000)
176+
?: 10_000
177+
if (!backfillRunning.compareAndSet(false, true)) {
178+
call.response.header(HttpHeaders.RetryAfter, "60")
179+
return@post call.respond(
180+
HttpStatusCode.Conflict,
181+
BackfillResponse(scheduled = 0, started = false, message = "backfill_already_running"),
182+
)
183+
}
184+
val candidates = transaction {
185+
Repos.selectAll()
186+
.where { Repos.pushedAtGh.isNull() }
187+
.orderBy(Repos.id)
188+
.limit(limit)
189+
.map { it[Repos.id] to it[Repos.fullName] }
190+
}
191+
if (candidates.isEmpty()) {
192+
backfillRunning.set(false)
193+
return@post call.respond(
194+
HttpStatusCode.OK,
195+
BackfillResponse(scheduled = 0, started = false, message = "no rows missing pushed_at"),
196+
)
197+
}
198+
backfillScope.launch {
199+
try {
200+
runBackfill(searchClient, candidates)
201+
} finally {
202+
backfillRunning.set(false)
203+
}
204+
}
205+
call.response.header(HttpHeaders.CacheControl, "no-store")
206+
call.respond(
207+
HttpStatusCode.Accepted,
208+
BackfillResponse(scheduled = candidates.size, started = true),
209+
)
210+
}
211+
163212
// Browser dashboard. Basic Auth required in prod so the browser prompts
164213
// for credentials on first visit; optional in dev for local inspection.
165214
authenticate(ADMIN_BASIC_AUTH, optional = adminToken == null) {
@@ -259,7 +308,10 @@ private fun upsertMetadataOnly(repo: GitHubRepo) {
259308
it[licenseSpdxId] = repo.license?.spdxId
260309
it[licenseName] = repo.license?.name
261310
it[description] = repo.description
262-
it[indexedAt] = java.time.OffsetDateTime.now()
311+
it[pushedAtGh] = repo.pushedAt?.let { raw ->
312+
try { OffsetDateTime.parse(raw) } catch (_: Exception) { null }
313+
}
314+
it[indexedAt] = OffsetDateTime.now()
263315
}
264316
}
265317
}

src/main/kotlin/zed/rainxch/githubstore/routes/RepoRoutes.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,5 @@ internal fun GitHubRepo.toMetadataOnlyResponse(): RepoResponse = RepoResponse(
143143
releasesUrl = "$htmlUrl/releases",
144144
updatedAt = updatedAt,
145145
createdAt = createdAt,
146+
pushedAt = pushedAt,
146147
)

src/main/kotlin/zed/rainxch/githubstore/routes/SearchRoutes.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ private fun zed.rainxch.githubstore.db.MeiliRepoHit.toRepoResponse() = RepoRespo
292292
releasesUrl = "$html_url/releases",
293293
updatedAt = null,
294294
createdAt = null,
295+
pushedAt = pushed_at,
295296
latestReleaseDate = latest_release_date,
296297
latestReleaseTag = latest_release_tag,
297298
downloadCount = download_count,

0 commit comments

Comments
 (0)