fix: reactivate repositories with future expiration (#781)

This commit is contained in:
Thomas Durieux
2026-08-20 12:29:52 +02:00
committed by GitHub
parent 7aa9557905
commit 46e956a779
2 changed files with 89 additions and 6 deletions
+47 -6
View File
@@ -190,15 +190,32 @@ router.post(
const newExpiration = extendExpirationDate( const newExpiration = extendExpirationDate(
repo.model.options.expirationDate repo.model.options.expirationDate
); );
const reactivating = repo.status === RepositoryStatus.EXPIRED;
const updates: Record<string, Date> = {
"options.expirationDate": newExpiration,
};
repo.model.options.expirationDate = newExpiration; repo.model.options.expirationDate = newExpiration;
if (reactivating) {
repo.model.anonymizeDate = new Date();
updates.anonymizeDate = repo.model.anonymizeDate;
}
await AnonymizedRepositoryModel.updateOne( await AnonymizedRepositoryModel.updateOne(
{ _id: repo.model._id }, { _id: repo.model._id },
{ $set: { "options.expirationDate": newExpiration } } { $set: updates }
).exec(); ).exec();
// Re-anonymize so an expired repository comes back online, mirroring the if (reactivating) {
// refresh flow. // Expiration removes the cached files. Rebuild the saved commit
await repo.updateIfNeeded({ force: true }); // 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 }); res.json({ status: repo.status, expirationDate: newExpiration });
} catch (error) { } catch (error) {
handleError(error, res, req); handleError(error, res, req);
@@ -424,9 +441,9 @@ function updateRepoModel(
}; };
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function hasRepositorySourceChanged( export function hasRepositorySourceChanged(
model: IAnonymizedRepositoryDocument, model: IAnonymizedRepositoryDocument,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
repoUpdate: any repoUpdate: any
): boolean { ): boolean {
return ( 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 // update a repository
router.post( router.post(
"/:repoId/", "/:repoId/",
@@ -461,6 +497,11 @@ router.post(
const sourceChanged = hasRepositorySourceChanged(repo.model, repoUpdate); const sourceChanged = hasRepositorySourceChanged(repo.model, repoUpdate);
updateRepoModel(repo.model, repoUpdate); updateRepoModel(repo.model, repoUpdate);
const reactivating = shouldReactivateExpiredRepository(repo.model);
if (reactivating) {
repo.model.anonymizeDate = new Date();
}
if (sourceChanged) { if (sourceChanged) {
const parsedRepository = gh(repoUpdate.fullName); const parsedRepository = gh(repoUpdate.fullName);
@@ -553,7 +594,7 @@ router.post(
}, },
} }
).exec(); ).exec();
if (!sourceChanged) { if (!sourceChanged && !reactivating) {
return res.json({ status: repo.status }); return res.json({ status: repo.status });
} }
+42
View File
@@ -8,6 +8,7 @@ const {
} = require("../src/server/routes/conference"); } = require("../src/server/routes/conference");
const { const {
hasRepositorySourceChanged, hasRepositorySourceChanged,
shouldReactivateExpiredRepository,
} = require("../src/server/routes/repository-private"); } = require("../src/server/routes/repository-private");
const { const {
processRemoveRepository, processRemoveRepository,
@@ -79,6 +80,47 @@ describe("repository update source detection", function () {
}) })
).to.equal(true); ).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 () { describe("removal workers", function () {