diff --git a/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx b/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx
index 8a9b020..e9e2642 100644
--- a/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx
+++ b/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx
@@ -82,6 +82,7 @@ import {
dismissPullReview,
getCommentPage,
getTimelineEventPage,
+ markPullReadyForReview,
mergePullRequest,
replyToReviewComment,
requestPullReviewers,
@@ -254,6 +255,8 @@ export function PullDetailActivitySection({
pullNumber={pullNumber}
prTitle={pr.title}
firstCommitMessage={commits?.[0]?.message}
+ isDraft={pr.isDraft}
+ pullId={pr.graphqlId}
/>
)}
@@ -328,6 +331,8 @@ function MergeStatusSection({
pullNumber,
prTitle,
firstCommitMessage,
+ isDraft,
+ pullId,
}: {
scope: GitHubQueryScope;
owner: string;
@@ -335,6 +340,8 @@ function MergeStatusSection({
pullNumber: number;
prTitle: string;
firstCommitMessage?: string;
+ isDraft: boolean;
+ pullId: string | undefined;
}) {
const input = useMemo(
() => ({ owner, repo, pullNumber }),
@@ -389,6 +396,8 @@ function MergeStatusSection({
pullNumber={pullNumber}
prTitle={prTitle}
firstCommitMessage={firstCommitMessage}
+ isDraft={isDraft}
+ pullId={pullId}
/>
);
}
@@ -486,6 +495,8 @@ function MergeStatusCard({
pullNumber,
prTitle,
firstCommitMessage,
+ isDraft,
+ pullId,
}: {
scope: GitHubQueryScope;
status: PullStatus;
@@ -494,6 +505,8 @@ function MergeStatusCard({
pullNumber: number;
prTitle: string;
firstCommitMessage?: string;
+ isDraft: boolean;
+ pullId: string | undefined;
}) {
const {
checks,
@@ -585,18 +598,27 @@ function MergeStatusCard({
/>
)}
- {/* Merge action footer */}
-
+ {/* Merge action footer (or "ready for review" CTA when draft) */}
+ {isDraft ? (
+
+ ) : (
+
+ )}
);
}
@@ -1330,6 +1352,71 @@ function UpdateBranchButton({
);
}
+// ── Ready for review footer ─────────────────────────────────────────
+
+function ReadyForReviewFooter({
+ owner,
+ repo,
+ pullNumber,
+ pullId,
+}: {
+ owner: string;
+ repo: string;
+ pullNumber: number;
+ pullId: string | undefined;
+}) {
+ const [isMarking, setIsMarking] = useState(false);
+ const queryClient = useQueryClient();
+
+ const handleMarkReady = async () => {
+ if (!pullId) {
+ toast.error("Missing pull request id");
+ return;
+ }
+ setIsMarking(true);
+ try {
+ const result = await markPullReadyForReview({
+ data: { owner, repo, pullNumber, pullId },
+ });
+ if (result.ok) {
+ await queryClient.invalidateQueries({ queryKey: ["github"] });
+ } else {
+ toast.error(result.error);
+ checkPermissionWarning(result, `${owner}/${repo}`);
+ setIsMarking(false);
+ }
+ } catch {
+ toast.error("Failed to mark as ready for review");
+ setIsMarking(false);
+ }
+ };
+
+ return (
+
+
+
+ This pull request is a draft. Mark it as ready for review to allow
+ merging.
+
+
+ );
+}
+
// ── Merge footer ────────────────────────────────────────────────────
const MERGE_STRATEGIES = [
diff --git a/apps/dashboard/src/lib/github.functions.ts b/apps/dashboard/src/lib/github.functions.ts
index 21c26f4..8122bd4 100644
--- a/apps/dashboard/src/lib/github.functions.ts
+++ b/apps/dashboard/src/lib/github.functions.ts
@@ -6402,6 +6402,35 @@ export const mergePullRequest = createServerFn({ method: "POST" })
}
});
+export type ReadyForReviewInput = PullFromRepoInput & {
+ /** GraphQL node id of the pull request (e.g. `PR_kwD...`). */
+ pullId: string;
+};
+
+export const markPullReadyForReview = createServerFn({ method: "POST" })
+ .inputValidator(identityValidator)
+ .handler(async ({ data }): Promise => {
+ const context = await getGitHubUserContextForRepository(data);
+ if (!context) {
+ return { ok: false, error: "Not authenticated" };
+ }
+
+ try {
+ await context.octokit.graphql(
+ `mutation($pullRequestId: ID!) {
+ markPullRequestReadyForReview(input: { pullRequestId: $pullRequestId }) {
+ pullRequest { isDraft }
+ }
+ }`,
+ { pullRequestId: data.pullId },
+ );
+ await bustPullDetailCaches(context.session.user.id, data);
+ return { ok: true };
+ } catch (error) {
+ return toMutationError("mark pull request as ready for review", error);
+ }
+ });
+
export const deleteBranch = createServerFn({ method: "POST" })
.inputValidator(
identityValidator<{