Merge remote-tracking branch 'origin/main' into pr58-sync

# Conflicts:
#	packages/server/src/shared/messages.ts
This commit is contained in:
Mohamed Boudra
2026-02-18 18:19:41 +07:00
39 changed files with 2179 additions and 257 deletions
@@ -471,6 +471,109 @@ describe("checkout git utilities", () => {
}
});
it("returns merged PR status when no open PR exists for the current branch", async () => {
execSync("git checkout -b feature", { cwd: repoDir });
execSync("git remote add origin https://github.com/getpaseo/paseo.git", { cwd: repoDir });
const fakeBinDir = join(tempDir, "fake-bin-gh-merged");
mkdirSync(fakeBinDir);
const gitPath = execSync("command -v git", { stdio: "pipe" }).toString().trim();
symlinkSync(gitPath, join(fakeBinDir, "git"));
writeFileSync(
join(fakeBinDir, "gh"),
[
"#!/usr/bin/env bash",
"set -euo pipefail",
"if [[ \"${1-}\" == \"--version\" ]]; then",
" echo \"gh version 2.0.0\"",
" exit 0",
"fi",
"args=\"$*\"",
"if [[ \"$args\" == *\"state=open\"* ]]; then",
" echo \"[]\"",
" exit 0",
"fi",
"if [[ \"$args\" == *\"state=closed\"* ]]; then",
" echo '[{\"html_url\":\"https://github.com/getpaseo/paseo/pull/123\",\"title\":\"Ship feature\",\"state\":\"closed\",\"merged_at\":\"2026-02-18T00:00:00Z\",\"base\":{\"ref\":\"main\"},\"head\":{\"ref\":\"feature\"}}]'",
" exit 0",
"fi",
"echo \"unexpected gh args: $args\" >&2",
"exit 1",
"",
].join("\n"),
"utf8"
);
execSync(`chmod +x ${join(fakeBinDir, "gh")}`);
const originalPath = process.env.PATH;
process.env.PATH = `${fakeBinDir}:${originalPath ?? ""}`;
try {
const status = await getPullRequestStatus(repoDir);
expect(status.githubFeaturesEnabled).toBe(true);
expect(status.status).not.toBeNull();
expect(status.status?.url).toContain("/pull/123");
expect(status.status?.baseRefName).toBe("main");
expect(status.status?.headRefName).toBe("feature");
expect(status.status?.isMerged).toBe(true);
expect(status.status?.state).toBe("merged");
} finally {
if (originalPath === undefined) {
delete process.env.PATH;
} else {
process.env.PATH = originalPath;
}
}
});
it("does not treat closed-unmerged PRs as shipped status", async () => {
execSync("git checkout -b feature", { cwd: repoDir });
execSync("git remote add origin https://github.com/getpaseo/paseo.git", { cwd: repoDir });
const fakeBinDir = join(tempDir, "fake-bin-gh-closed");
mkdirSync(fakeBinDir);
const gitPath = execSync("command -v git", { stdio: "pipe" }).toString().trim();
symlinkSync(gitPath, join(fakeBinDir, "git"));
writeFileSync(
join(fakeBinDir, "gh"),
[
"#!/usr/bin/env bash",
"set -euo pipefail",
"if [[ \"${1-}\" == \"--version\" ]]; then",
" echo \"gh version 2.0.0\"",
" exit 0",
"fi",
"args=\"$*\"",
"if [[ \"$args\" == *\"state=open\"* ]]; then",
" echo \"[]\"",
" exit 0",
"fi",
"if [[ \"$args\" == *\"state=closed\"* ]]; then",
" echo '[{\"html_url\":\"https://github.com/getpaseo/paseo/pull/999\",\"title\":\"Closed without merge\",\"state\":\"closed\",\"merged_at\":null,\"base\":{\"ref\":\"main\"},\"head\":{\"ref\":\"feature\"}}]'",
" exit 0",
"fi",
"echo \"unexpected gh args: $args\" >&2",
"exit 1",
"",
].join("\n"),
"utf8"
);
execSync(`chmod +x ${join(fakeBinDir, "gh")}`);
const originalPath = process.env.PATH;
process.env.PATH = `${fakeBinDir}:${originalPath ?? ""}`;
try {
const status = await getPullRequestStatus(repoDir);
expect(status.githubFeaturesEnabled).toBe(true);
expect(status.status).toBeNull();
} finally {
if (originalPath === undefined) {
delete process.env.PATH;
} else {
process.env.PATH = originalPath;
}
}
});
it("returns typed MergeConflictError on merge conflicts", async () => {
const conflictFile = join(repoDir, "conflict.txt");
writeFileSync(conflictFile, "base\n");
+103 -25
View File
@@ -1517,6 +1517,7 @@ export interface PullRequestStatus {
state: string;
baseRefName: string;
headRefName: string;
isMerged: boolean;
}
export interface PullRequestStatusResult {
@@ -1592,6 +1593,62 @@ async function resolveGitHubRepo(cwd: string): Promise<string | null> {
return null;
}
async function listPullRequestsForHead(options: {
cwd: string;
repo: string;
owner: string;
head: string;
state: "open" | "closed";
}): Promise<any[]> {
const { stdout } = await execFileAsync(
"gh",
[
"api",
`repos/${options.repo}/pulls`,
"-X",
"GET",
"-F",
`head=${options.owner}:${options.head}`,
"-F",
`state=${options.state}`,
],
{ cwd: options.cwd }
);
const parsed = JSON.parse(stdout.trim());
return Array.isArray(parsed) ? parsed : [];
}
function buildPullRequestStatus(current: any, fallbackHead: string): PullRequestStatus | null {
if (!current || typeof current !== "object") {
return null;
}
const url = current.html_url ?? current.url;
const title = current.title;
if (typeof url !== "string" || typeof title !== "string" || !url || !title) {
return null;
}
const mergedAt =
typeof current.merged_at === "string" && current.merged_at.trim().length > 0
? current.merged_at
: null;
const state =
mergedAt !== null
? "merged"
: typeof current.state === "string" && current.state.trim().length > 0
? current.state
: "";
return {
url,
title,
state,
baseRefName: current.base?.ref ?? "",
headRefName: current.head?.ref ?? fallbackHead,
isMerged: mergedAt !== null,
};
}
export async function createPullRequest(
cwd: string,
options: CreatePullRequestOptions
@@ -1652,22 +1709,15 @@ export async function getPullRequestStatus(cwd: string): Promise<PullRequestStat
};
}
const owner = repo.split("/")[0];
let stdout: string;
let openPulls: any[];
try {
({ stdout } = await execFileAsync(
"gh",
[
"api",
`repos/${repo}/pulls`,
"-X",
"GET",
"-F",
`head=${owner}:${head}`,
"-F",
"state=open",
],
{ cwd }
));
openPulls = await listPullRequestsForHead({
cwd,
repo,
owner,
head,
state: "open",
});
} catch (error) {
if (isGhAuthError(error)) {
return {
@@ -1677,22 +1727,50 @@ export async function getPullRequestStatus(cwd: string): Promise<PullRequestStat
}
throw error;
}
const parsed = JSON.parse(stdout.trim());
const current = Array.isArray(parsed) && parsed.length > 0 ? parsed[0] : null;
if (!current) {
const openPull = openPulls[0] ?? null;
const openStatus = buildPullRequestStatus(openPull, head);
if (openStatus) {
return {
status: openStatus,
githubFeaturesEnabled: true,
};
}
let closedPulls: any[];
try {
closedPulls = await listPullRequestsForHead({
cwd,
repo,
owner,
head,
state: "closed",
});
} catch (error) {
if (isGhAuthError(error)) {
return {
status: null,
githubFeaturesEnabled: false,
};
}
throw error;
}
const mergedClosedPull =
closedPulls.find(
(entry) =>
entry &&
typeof entry === "object" &&
typeof entry.merged_at === "string" &&
entry.merged_at.trim().length > 0
) ?? null;
const mergedStatus = buildPullRequestStatus(mergedClosedPull, head);
if (!mergedStatus) {
return {
status: null,
githubFeaturesEnabled: true,
};
}
return {
status: {
url: current.html_url ?? current.url,
title: current.title,
state: current.state,
baseRefName: current.base?.ref ?? "",
headRefName: current.head?.ref ?? head,
},
status: mergedStatus,
githubFeaturesEnabled: true,
};
}