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
16 changes: 16 additions & 0 deletions cloudflare/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,22 @@ the item showed (a question, a GitHub change, a quest's text). A newer version
is unread again, and skipping an item lasts until it changes. A finished or
stopped job sets its item aside the same way.

### Landing queue

`/projects/github/<owner>/<name>/queue` (`src/queue.tsx`) shows each base's
landing queue, under the same maintainer check as the project's other pages,
read from GitHub when the page loads (`src/github/queue.ts`); kix stores none
of it. A base with GitHub's merge queue lists its entries by position, building
entries linking their merge group's run, then the PRs whose armed auto-merge
waits on checks to join. Any other base lists its armed PRs in the order they
were armed, the watcher's own queue order, each with the `quest queue` check
the watcher writes, whose in-progress run links its build. Recent drops are
the watcher's marked removal comments from the last day, in both modes, each
paired with the watcher's next marked comment on that pick: re-enqueued,
handed back, not applied, refused, or held. A drop with no later word, such
as one a hold, a push, or a person's removal ends, shows what its own comment
said happens next. Only comments from an app or a writer count.

### Installed app and notifications

kix installs as a progressive web app (`src/manifest.webmanifest`,
Expand Down
91 changes: 91 additions & 0 deletions cloudflare/dev/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export type Pull = Issue & {
landing?: boolean;
/** It is in its base's merge queue; only a base with `queue` takes it. */
queued?: boolean;
/** Its merge queue entry's state, `QUEUED` by default, and its merge group's check runs, while `queued`. */
group?: { state: string; runs: NonNullable<Pull["runs"]> };
/** The enabled auto-merge's time and commit body; `quest land` reads its queue from them. */
autoMerge?: { enabledAt: string; commitBody: string };
/** A fork's head branch accepts pushes from the upstream's maintainers. */
Expand Down Expand Up @@ -463,6 +465,50 @@ function contextPage(pull: Pick<Pull, "runs">, after: unknown) {
};
}

/** An open pull request as kix's landing queue page reads it: its landing, merge group, and `quest queue` check. */
function landingTarget(repo: Repo, pull: Pull, queued: Pull[]) {
const head = headOf(repo, pull);
const run = (repo.checkRuns ?? []).filter((r) => r.name === "quest queue" && r.head_sha === head).at(-1);
return {
number: pull.number,
title: pull.title,
url: `https://github.com/${repo.owner}/${repo.name}/pull/${pull.number}`,
baseRefName: pull.base ?? defaultOf(repo),
isMergeQueueEnabled: repo.queue === true && (pull.base ?? defaultOf(repo)) === defaultOf(repo),
autoMergeRequest: pull.landing ? { enabledAt: pull.autoMerge?.enabledAt ?? updatedAt } : null,
mergeQueueEntry: pull.queued
? {
position: queued.indexOf(pull) + 1,
state: pull.group?.state ?? "QUEUED",
headCommit: pull.group ? { statusCheckRollup: { contexts: contextPage(pull.group, null) } } : null,
}
: null,
commits: {
nodes: [
{
commit: {
checkSuites: page(
run
? [
{
checkRuns: page([
{
status: (run.status ?? "completed").toUpperCase(),
title: run.output.title ?? null,
detailsUrl: run.details_url ?? null,
},
]),
},
]
: [],
),
},
},
],
},
};
}

function graphql(repo: Repo, query: string, variables: Record<string, unknown>) {
const blobs = [...query.matchAll(/(q\d+): object\(oid: "([0-9a-f]{40})"\)/g)];
if (blobs.length) {
Expand Down Expand Up @@ -559,6 +605,25 @@ function graphql(repo: Repo, query: string, variables: Record<string, unknown>)
},
};
}
if (query.includes('checkName: "quest queue"')) {
const open = repo.pulls.filter((pull) => !pull.closed);
const queued = open.filter((pull) => pull.queued);
return { data: { repository: { pullRequests: page(open.map((pull) => landingTarget(repo, pull, queued))) } } };
}
// The pull requests a landing drop names, by number, which may have closed.
const dropped = [...query.matchAll(/(p\d+): pullRequest\(number: (\d+)\)/g)];
if (dropped.length) {
const found = Object.fromEntries(
dropped.map(([, alias, number]) => {
const pull = repo.pulls.find((p) => p.number === Number(number));
return [alias, pull ? landingTarget(repo, pull, []) : null];
}),
);
const errors = dropped
.filter(([, alias]) => !found[alias!])
.map(([, alias]) => ({ type: "NOT_FOUND", path: ["repository", alias] }));
return { data: { repository: found }, ...(errors.length ? { errors } : {}) };
}
if (query.includes("defaultBranchRef"))
return { data: { repository: { defaultBranchRef: { name: defaultOf(repo) } } } };
if (query.includes("queue:pullRequests(")) {
Expand Down Expand Up @@ -1264,6 +1329,32 @@ export function fakeGitHub(world: World) {
message: "Pull Request successfully merged",
});
}
// Every issue and pull request comment, oldest first, updated at or after `since`.
if (tail === "/issues/comments" && method === "GET") {
if (!scope.permissions.issues && !scope.permissions.pull_requests) return forbidden();
const since = Date.parse(url.searchParams.get("since") ?? "1970-01-01T00:00:00Z");
const page = Number(url.searchParams.get("page") ?? 1);
const size = Number(url.searchParams.get("per_page") ?? 30);
const all = [...repo.pulls, ...repo.issues]
.flatMap((item) =>
(item.comments ?? []).map((c, i) => ({
id: item.number * 1000 + i,
html_url: `https://github.com/${fullName}/pull/${item.number}#issuecomment-${item.number * 1000 + i}`,
issue_url: `https://api.github.com/repos/${fullName}/issues/${item.number}`,
body: c.body,
created_at: c.at,
updated_at: c.at,
user: c.author && { login: c.author, type: c.author.endsWith("[bot]") ? "Bot" : "User" },
author_association:
c.author && ["admin", "maintain", "write"].includes(roleOf(repo, c.author) ?? "")
? "COLLABORATOR"
: "NONE",
})),
)
.filter((c) => Date.parse(c.created_at) >= since)
.sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at));
return json(all.slice((page - 1) * size, page * size));
}
const comment = tail.match(/^\/issues\/(\d+)\/comments$/);
if (comment && method === "GET") {
const item = [...repo.pulls, ...repo.issues].find((i) => String(i.number) === comment[1]);
Expand Down
Loading