Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 21 additions & 3 deletions cloudflare/GITHUB.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,15 +391,33 @@ pushes/comments/reviews, and reconciliation all update the same item. Webhooks
retain identity pointers only, never issue/PR bodies. Authoritative API reads add
escaped titles, state, comment/review counts, current PR head, that head's check
rollup, aggregate review decision, and up to 20 latest reviewer records with an
explicit pagination flag. Raw bodies and individual check logs are not cached.
explicit pagination flag. Raw bodies and individual check logs are not cached:
a PR's body is kept only as whether it has a `## Decisions needed` section.
Unavailable/deleted entities retain their notification identity and are marked
missing rather than disappearing or presenting old state as current.

GitHub reports `mergeStateStatus` `UNKNOWN` while it computes mergeability and
may send no event once it has. A read that sees it on a recorded PR at the same
head keeps the last known merge state, so nothing looks new; at a new head it
stores `UNKNOWN` but leaves the item's change time, so it becomes news once a
definite state arrives. A newly seen PR is news either way.

Closed entities only appear in the activity log, so a read that finds an entity
closed for longer than `RETENTION` (`cloudflare/src/github/state.ts`) deletes its
row instead of updating it, unless a webhook named it again since. History
rereads every entity, so a pass prunes every expired row. The board, inbox, jobs
and `/data` read only open entities and unread webhook pointers, through a
partial index, and the activity log pages 50 rows at a time, so neither read
grows with a repository's history.

A PR associates with a quest only when its head branch exactly matches the quest's
path without `.md` (or an epic's `/README.md`). An unrelated human PR remains in
activity without a guessed association. Open associated PRs and observed quest
branch claims exclude that quest from candidate data; branch claims display as
Claimed on the board. Runner/run associations belong to job coordination.
Claimed on the board. So does a draft PR its starter still holds, one without a
`## Decisions needed` section, which the inbox lists as in progress rather than
as a decision; a ready PR or a handed-back draft is Ready to turn in.
Runner/run associations belong to job coordination.

Every cached read and refresh proves current maintainer authority, live repository
identity/public visibility, installation access, and enrollment for that
Expand All @@ -414,7 +432,7 @@ observation tied to its commit, **never execution permission**: job coordination
must validate current eligibility, fresh revision, locks, and PRs before a start.
Review/check data is similarly descriptive and cannot approve a merge.

The `/data` endpoint exposes the same authenticated snapshot/activity with
The `/data` endpoint exposes the same authenticated snapshot and open activity with
`Cache-Control: no-store` for future decision and job consumers. The activity
page is a secondary read view; the durable decision inbox and its unread/answer
state belong to the prompts workflow. Neither incoming activity nor refreshing
Expand Down
6 changes: 5 additions & 1 deletion cloudflare/dev/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ export type Pull = Issue & {
review: "APPROVED" | "CHANGES_REQUESTED" | "REVIEW_REQUIRED" | null;
reviews?: { state: string; author: string }[];
draft?: boolean;
/** A `## Decisions needed` section in it hands a draft back to the maintainer. */
body?: string;
/** GitHub's mergeStateStatus; only CLEAN merges. Defaults to BLOCKED, or DRAFT for a draft. */
merge?: "CLEAN" | "BLOCKED" | "DIRTY" | "UNSTABLE";
merge?: "CLEAN" | "BLOCKED" | "DIRTY" | "UNSTABLE" | "UNKNOWN";
};
export type Repo = {
id: number;
Expand Down Expand Up @@ -49,6 +51,7 @@ function entity(repo: Repo, item: Issue) {
title: item.title,
state: item.closed ? "CLOSED" : "OPEN",
updatedAt,
closedAt: item.closed ? updatedAt : null,
author: { login: item.author },
comments: { totalCount: item.comments ?? 0 },
};
Expand All @@ -59,6 +62,7 @@ function pullNode(repo: Repo, pull: Pull) {
const reviews = pull.reviews ?? [];
return {
...entity(repo, pull),
body: pull.body ?? "",
headRefName: pull.branch,
headRefOid: oid,
isDraft: !!pull.draft,
Expand Down
37 changes: 37 additions & 0 deletions cloudflare/dev/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ export function world(): World {
content: quest("Later", "Work after the beta.", [
"- [Offline mode](/quest/m1/offline.md) - keep working without a network",
"- [Themes](/quest/m1/themes.md) - custom colors",
"- [Search](/quest/m1/search.md) - find anything",
]),
},
{ path: "quest/m1/search.md", content: quest("[S] Search across projects", "Find anything.") },
{
path: "quest/m1/offline.md",
content: quest(
Expand Down Expand Up @@ -109,6 +111,38 @@ export function world(): World {
checks: null,
review: "REVIEW_REQUIRED",
},
// A claim its starter still holds: work in progress, not a decision.
{
number: 17,
title: "Draft the theme picker",
author: "contributor",
branch: "quest/m1/themes",
checks: "PENDING",
review: null,
draft: true,
body: "Started on the picker.",
},
// A draft handed back with decisions: it waits on the maintainer.
{
number: 18,
title: "Search quest titles",
author: "contributor",
branch: "quest/m1/search",
checks: "SUCCESS",
review: null,
draft: true,
body: "Titles are indexed.\n\n## Decisions needed\n\n1. Index quest bodies too?\n",
},
// Clean when first read; `seed` rereads it while GitHub reports UNKNOWN.
{
number: 19,
title: "Speed up cold start",
author: "contributor",
branch: "perf/cold-start",
checks: "SUCCESS",
review: "APPROVED",
merge: "CLEAN",
},
],
claims: ["quest/m0/docs"],
};
Expand Down Expand Up @@ -213,6 +247,9 @@ export async function seed(options: { fetch: Fetch; db: D1Database; world: World
"pause archive",
);

// GitHub recomputes a recorded PR's mergeability, such as after a push to
// its base. The read below keeps its last known state rather than marking it new.
rocket.pulls.find((pull) => pull.number === 19)!.merge = "UNKNOWN";
// An issue opened since the refresh: its webhook rereads the project in the background.
const pointer = { number: 9, title: "Sync stops after the laptop sleeps", author: "reporter" };
rocket.issues.push(pointer);
Expand Down
9 changes: 9 additions & 0 deletions cloudflare/migrations/0015_activity_reads.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- Activity reads that every board, inbox, and background read makes stay
-- proportional to open work, not to a repository's history. `readState` reads
-- only open entities and unread webhook pointers, in this index's order (its
-- WHERE term matches the index's exactly), and a background read takes the
-- entities webhooks named. The paged activity log, read only on its own page,
-- sorts the repository's rows, which retention bounds.
CREATE INDEX github_project_activity_live ON github_project_activity(repository,firstSeen,kind,id)
WHERE (data IS NULL OR json_extract(data,'$.state')='OPEN');
CREATE INDEX github_project_activity_stale ON github_project_activity(repository,changedAt,id) WHERE stale>0;
5 changes: 3 additions & 2 deletions cloudflare/src/board/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export type Status =
| { kind: "turn-in"; change: Change }
| { kind: "available" }
| { kind: "pending" }
| { kind: "accepted" }
/** Claimed by a quest branch, or by the draft PR its starter still holds. */
| { kind: "accepted"; change?: Change }
| { kind: "blocked"; requires: Ref[] };

/** A link to another quest, or a blocker's own words when it names none. */
Expand Down Expand Up @@ -135,7 +136,7 @@ export function readBoard(project: Project): Board {

const status = (doc: Doc): Status => {
const change = changes.get(doc.path);
if (change) return { kind: "turn-in", change };
if (change) return change.owned ? { kind: "accepted", change } : { kind: "turn-in", change };
if (project.claims?.includes(doc.path)) return { kind: "accepted" };
if (free.has(doc.path)) return { kind: "available" };
if (open.has(doc.path)) return { kind: "pending" };
Expand Down
15 changes: 11 additions & 4 deletions cloudflare/src/board/pages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -340,10 +340,17 @@ function Actions(props: { board: Board; quest: Quest; user: User | null; path: s
if (status.kind === "accepted")
return (
<Panel title="Claimed">
<p>
A quest branch already claims this work. Review its current GitHub state before starting another
implementation.
</p>
{status.change ? (
<p>
{status.change.author} is working on this in a <a href={status.change.href}>draft PR</a>. It
waits on you once they mark it ready or hand back decisions.
</p>
) : (
<p>
A quest branch already claims this work. Review its current GitHub state before starting another
implementation.
</p>
)}
</Panel>
);
if (status.kind === "pending")
Expand Down
2 changes: 2 additions & 0 deletions cloudflare/src/board/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export type Change = {
href: string;
/** The pull request's number, when the change is one observed on GitHub. */
number?: number;
/** A draft its author still holds, without a `## Decisions needed` section: a claim, not yet turned in. */
owned?: boolean;
};

/** An issue filed from a fork, quarantined under `issues/` until a maintainer promotes it. */
Expand Down
Loading
Loading