fix(server): checkout fork PRs into owner branches
This commit is contained in:
@@ -161,6 +161,34 @@ function pullRequestJson(title: string): string {
|
||||
]);
|
||||
}
|
||||
|
||||
function pullRequestCheckoutTargetJson(): string {
|
||||
return JSON.stringify({
|
||||
data: {
|
||||
repository: {
|
||||
pullRequest: {
|
||||
number: 526,
|
||||
baseRefName: "main",
|
||||
headRefName: "main",
|
||||
isCrossRepository: true,
|
||||
headRepositoryOwner: { login: "therainisme" },
|
||||
headRepository: {
|
||||
sshUrl: "git@github.com:therainisme/paseo.git",
|
||||
url: "https://github.com/therainisme/paseo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function repoViewJson(): string {
|
||||
return JSON.stringify({
|
||||
owner: { login: "getpaseo" },
|
||||
name: "paseo",
|
||||
parent: null,
|
||||
});
|
||||
}
|
||||
|
||||
function issueJson(title: string): string {
|
||||
return JSON.stringify([
|
||||
{
|
||||
@@ -275,6 +303,38 @@ describe("GitHubService", () => {
|
||||
expect(computeGithubNextInterval(stableStatus, 4)).toBe(EXPECTED_GITHUB_ERROR_BACKOFF_CAP_MS);
|
||||
});
|
||||
|
||||
it("loads pull request checkout target details through GraphQL", async () => {
|
||||
const runner = createRunner([repoViewJson(), pullRequestCheckoutTargetJson()]);
|
||||
const service = createGitHubService({
|
||||
runner: runner.runner,
|
||||
resolveGhPath: async () => "/usr/bin/gh",
|
||||
now: () => 100,
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.getPullRequestCheckoutTarget?.({ cwd: "/repo", number: 526 }),
|
||||
).resolves.toEqual({
|
||||
number: 526,
|
||||
baseRefName: "main",
|
||||
headRefName: "main",
|
||||
headOwnerLogin: "therainisme",
|
||||
headRepositorySshUrl: "git@github.com:therainisme/paseo.git",
|
||||
headRepositoryUrl: "https://github.com/therainisme/paseo",
|
||||
isCrossRepository: true,
|
||||
});
|
||||
|
||||
expect(runner.calls).toHaveLength(2);
|
||||
expect(runner.calls[0]).toEqual({
|
||||
cwd: "/repo",
|
||||
args: ["repo", "view", "--json", "owner,name,parent"],
|
||||
});
|
||||
expect(runner.calls[1]?.cwd).toBe("/repo");
|
||||
expect(runner.calls[1]?.args.slice(0, 3)).toEqual(["api", "graphql", "-f"]);
|
||||
expect(runner.calls[1]?.args).toContain("owner=getpaseo");
|
||||
expect(runner.calls[1]?.args).toContain("name=paseo");
|
||||
expect(runner.calls[1]?.args).toContain("number=526");
|
||||
});
|
||||
|
||||
it("polls PR status at fast cadence while checks are pending", async () => {
|
||||
let now = 0;
|
||||
const runner = createRunner([
|
||||
|
||||
@@ -185,6 +185,53 @@ const GitHubRepoViewSchema = z.object({
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const PullRequestCheckoutTargetSchema = z.object({
|
||||
data: z.object({
|
||||
repository: z.object({
|
||||
pullRequest: z
|
||||
.object({
|
||||
number: z.number(),
|
||||
baseRefName: z.string().catch(""),
|
||||
headRefName: z.string().catch(""),
|
||||
isCrossRepository: z.boolean().catch(false),
|
||||
headRepositoryOwner: z
|
||||
.object({
|
||||
login: z.string().catch(""),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
headRepository: z
|
||||
.object({
|
||||
sshUrl: z.string().nullable().optional(),
|
||||
url: z.string().nullable().optional(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
})
|
||||
.nullable(),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
const PULL_REQUEST_CHECKOUT_TARGET_QUERY = `
|
||||
query PullRequestCheckoutTarget($owner: String!, $name: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
pullRequest(number: $number) {
|
||||
number
|
||||
baseRefName
|
||||
headRefName
|
||||
isCrossRepository
|
||||
headRepositoryOwner {
|
||||
login
|
||||
}
|
||||
headRepository {
|
||||
sshUrl
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const CURRENT_PR_STATUS_FIELDS =
|
||||
"number,url,title,state,isDraft,baseRefName,headRefName,mergedAt,statusCheckRollup,reviewDecision,headRepositoryOwner";
|
||||
|
||||
@@ -266,6 +313,16 @@ export interface GitHubPullRequestSummary {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface GitHubPullRequestCheckoutTarget {
|
||||
number: number;
|
||||
baseRefName: string;
|
||||
headRefName: string;
|
||||
headOwnerLogin: string | null;
|
||||
headRepositorySshUrl: string | null;
|
||||
headRepositoryUrl: string | null;
|
||||
isCrossRepository: boolean;
|
||||
}
|
||||
|
||||
export interface GitHubIssueSummary {
|
||||
number: number;
|
||||
title: string;
|
||||
@@ -417,6 +474,9 @@ export interface GitHubService {
|
||||
listIssues(options: ListGitHubIssuesOptions): Promise<GitHubIssueSummary[]>;
|
||||
getPullRequest(options: GetGitHubPullRequestOptions): Promise<GitHubPullRequestSummary>;
|
||||
getPullRequestHeadRef(options: GetGitHubPullRequestOptions): Promise<string>;
|
||||
getPullRequestCheckoutTarget?(
|
||||
options: GetGitHubPullRequestOptions,
|
||||
): Promise<GitHubPullRequestCheckoutTarget>;
|
||||
getCurrentPullRequestStatus(
|
||||
options: {
|
||||
cwd: string;
|
||||
@@ -756,6 +816,40 @@ export function createGitHubService(options: CreateGitHubServiceOptions = {}): G
|
||||
return pullRequest.headRefName;
|
||||
},
|
||||
|
||||
getPullRequestCheckoutTarget(options) {
|
||||
return cached({
|
||||
cwd: options.cwd,
|
||||
method: "getPullRequestCheckoutTarget",
|
||||
args: { number: options.number },
|
||||
readOptions: options,
|
||||
load: async () => {
|
||||
const repo = await getGitHubRepoView({ cwd: options.cwd, run });
|
||||
const owner = repo?.owner?.login;
|
||||
const name = repo?.name;
|
||||
if (!owner || !name) {
|
||||
throw new Error("Unable to resolve GitHub repository for pull request checkout");
|
||||
}
|
||||
|
||||
const stdout = await run(
|
||||
[
|
||||
"api",
|
||||
"graphql",
|
||||
"-f",
|
||||
`query=${PULL_REQUEST_CHECKOUT_TARGET_QUERY}`,
|
||||
"-F",
|
||||
`owner=${owner}`,
|
||||
"-F",
|
||||
`name=${name}`,
|
||||
"-F",
|
||||
`number=${options.number}`,
|
||||
],
|
||||
{ cwd: options.cwd },
|
||||
);
|
||||
return parsePullRequestCheckoutTarget(stdout);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
getCurrentPullRequestStatus(options) {
|
||||
return cached({
|
||||
cwd: options.cwd,
|
||||
@@ -1364,6 +1458,23 @@ function parsePullRequestSummary(stdout: string): GitHubPullRequestSummary {
|
||||
return toPullRequestSummary(GitHubPullRequestSummarySchema.parse(JSON.parse(stdout || "{}")));
|
||||
}
|
||||
|
||||
function parsePullRequestCheckoutTarget(stdout: string): GitHubPullRequestCheckoutTarget {
|
||||
const parsed = PullRequestCheckoutTargetSchema.parse(JSON.parse(stdout || "{}"));
|
||||
const pullRequest = parsed.data.repository.pullRequest;
|
||||
if (!pullRequest) {
|
||||
throw new Error("Pull request not found");
|
||||
}
|
||||
return {
|
||||
number: pullRequest.number,
|
||||
baseRefName: pullRequest.baseRefName,
|
||||
headRefName: pullRequest.headRefName,
|
||||
headOwnerLogin: pullRequest.headRepositoryOwner?.login || null,
|
||||
headRepositorySshUrl: pullRequest.headRepository?.sshUrl || null,
|
||||
headRepositoryUrl: pullRequest.headRepository?.url || null,
|
||||
isCrossRepository: pullRequest.isCrossRepository,
|
||||
};
|
||||
}
|
||||
|
||||
function toPullRequestSummary(
|
||||
item: z.infer<typeof GitHubPullRequestSummarySchema>,
|
||||
): GitHubPullRequestSummary {
|
||||
|
||||
Reference in New Issue
Block a user