diff --git a/README.md b/README.md index b3f3a9fa..926ec8c1 100644 --- a/README.md +++ b/README.md @@ -230,7 +230,8 @@ pass), it: - finishes a plan: one under way whose issue closed, or all of whose issues closed, with no PRs open, is done, or dropped when they were closed as not planned. One past its hard end with none open, its status untouched - since, is done. + since, is done. Never an ongoing plan: upkeep has no end, so closed + issues only mean nothing is asked of it, and it parks once it's idle. - plans a project: one with 3 or more PRs open or merged in the last 14 days that never had a plan gets one, in progress from its first open PR's week (or its first PR's, when all have merged) with no end, named diff --git a/frontend-v2/src/model/autoCalls.test.ts b/frontend-v2/src/model/autoCalls.test.ts index 6f269135..3ac33b14 100644 --- a/frontend-v2/src/model/autoCalls.test.ts +++ b/frontend-v2/src/model/autoCalls.test.ts @@ -318,6 +318,17 @@ describe('finished', () => { expect(calls([plan], { p1: [] }, { rows: dropped })[0]).toMatchObject({ status: 'dropped' }); }); + it('never finishes an ongoing plan, whose issues closing means nothing is asked of it', () => { + const plan = item(1, { start: '2026-09-07', end_kind: 'ongoing' }); + const issues = new Map([['p1', { total: 3, open: 0, done: 3, dropped: 0, lastClosedAt: ago(2) }]]); + const finishes = (c: ReturnType) => + c.filter(x => x.kind === 'status' && (x.status === 'done' || x.status === 'dropped')); + const rows = rowsFor(plan, [project('p1')], { issues }); + expect(finishes(calls([plan], { p1: [] }, { rows }))).toEqual([]); + const closed = rowsFor(plan, [project('p1')], { closed: closedOn('done', 3) }); + expect(finishes(calls([plan], { p1: [] }, { rows: closed }))).toEqual([]); + }); + it('finishes a plan past its hard end with none open, until someone reopens it', () => { const plan = item(1, { end_kind: 'hard', start: '2026-08-03', weeks: 4, status_at: ago(60) }); const rows = rowsFor(plan, [project('p1')]); diff --git a/shared/model/autoCalls.ts b/shared/model/autoCalls.ts index 923ca88f..6540b7f6 100644 --- a/shared/model/autoCalls.ts +++ b/shared/model/autoCalls.ts @@ -26,7 +26,8 @@ import { isUnderWay, mondayOf, planEnd, type RoadmapItem } from './roadmap'; * - finished: a plan under way whose issue closed, or all of whose issues * closed, with none of its PRs open, is done (dropped when they were * closed as not planned); one past its hard end with none open, its - * status untouched since, is done (model/decide.ts answeredOnItsOwn) + * status untouched since, is done (model/decide.ts answeredOnItsOwn). + * Never an ongoing plan: upkeep has no end, so it parks when idle * - new: a project with DECIDE_MIN_PRS or more PRs open or merged in the * last LIVE_DAYS, that never had a plan (not even one since removed), * gets one in progress, with no end; put down parks it once it's idle. @@ -239,6 +240,9 @@ export function autoCalls({ for (const r of row.reasons) { const item = row.item; if (!item || !isUnderWay(item.status) || !answeredOnItsOwn(r, item, timeZone)) continue; + // upkeep has no end: closed issues mean nothing is asked of it right + // now, not that it's finished; it parks once it's idle instead + if (item.end_kind === 'ongoing') continue; if (r.kind === 'issue_closed') { const as = r.as === 'dropped' ? 'not planned' : 'completed'; setStatus(item, r.as, `Finished: its issue was closed as ${as} on ${r.on}`);