Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
119 commits
Select commit Hold shift + click to select a range
6205792
Add a shared model for projects
jarstelfox Sep 29, 2026
51a635f
Repair missed webhooks every hour
jarstelfox Sep 29, 2026
d57c1e8
Serve project issues and backlog stats by window
jarstelfox Sep 29, 2026
47eb44b
Split project work by developer team and count reviews
jarstelfox Sep 30, 2026
1cd633a
Keep a roadmap: planned projects in their own table
jarstelfox Sep 30, 2026
cca71bc
Add a Projects tab for whoever plans the work
jarstelfox Sep 30, 2026
492799c
Post health updates on roadmap items
jarstelfox Sep 30, 2026
ccd6a39
Mark milestones on the roadmap and add now, next, later
jarstelfox Sep 30, 2026
05e5bbf
Format the Projects tab the way the rest of the board is
jarstelfox Sep 30, 2026
2dbcbd1
Show milestone dates as their UTC day everywhere
jarstelfox Sep 30, 2026
b21858b
Split the merged work by whether the roadmap planned it
jarstelfox Sep 30, 2026
fcad466
Let roadmap items wait on each other
jarstelfox Sep 30, 2026
d74b8e2
Keep the Projects tab's code out of the board's bundle
jarstelfox Sep 30, 2026
8874866
Say how many plans each team runs at once
jarstelfox Sep 30, 2026
a82a760
Refresh the roadmap once, however many views show it
jarstelfox Sep 30, 2026
d6b436d
Compare the forecast with the milestone's own day
jarstelfox Sep 30, 2026
e1df617
Show each project's plan in the list and on its page
jarstelfox Sep 30, 2026
203ebec
Let a script run the roadmap through /api/v1
jarstelfox Sep 30, 2026
59761b7
Fix what a review of the roadmap work found
jarstelfox Sep 30, 2026
6cc3acf
Rebuild the roadmap timeline for 100 projects in flight
jarstelfox Sep 30, 2026
86e1308
Stack the roadmap's rows on a phone
jarstelfox Sep 30, 2026
509f56b
Edit the developer teams from the board or the API
jarstelfox Sep 30, 2026
dcd88a1
Let the dummy board run at a real team's size
jarstelfox Sep 30, 2026
844f7d7
Plan in-flight projects in one gesture, and find any row
jarstelfox Sep 30, 2026
c7da32c
Make every part of the roadmap do what it names on click
jarstelfox Sep 30, 2026
8d9440d
Let the Overview's numbers and plan words click through
jarstelfox Sep 30, 2026
aa52120
Add a parked status for work stopped for now
jarstelfox Sep 30, 2026
53a3466
Add Decide, a list of the calls the roadmap owes
jarstelfox Sep 30, 2026
5325c7f
Count the weeks ahead as if nothing changes
jarstelfox Sep 30, 2026
dc9333a
Drop the flag for a lead with work in other projects
jarstelfox Sep 30, 2026
eb5c612
Plan work in flight through a month or quarter's end
jarstelfox Sep 30, 2026
43bda2f
Lead the Overview with the calls Decide has waiting
jarstelfox Sep 30, 2026
3aa2014
Count a plan's update clock from when it was made
jarstelfox Sep 30, 2026
3c90383
Make Decide's rows right, and few enough to finish
jarstelfox Sep 30, 2026
9d98bed
Mark where each team's people run out on the roadmap
jarstelfox Sep 30, 2026
4a12744
Let Decide copy its list as text for a meeting
jarstelfox Sep 30, 2026
6d9c41e
Read project issues from every tracked repo
jarstelfox Sep 30, 2026
f2e4b18
Say where each plan's work came from
jarstelfox Sep 30, 2026
9c9cd40
Keep Q4 capitalized when Decide says what it did
jarstelfox Sep 30, 2026
8434b08
Read a project issue's Start, Target and Priority
jarstelfox Sep 30, 2026
4f034df
Copy each plan's dates into its issue's fields
jarstelfox Sep 30, 2026
08fb752
Name who runs Decide each week
jarstelfox Sep 30, 2026
13d9231
List who owes a plan update, for a reminder
jarstelfox Sep 30, 2026
76c493c
Merge master for the secret-logging fix (#505)
jarstelfox Sep 30, 2026
79f890e
Add Look back: where the team's days went
jarstelfox Sep 30, 2026
a684b71
Build Look back out for a retro
jarstelfox Sep 30, 2026
0c2e813
Make every Projects chart say what it counts
jarstelfox Sep 30, 2026
761efed
Count a project's last activity from real work
jarstelfox Sep 30, 2026
6f007f0
Let an opened list fold back
jarstelfox Sep 30, 2026
427c86f
Make the Projects Overview answer what managers ask
jarstelfox Oct 1, 2026
c92c6d6
Say what each Projects label counts, in one set of words
jarstelfox Oct 1, 2026
9447df4
Count "in progress" the same way everywhere
jarstelfox Oct 1, 2026
62c02eb
Lay Look back out as a retro reads it
jarstelfox Oct 1, 2026
de08210
Flag the same people as overloaded on both tabs
jarstelfox Oct 1, 2026
adde999
Call roadmap entries plans in the errors people see
jarstelfox Oct 1, 2026
5bc0061
Keep a PR's closes and connects across a restart
jarstelfox Oct 1, 2026
287f5f7
Let a plan name the issue that says what it delivers
jarstelfox Oct 1, 2026
2e52c0d
Fix what the reviews of plan scopes found
jarstelfox Oct 1, 2026
bf7252d
List a project's issues, and find them on GitHub as you type
jarstelfox Oct 1, 2026
cf4f37a
Fix what the reviews of the issue list found, server side
jarstelfox Oct 1, 2026
3109704
Attach issues to a project, not to a spec epic
jarstelfox Oct 1, 2026
aeb2d4a
Fix what the reviews of the issues redesign found
jarstelfox Oct 1, 2026
92f30b6
Fix the plan and suggestion bugs a check found
jarstelfox Oct 1, 2026
37f7e0c
Rethink the project page and link the views
jarstelfox Oct 1, 2026
6dc1cc6
Fix what the reviews of the rethink found
jarstelfox Oct 1, 2026
7b5e1bb
Name where each PR and issue stands in PM words
jarstelfox Oct 1, 2026
b0c4682
Make the project page answer a PM at a glance
jarstelfox Oct 1, 2026
7e436ce
Move the PR stage rule into the shared model
jarstelfox Oct 1, 2026
7236473
Fix what the review of the PM page found
jarstelfox Oct 1, 2026
e01ea90
Let Finish accept the PRs after done, and date it
jarstelfox Oct 1, 2026
b9f19ee
Lay the shared rules the Projects views build on
jarstelfox Oct 1, 2026
f63ef51
Keep open boards current, and name missing migrations
jarstelfox Oct 1, 2026
6c9deba
Show Decide's reopened call on the dummy board
jarstelfox Oct 1, 2026
02a65b4
Write down the Projects rules this round settled
jarstelfox Oct 1, 2026
e3cae6b
Let an Undo put back the times a call replaced
jarstelfox Oct 1, 2026
2bc526e
Send Look back when each PR closed
jarstelfox Oct 1, 2026
b27a616
Fix what the UI review of every Projects view found
jarstelfox Oct 1, 2026
2d848ab
Lay the shared fixes the second UI review found
jarstelfox Oct 1, 2026
b59999b
Give the dummy board the states the review missed
jarstelfox Oct 1, 2026
69c3dec
Fix what the second UI review of Projects found
jarstelfox Oct 1, 2026
c4feac9
Fill projects in from PR links and their numbers
jarstelfox Oct 2, 2026
9261aa4
Let every save be taken back, and keep keys in Projects
jarstelfox Oct 2, 2026
458d4c3
Ready Projects for real data and the review board
jarstelfox Oct 2, 2026
c645d01
Show a roadmap plan before its fields
jarstelfox Oct 2, 2026
b4bd4a3
Let a plan's end be hard, soft or ongoing
jarstelfox Oct 5, 2026
d50e1c7
Make the Projects tab calmer and say things once
jarstelfox Oct 9, 2026
5276621
Update DESIGN.md for the calmer Projects tab
jarstelfox Oct 9, 2026
f55a844
Merge remote-tracking branch 'origin/master' into projectdasher
jarstelfox Oct 9, 2026
9fe06e6
Name projects only from the projects repo
jarstelfox Oct 9, 2026
6a18835
Make the Projects tab read well on raw data
jarstelfox Oct 9, 2026
b7829d2
Tell the truth about overload, dates and old pages
jarstelfox Oct 9, 2026
2ee6ebc
Stop chart ticks reading as other years
jarstelfox Oct 9, 2026
4630d50
Recover a board after the server restarts
jarstelfox Oct 9, 2026
b400eeb
Add PR jail for people with too many open PRs
jarstelfox Oct 9, 2026
507840b
Keep PR jail's list live while the cell is down
jarstelfox Oct 9, 2026
9542f95
Group PR jail's list so the way out is on top
jarstelfox Oct 9, 2026
5d262dd
Make PR jail a plain modal you can turn off
jarstelfox Oct 9, 2026
d21e8d6
Split PR jail's switch into Automatic, Manual and Off
jarstelfox Oct 9, 2026
65b15e8
Snooze PR jail by holding a key, and count down to free
jarstelfox Oct 9, 2026
a56e033
Open PR jail's lock, and give its list room to read
jarstelfox Oct 9, 2026
74ad9c2
Rework PR jail into one clear button and the board's own look
jarstelfox Oct 9, 2026
fd0d8e0
Model the self-review policy in derive()
jarstelfox Oct 10, 2026
349dba9
Keep stamps through base merges, read team requests
jarstelfox Oct 10, 2026
532b4c3
Rework Stats for self-review
jarstelfox Oct 10, 2026
1aad1d6
Never count a bot as a developer
jarstelfox Oct 10, 2026
ebf4420
Rework the board's review flow for self-review
jarstelfox Oct 10, 2026
7f1f01c
Show a review request's age on the row
jarstelfox Oct 10, 2026
483bd5f
Stop treating answered or unasked reviewers as owing work
jarstelfox Oct 10, 2026
8160fd3
Fix the board's review flow where review found holes
jarstelfox Oct 10, 2026
66c86fe
Fix stamp keeping, API use and team times on the server
jarstelfox Oct 10, 2026
17b8df3
Move PR jail and self-review to their own pulls
jarstelfox Oct 10, 2026
73c65fd
Merge remote-tracking branch 'origin/master' into projectdasher
jarstelfox Oct 10, 2026
620f330
Fix what the review of the Projects branch found
jarstelfox Oct 10, 2026
1097efd
Index the dates the Projects windows filter on
jarstelfox Oct 10, 2026
23832ae
Make the index migration resumable, and widen project names
jarstelfox Oct 10, 2026
7fb326d
Fix stale numbers, Undo and dates in the Projects views
jarstelfox Oct 10, 2026
5bab4df
Refetch Projects data after a reconnect, and refuse twin teams
jarstelfox Oct 10, 2026
5d4a4fa
Handle deleted issues, failed loads and stuck syncs in Projects
jarstelfox Oct 10, 2026
2e81584
Fix two round-4 Projects fixes that missed their case
jarstelfox Oct 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,21 @@ jobs:
node-version: "24"
cache: npm

# also builds shared/dist and installs frontend-v2 (package.json postinstall)
- run: npm install

- name: Lint
run: npm run lint

- run: npm run build

- name: Server tests
run: npm test

- name: Frontend types
working-directory: frontend-v2
run: npx tsc --noEmit

- name: Frontend tests
working-directory: frontend-v2
run: npx vitest run
533 changes: 530 additions & 3 deletions DESIGN.md

Large diffs are not rendered by default.

183 changes: 183 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,189 @@ Two query string parameters are available to filter the displayed pulls:

`ex. https://pulldasher.example.com?milestone=site-redesign,12/5,12/19`

### Projects

An optional tab for whoever plans the work: which projects the open PRs
serve, who is on each, how they are moving, and a roadmap to plan them on.
Everything but the roadmap is read from GitHub; something else (a person,
or a job) files the PRs:

- A PR joins a project through one label, `project:<slug>`. PRs that fit no
project get `project:misc`. Keep a label to 32 characters with no spaces:
that's what Pulldasher's label table and filter box hold.
- A project's record is the issue in the projects repo carrying the same
label; with no projects repo, names come from labels. The issue's title
is the project's name, its
assignee the lead, its milestone (and due date) the target,
`parent:<slug>` labels its parents (any number), and an `ongoing` label
marks work with no end. GitHub's own issue fields show too: its Start
date, its Priority, and its Target date, which wins over a milestone,
since it's set on that one issue. Close it as completed when it's done, or as not
planned when it's dropped or merged into another. Labeling an issue in
the projects repo is also how a project starts before it has a PR. An
issue with the label in any other repo is work inside the project and
never renames it.

Turn it on with the `projects` block in `config.js` (see
`config.example.js`): a projects repo if you want one, and `developerTeams`, the teams
whose members count as developers (anyone else with a PR is a
non-developer, whose work needs a developer's review). The teams can also
be edited from the People view or the API; saved ones replace
`developerTeams` until someone goes back to it. Send Issues events in the
tracked repos' webhook, and give a projects repo the same webhook, so a
newly labeled issue shows up right away; an hourly pass catches what a
webhook missed.

The tab has five views and a page per project:

- **Overview**: where the plans stand (the latest update on every item in
progress, worst first, which copies as text for a status email),
headline numbers (how many calls Decide has waiting, then a date
range's numbers, each compared with the same number of days before),
whether the backlog is growing (the PRs open each day, and the PRs
opened and merged each week), where the time went (the projects that
took the most developer-days, a door to Look back), and every
project on one list you can sort, group by parent, lead or team,
search, and download as CSV, each with how its roadmap plan is going.
A project's own page shows its plan and latest update too.
- **Decide**: the calls owed now, as a list that empties: projects with 3
or more PRs (open, or merged in the last 14 days) and no decision, plans
past their end or their target date, stalls (open PRs with no activity
for 21 days), updates that say at risk or off track, work marked done or
dropped (on the roadmap or by closing its issue) whose PRs are still
open, and parked work whose PRs moved. Each row says why it's there, with
its team, size and target, and takes one click: commit through the end
of a coming month or quarter, park, finish, or drop. The row can also say
where the work came from, saved with the call. People take turns running
the list, a week each: it names this week's and next week's, and the
turns can be changed in place or with `PATCH /api/v1/settings`. A team's lead can
take just their team's rows, and copy them as text for a meeting's
notes. It weighs every project, whatever the filter bar narrows the
other views to. The tab's label counts what's left.
- **Roadmap**: the plan set against everything actually in flight. Across
the top, how many projects were in flight each week (from their PRs)
and, for the weeks ahead, the plans plus every project big enough to owe
a decision that has none, as if nothing changes, against a line at the
number of
developers. Below, the plans in priority order, then every live project
with no plan, one click from being planned.
- By month or by quarter; click a quarter or a month to fill the width
with it, and zoom out or step to the next one from the toolbar. Or
now, next and later, without week dates, for readers outside
engineering.
- Click a week in the load chart to see only what was in flight then, or
a count to see only those ("with no plan"). Find narrows by name,
lead or team.
- Each plan can say where its work came from: asked for from above
(top-down), or found by the team, as a fire to put out or its own pick
(bottom-up). Once the rows are narrowed to the plans, the load chart
splits its roadmap count that way, and a click on one shows only those
plans.
- Drag rows to set priority, drag a bar to move it and its edge to change
its length. A click on a bar opens the plan's details under its row,
where Decide's calls commit it through a month or a quarter in one
click, and Edit details holds the rest.
Team lanes say the most each team has in flight at once against its
developers, and draw a dashed line where, in priority order, the work
in flight outnumbers them: what's below it is what to park.
- A linked plan still in flight past its end grows an amber "+3 wk over";
its project's milestone is a flag with its date. An item can wait on
others, and says so when its plan starts before one of them ends.
Parked work is stopped for now without being dropped, and leaves the
load.
- Each item takes updates: on track, at risk or off track, and a note,
kept as a history. Work in progress with no update for 14 days is
flagged. The roadmap is the one thing the tab stores itself, in the
`roadmap_items` and `roadmap_updates` tables.
- **People**: developers by team and everyone else: live projects each,
open PRs, PRs opened and merged, and reviews given, including how many
went to non-developers' PRs.
- **Look back**: where the time went over a date range, for a retro, in
developer-days rather than PR counts, so a month of work outweighs a
one-line fix. A developer-day is a day someone opened, merged, commented
on, stamped or reviewed a PR, split across the PRs they touched that
day, as writing (their own) or reviewing (anyone else's). Commits aren't
counted: in a sample of 60 merged PRs they added about 4% more days,
since PRs open and merge within a day or two.
- Tiles open it, each against the same number of days before: the
days, how much went to reviewing, to work on the roadmap, to fires,
to PRs filed to no project, and how many different projects each
person touched a week.
- One list splits the days by project, by where the work came from, by
whose PR it was (their own, other developers', non-developers',
bots'), by team, by person or by repo, and can keep only the writing
or the reviewing days. Each row draws its days week by week on one
scale, with its share then and now, and how many of its PRs merged.
- A row's name goes where it names: a project to its page, an origin to
its plans, a team or person to only their days. Anywhere else on the
row opens the PRs that took the most days and who spent them. It all
copies as text for the retro's notes.

To put the plans where people already look, `bin/sync-issue-fields` copies
each plan under way into its project issue's Start date, Target date and
Priority fields: the plan's first Monday and last day, and Urgent for a
fire, else High, Medium or Low for work now, next or later. A team's project
board shows those fields, and the issue's history keeps every change. It
fills a field nobody has set, and changes one only if its own token set it
last, so a date a person moved stays moved; it says so instead. It's a dry
run unless given `--apply`, and its writes show as `github.token`'s account.

```sh
bin/sync-issue-fields # what it would write
bin/sync-issue-fields --apply # write it
```

Everything the tab shows, and every change the roadmap takes, is in the API,
Bearer-authed with the caller's own GitHub token like `/api/v1/pulls`, so a
script or a Claude session can read the projects and run the roadmap
without a browser. `GET /api/v1` lists every route with what it does, and
the rules a roadmap write is checked against:

```sh
curl -s -H "Authorization: Bearer $(gh auth token)" https://pulldasher.example.com/api/v1
```

- `GET /api/v1/projects` and `GET /api/v1/people`: each project and each
person with where they stand today and their numbers for a window. Both
take `start` and `end` as `YYYY-MM-DD` days (default: the last 30 days, at
most 400), and `project=<slug>` narrows the numbers to one project.
- `GET /api/v1/decide`: the calls owed now, worst first, each with its
project, its roadmap item if it has one, and its reasons. `GET /api/v1`
says what each reason means and which write clears it. It also names who
runs Decide this week and next.
- `GET /api/v1/retro`: where the days went in a window (`start`, `end`,
default the last 30 days): per person and PR, their share of active
days, whether they wrote it, and its project.
- `GET /api/v1/updates-owed`: each lead who owes an update, with their
plans in progress that have gone 14 days without one, longest overdue
first. It's what a weekly reminder would send each lead; nothing sends
it yet.
- `GET /api/v1/load`: projects in flight each week against the developer
count, on the roadmap and not, with the weeks after this one counted as
if nothing changes. `start` and `end` as above (default: 12 weeks back to
26 ahead).
- `GET /api/v1/roadmap` and `GET /api/v1/roadmap/:id`: the items in
priority order, or one, each with its latest update.
- `POST /api/v1/roadmap`, `PATCH` and `DELETE /api/v1/roadmap/:id`: add,
change or remove an item. A removed item is kept, out of every list, and
`PATCH` with `{"restore": true}` puts it back with its updates, place and
waits. `POST /api/v1/roadmap/:id/move` with
`{"before": <id>}` (or `null` for the bottom) moves one item without
sending the whole order; `PUT /api/v1/roadmap/order` sets the whole order.
- `GET` and `POST /api/v1/roadmap/:id/updates`: an item's updates, or a new
one, `{"health": "at_risk", "body": "..."}`. `DELETE
/api/v1/roadmap/:id/updates/:update` takes one back; only its author can.
- `GET` and `PATCH /api/v1/settings`: the developer teams and whether
they're saved or `config.js`'s. `{"developer_teams": {"Store": ["dana"]}}`
replaces them; `null` goes back to `config.js`.

A write takes a JSON body and is recorded as the token's login. The board
itself uses the same handlers through `/roadmap`, with its session.

Pulldasher also re-lists every tracked repo's open PRs once an hour and
refreshes only the ones its database has wrong, so a lost webhook can't leave
a merged PR counted as open.

## Architecture

When first started, the Pulldasher server fetches information about the current
Expand Down
125 changes: 117 additions & 8 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,15 @@ import mainController from './controllers/main.js';
import hooksController from './controllers/githubHooks.js';
import statsController from './controllers/stats.js';
import userNamesController from './controllers/user-names.js';
import apiController from './controllers/api.js';
import projectsController from './controllers/projects.js';
import roadmapController, { canWrite, forgetProjectCaches } from './controllers/roadmap.js';
import { API_ROUTES, apiIndex } from './controllers/api-routes.js';
import settingsController from './controllers/settings.js';
import { syncRepoIssues } from './lib/project-issue-sync.js';
import { loadSettingsWithRetry } from './lib/settings.js';
import { issueRepos, projectSettings } from './lib/projects.js';
import { setOnWorkSynced, syncWork } from './lib/work.js';
import { missingMigrations } from './lib/schema-check.js';
import apiAuth from './lib/api-auth.js';
import Debug from './lib/debug.js';
import { createServer } from 'http';
Expand Down Expand Up @@ -85,21 +93,81 @@ app.use('/', express.static(__dirname + '/frontend-v2/dist'));
app.get('/token', mainController.getToken);
app.get('/stats-history', statsController.getHistory);
app.get('/user-names', userNamesController.getNames);
app.get('/projects-data', projectsController.getBoardData);
app.get('/retro-data', projectsController.getRetro);
app.get('/work-data', projectsController.getWork);
app.get('/issue-search', projectsController.searchIssues);
app.get('/project-work', projectsController.getProjectWork);
// the review board's view of projects: which projects its PRs link, and
// each project's plan, so it orders and names PRs by the Projects tab's rule
app.get('/project-standing', roadmapController.standing);

// Something open boards show of the projects changed: what's kept of them is
// read again, and every open board fetches again, so one person's call shows
// on another's screen without a reload.
function projectsChanged() {
forgetProjectCaches();
pullManager.projectsChanged();
}
// every write the Projects tab can see
const PROJECT_WRITES = ['/project-issues', '/roadmap', '/settings', '/api/v1/project-issues', '/api/v1/roadmap', '/api/v1/settings'];
app.use(PROJECT_WRITES, function (req, res, next) {
if (req.method !== 'GET') {
res.on('finish', function () {
if (res.statusCode < 300) projectsChanged();
});
}
next();
});
setOnWorkSynced(projectsChanged);
app.post('/project-issues', canWrite, projectsController.attachIssue);
app.delete('/project-issues', canWrite, projectsController.detachIssue);
// the roadmap is the one part of the Projects tab people edit here: reads are
// gated like the other board data (lib/authentication.js), writes by canWrite
app.get('/roadmap', roadmapController.list);
app.post('/roadmap', canWrite, roadmapController.create);
app.put('/roadmap/order', canWrite, roadmapController.reorder);
app.patch('/roadmap/:id', canWrite, roadmapController.update);
app.delete('/roadmap/:id', canWrite, roadmapController.remove);
app.get('/roadmap/:id/updates', roadmapController.updates);
app.post('/roadmap/:id/updates', canWrite, roadmapController.postUpdate);
app.delete('/roadmap/:id/updates/:update', canWrite, roadmapController.removeUpdate);
app.get('/settings', settingsController.get);
app.patch('/settings', canWrite, settingsController.update);
app.post('/hooks/main', hooksController.main);

// /api/v1: machine-to-machine JSON for the review skills, Bearer-authed with
// the caller's own GitHub token (see lib/api-auth). Independent of the
// cookie-session gate -- setupRoutes never registers these paths, so the
// session `auth` middleware doesn't run for them.
app.get('/api/v1/me', apiAuth, apiController.getMe);
app.get('/api/v1/pulls', apiAuth, apiController.getPulls);
// /api/v1: machine-to-machine JSON for the review skills and scripts,
// Bearer-authed with the caller's own GitHub token (see lib/api-auth). Every
// route is in controllers/api-routes.js, and GET /api/v1 lists them.
// Independent of the cookie-session gate -- setupRoutes never registers these
// paths, so the session `auth` middleware doesn't run for them.
app.get('/api/v1', apiAuth, apiIndex);
for (const { method, path, handlers } of API_ROUTES) app[method](path, apiAuth, ...handlers);

// Warm the bot-login cache (used to tell a pulldasher claim apart from a
// GitHub-UI self-request) before any webhook or socket traffic needs it.
// Memoized in git-manager, so this just avoids the first caller paying for
// the lookup.
git.getBotLogin();

// Saved settings (the developer teams) replace config.js's once loaded;
// until then, and if the table can't be read, config.js's stand.
loadSettingsWithRetry();

// Migrations run by hand, before a deploy (migrations/*.sql never run on
// one). Say loudly at startup which are missing, since the board then
// fails in pieces: pull refreshes, the issue syncs, roadmap reads.
missingMigrations()
.then(missing => {
if (missing.length) {
console.error(
'The database is missing migrations, so parts of the board will fail. Run these by hand, in order: %s',
missing.join(', ')
);
}
})
.catch(err => console.error('Checking the schema failed: %s', (err && err.message) || err));

debug('Loading all recent pulls from the DB');
dbManager
.getRecentPulls(pullManager.getOldestAllowedPullTimestamp())
Expand All @@ -114,20 +182,61 @@ dbManager
.then(function () {
debug('Refreshing all open pulls from the API');
refresh.openPulls().finally(boardLoaded);
syncProjectIssues();
syncAttachedIssues();
})
.done();

// Webhooks get lost, and a lost `closed` left a PR open on the board until the
// next restart (pulldasher#501 repairs it only at startup), which inflates every
// backlog number. Once an hour, list each repo's open pulls and refresh just the
// ones the DB has wrong.
// ones the DB has wrong, and pick up project issues that changed.
const RECONCILE_MS = 60 * 60 * 1000;
setInterval(function () {
refresh.reconcileOpenPulls().catch(function (err) {
console.error('Hourly open-pull repair failed: %s', (err && err.message) || err);
});
syncProjectIssues();
syncAttachedIssues();
}, RECONCILE_MS);

// The issues added to projects by hand, and which PRs link each issue a
// project has (lib/work.js), read off GitHub once an hour; a webhook on one
// of those issues reads them sooner.
function syncAttachedIssues() {
syncWork(projectSettings()).catch(function (err) {
console.error('Work sync failed: %s', (err && err.message) || err);
});
}

// Project issues can live in any tracked repo (lib/projects.js issueRepos).
// The projects repo is synced whole at startup; a tracked repo only from
// TRACKED_ISSUES_DAYS back, since listing every issue of a big repo costs
// thousands of calls, and webhooks already keep its issues current. So an
// issue labeled while webhooks were down for longer than that waits for its
// next change (or bin/refresh-open-issues). After that, only what changed.
// `since` backs off a few minutes so GitHub's clock can't skip an update.
const TRACKED_ISSUES_DAYS = 7;
function syncProjectIssues() {
const projects = projectSettings();
if (!projects) return;
const startedAt = new Date(Date.now() - 5 * 60 * 1000).toISOString();
const lookback = new Date(Date.now() - TRACKED_ISSUES_DAYS * 86400 * 1000).toISOString();
const synced = issueRepos(projects).map(function (repo) {
const firstSince = repo === projects.repo ? null : lookback;
return syncRepoIssues(refresh, repo, firstSince, startedAt).catch(function (err) {
console.error('Project issue sync failed in %s: %s', repo, (err && err.message) || err);
return false;
});
});
// a project's name, target or state may have changed: open boards hear it
// once, after every repo is read, and only when an issue changed (a word
// per repo had every open Projects tab fetch it all again once per repo)
Promise.all(synced).then(function (changed) {
if (changed.some(Boolean)) projectsChanged();
});
}

//====================================================
// Socket.IO
const io = new Server(httpServer);
Expand Down
Loading
Loading