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
14 changes: 8 additions & 6 deletions .github/scripts/preview-github.mts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ export function isCurrentPullRequest(
}

// Determine identity exclusively through GitHub's API, never through files
// uploaded by PR code. workflow_run.pull_requests can be empty for fork PRs,
// so resolve associated PRs through the commit API in that case as well.
// uploaded by PR code. Both workflow_run.pull_requests and the commit's PR
// associations can be empty for a fork, including a draft PR. Query current
// open PR heads directly and require an exact repository and SHA match.
export async function findPreview(
github: GitHubClient,
run: SourceRun,
Expand All @@ -63,10 +64,11 @@ export async function findPreview(
if (workflow.path !== ".github/workflows/ci.yml") {
return null;
}
const prs = await github.paginate(
github.rest.repos.listPullRequestsAssociatedWithCommit,
{ ...repo, commit_sha: run.head_sha, per_page: 100 },
);
const prs = await github.paginate(github.rest.pulls.list, {
...repo,
state: "open",
per_page: 100,
});
const candidates = prs.filter((pr) =>
isCurrentPullRequest(pr, run.head_sha, repository),
);
Expand Down
34 changes: 31 additions & 3 deletions .github/scripts/preview-github.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const run: SourceRun = {
const pr = {
number: 42,
state: "open",
draft: true,
head: { sha },
base: { repo: { full_name: "LiveSplit/LiveSplitOne" } },
};
Expand Down Expand Up @@ -60,8 +61,12 @@ function githubFixture(
let result: unknown;
if (url.pathname.endsWith("/actions/workflows/1")) {
result = { path: options.workflow ?? ".github/workflows/ci.yml" };
} else if (url.pathname.endsWith(`/commits/${sha}/pulls`)) {
} else if (url.pathname === `/repos/${repo.owner}/${repo.repo}/pulls`) {
result = options.candidates ?? [pr];
} else if (url.pathname.endsWith(`/commits/${sha}/pulls`)) {
// GitHub returned no associations for the first live fork PR,
// despite its current head and uploaded artifact matching CI.
result = [];
} else if (url.pathname.endsWith("/pulls/42")) {
result = options.current ?? pr;
} else if (url.pathname.endsWith("/actions/runs/9/artifacts")) {
Expand Down Expand Up @@ -106,14 +111,37 @@ function githubFixture(
return { github: new Octokit({ request: { fetch: fetcher } }), calls };
}

test("discovers fork PRs through signed commit metadata without artifact-supplied identity", async () => {
test("discovers a draft fork PR even when its commit has no PR associations", async () => {
const { github, calls } = githubFixture();
assert.deepEqual(await findPreview(github, run, repo), {
number: 42,
sha,
artifact: 7,
});
assert(calls.some((call) => call.path.endsWith(`/commits/${sha}/pulls`)));
assert(
calls.some(
(call) => call.path === `/repos/${repo.owner}/${repo.repo}/pulls`,
),
);
});

test("ignores open PRs with a different head or base repository", async () => {
const { github } = githubFixture({
candidates: [
{ ...pr, number: 43, head: { sha: "b".repeat(40) } },
{
...pr,
number: 44,
base: { repo: { full_name: "other/repo" } },
},
pr,
],
});
assert.deepEqual(await findPreview(github, run, repo), {
number: 42,
sha,
artifact: 7,
});
});

test("ignores runs from other repositories, failed CI, and non-PR events", async () => {
Expand Down
Loading