From 46e956a779364d9a8f1fbda90faee13bac0d9eef Mon Sep 17 00:00:00 2001 From: Thomas Durieux <5577568+tdurieux@users.noreply.github.com> Date: Thu, 20 Aug 2026 00:29:52 -1000 Subject: [PATCH] fix: reactivate repositories with future expiration (#781) --- src/server/routes/repository-private.ts | 53 ++++++++++++++++++++++--- test/backend-reliability.test.js | 42 ++++++++++++++++++++ 2 files changed, 89 insertions(+), 6 deletions(-) diff --git a/src/server/routes/repository-private.ts b/src/server/routes/repository-private.ts index aef0d50..7640fe2 100644 --- a/src/server/routes/repository-private.ts +++ b/src/server/routes/repository-private.ts @@ -190,15 +190,32 @@ router.post( const newExpiration = extendExpirationDate( repo.model.options.expirationDate ); + const reactivating = repo.status === RepositoryStatus.EXPIRED; + const updates: Record = { + "options.expirationDate": newExpiration, + }; repo.model.options.expirationDate = newExpiration; + if (reactivating) { + repo.model.anonymizeDate = new Date(); + updates.anonymizeDate = repo.model.anonymizeDate; + } await AnonymizedRepositoryModel.updateOne( { _id: repo.model._id }, - { $set: { "options.expirationDate": newExpiration } } + { $set: updates } ).exec(); - // Re-anonymize so an expired repository comes back online, mirroring the - // refresh flow. - await repo.updateIfNeeded({ force: true }); + if (reactivating) { + // Expiration removes the cached files. Rebuild the saved commit + // directly instead of asking GitHub for the latest branch head first; + // that lookup can fail after the new date has already been persisted + // and leave the repository stuck in the expired state. + await repo.updateStatus(RepositoryStatus.PREPARING); + await downloadQueue.add( + repo.repoId, + { repoId: repo.repoId }, + { jobId: `repo-${repo.repoId}`, attempts: 3 } + ); + } res.json({ status: repo.status, expirationDate: newExpiration }); } catch (error) { handleError(error, res, req); @@ -424,9 +441,9 @@ function updateRepoModel( }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any export function hasRepositorySourceChanged( model: IAnonymizedRepositoryDocument, + // eslint-disable-next-line @typescript-eslint/no-explicit-any repoUpdate: any ): boolean { return ( @@ -436,6 +453,25 @@ export function hasRepositorySourceChanged( ); } +/** + * An expired repository has had its cached files removed, so saving a valid + * future expiration must rebuild it even when its GitHub source is unchanged. + */ +export function shouldReactivateExpiredRepository( + model: IAnonymizedRepositoryDocument, + now = new Date() +): boolean { + if (model.status !== RepositoryStatus.EXPIRED) return false; + if (model.options.expirationMode === "never") return true; + + const expirationDate = model.options.expirationDate; + return ( + !!expirationDate && + !isNaN(expirationDate.getTime()) && + expirationDate > now + ); +} + // update a repository router.post( "/:repoId/", @@ -461,6 +497,11 @@ router.post( const sourceChanged = hasRepositorySourceChanged(repo.model, repoUpdate); updateRepoModel(repo.model, repoUpdate); + const reactivating = shouldReactivateExpiredRepository(repo.model); + + if (reactivating) { + repo.model.anonymizeDate = new Date(); + } if (sourceChanged) { const parsedRepository = gh(repoUpdate.fullName); @@ -553,7 +594,7 @@ router.post( }, } ).exec(); - if (!sourceChanged) { + if (!sourceChanged && !reactivating) { return res.json({ status: repo.status }); } diff --git a/test/backend-reliability.test.js b/test/backend-reliability.test.js index 977f75b..eb26d09 100644 --- a/test/backend-reliability.test.js +++ b/test/backend-reliability.test.js @@ -8,6 +8,7 @@ const { } = require("../src/server/routes/conference"); const { hasRepositorySourceChanged, + shouldReactivateExpiredRepository, } = require("../src/server/routes/repository-private"); const { processRemoveRepository, @@ -79,6 +80,47 @@ describe("repository update source detection", function () { }) ).to.equal(true); }); + + it("rebuilds an expired repository when its expiration is in the future", function () { + const now = new Date("2026-08-20T00:00:00.000Z"); + expect( + shouldReactivateExpiredRepository( + { + status: "expired", + options: { + expirationMode: "redirect", + expirationDate: new Date("2027-05-01T03:57:53.395Z"), + }, + }, + now + ) + ).to.equal(true); + }); + + it("does not rebuild an expired repository with a stale expiration", function () { + const now = new Date("2026-08-20T00:00:00.000Z"); + expect( + shouldReactivateExpiredRepository( + { + status: "expired", + options: { + expirationMode: "redirect", + expirationDate: new Date("2026-01-01T00:00:00.000Z"), + }, + }, + now + ) + ).to.equal(false); + }); + + it("rebuilds an expired repository configured never to expire", function () { + expect( + shouldReactivateExpiredRepository({ + status: "expired", + options: { expirationMode: "never" }, + }) + ).to.equal(true); + }); }); describe("removal workers", function () {