Repository: Azure/azure-rest-api-specs
Pull request #33420: [context.js] Fallback to search API to find PR from head_sha
Author: login: mikeharder
Created: 2025-03-22T10:01:47Z | Merged: 2025-03-23T05:50:05Z

- Fixes #33415
- Fixes #33416
- Fixes #33417

Verification: https://github.com/Azure/azure-rest-api-specs/actions/runs/14016084388/job/39241926369#step:3:26

Linked issue #33415: [context.js] Cannot find PR for commit, if PR from default branch of fork
For most fork pull requests, you can find the associated PR by calling the following API on the fork repo:

```
https://api.github.com/repos/OWNER/REPO/commits/HEAD_SHA/pulls
```

However, there appears to be one edge case where this doesn't work, and you can't use this rest API to find the PR, either in the main repo or the fork repo.

If the fork PR is from the **default** branch of the fork repo.  This API will always return `[]`, even if there is an open PR from the fork to the main repo.

This isn't documented anywhere clearly and explicitly.  But it might be inferred from a very close reading of the offical docs [1]:

> Lists the merged pull request that introduced the commit to the repository. **If the commit is not present in the default branch**, will only return open pull requests associated with the commit.

The commit is present in the default branch, so the second sentence doesn't apply.

If the commit were merged to the fork via a PR, the API might even return **that** PR in the fork repo, rather than a PR in the main repo, which would be even worse than returning empty.

If the commit were merged to the fork via a direct push (most common), there is no merged PR associated with the commit either, so the API must return empty.

If the rest API in the fork repo returns nothing (or the wrong PR **into** the fork repo), the fallback is to use the search API in the main repo.  This API call is more expensive, but it should always return the PR associated with a commit correctly.

https://github.com/Azure/azure-rest-api-specs/blob/ebbcd8996243bfe6b8959545e629dc250067bc49/.github/workflows/src/context.js#L132-L149

https://github.com/Azure/azure-rest-api-specs/blob/ebbcd8996243bfe6b8959545e629dc250067bc49/.github/workflows/src/sdk-breaking-change-labels.js#L122-L125

https://github.com/Azure/azure-rest-api-specs/blob/ebbcd8996243bfe6b8959545e629dc250067bc49/.github/workflows/src/issues.js#L1-L9

Related, I believe our search helper should be updated to include the repo owner and name, to significantly reduce the search space.

https://github.com/Azure/azure-rest-api-specs/blob/ebbcd8996243bfe6b8959545e629dc250067bc49/.github/workflows/src/issues.js#L19-L21

https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests#search-within-a-users-or-organizations-repositories

It should also **remove** `state:open`, since there's no reason to exclude clsoed issues.

[1] https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#list-pull-requests-associated-with-a-commit

### Recent Example
PR: https://github.com/Azure/azure-rest-api-specs/pull/33298

Failed WF: https://github.com/Azure/azure-rest-api-specs/actions/runs/14000120985/job/39220701040#step:3:147

Commit into fork: https://api.github.com/repos/TheDonLuke/azure-rest-api-specs/commits/25024f0a9ed558e53715e6e3709ee9387328b3d9

commits/pulls rest API (returns empty): https://api.github.com/repos/TheDonLuke/azure-rest-api-specs/commits/25024f0a9ed558e53715e6e3709ee9387328b3d9/pulls

search API (returns PR): https://api.github.com/search/issues?q=25024f0a9ed558e53715e6e3709ee9387328b3d9+type:pr+repo:azure/azure-rest-api-specs


Diff:
diff --git a/.github/workflows/src/context.js b/.github/workflows/src/context.js
index 3a9730e7dd12..888e60808303 100644
--- a/.github/workflows/src/context.js
+++ b/.github/workflows/src/context.js
@@ -1,6 +1,7 @@
 // @ts-check
 
 import { PER_PAGE_MAX } from "./github.js";
+import { getIssueNumber } from "./issues.js";
 
 /**
  * Extracts inputs from context based on event name and properties.
@@ -95,7 +96,7 @@ export async function extractInputs(github, context, core) {
         context.payload
       );
 
-    let issue_number;
+    let issue_number = NaN;
 
     if (
       payload.workflow_run.event === "pull_request" ||
@@ -139,14 +140,31 @@ export async function extractInputs(github, context, core) {
           },
         );
 
-        if (pullRequests.length === 1) {
+        if (pullRequests.length === 0) {
+          // There are three cases where the "commits" REST API called above can return
+          // empty, even if there is an open PR from the commit:
+          //
+          // 1. If the head branch of a fork PR is the default branch of the fork repo, the
+          //    API always returns empty. (#33315)
+          // 2. If a PR was just merged, the API may return empty for a brief window (#33416).
+          // 3. The API may fail occasionally for no known reason (#33417).
+          //
+          // In any case, the solution is to fall back to the (lower-rate-limit) search API.
+          // The search API is confirmed to work in case #1, but has not been tested in #2 or #3.
+          issue_number = (await getIssueNumber({ head_sha, github, core }))
+            .issueNumber;
+        } else if (pullRequests.length === 1) {
           issue_number = pullRequests[0].number;
         } else {
-          // TODO: Consider calling search API in target repo if we get an unexpected result from the head repo (#33005)
           throw new Error(
             `Unexpected number of pull requests associated with commit '${head_sha}'. Expected: '1'. Actual: '${pullRequests.length}'.`,
           );
         }
+        if (!issue_number) {
+          core.info(
+            `Could not find PR for ${head_sha} in ${head_owner}:${head_repo} from either the "commits" or "search" REST APIs`,
+          );
+        }
       }
     } else if (
       payload.workflow_run.event === "issue_comment" ||
@@ -195,7 +213,6 @@ export async function extractInputs(github, context, core) {
         core.info(
           `Could not find 'issue-number' artifact, which is required to associate the triggering workflow run with a PR`,
         );
-        issue_number = NaN;
       }
     } else {
       throw new Error(
diff --git a/.github/workflows/test/context.test.js b/.github/workflows/test/context.test.js
index 432c2c624181..0ea09bd0bb7c 100644
--- a/.github/workflows/test/context.test.js
+++ b/.github/workflows/test/context.test.js
@@ -236,9 +236,40 @@ describe("extractInputs", () => {
         },
       );
 
-      const inputsPromise = extractInputs(github, context, createMockCore());
-      if (numPullRequests === 1) {
-        await expect(inputsPromise).resolves.toEqual({
+      if (numPullRequests === 0) {
+        github.rest.search.issuesAndPullRequests.mockResolvedValue({
+          data: { total_count: 0, items: [] },
+        });
+
+        await expect(
+          extractInputs(github, context, createMockCore()),
+        ).resolves.toEqual({
+          owner: "TestRepoOwnerLogin",
+          repo: "TestRepoName",
+          head_sha: "abc123",
+          issue_number: NaN,
+          run_id: 456,
+        });
+
+        github.rest.search.issuesAndPullRequests.mockResolvedValue({
+          data: { total_count: 1, items: [{ number: 789 }] },
+        });
+
+        await expect(
+          extractInputs(github, context, createMockCore()),
+        ).resolves.toEqual({
+          owner: "TestRepoOwnerLogin",
+          repo: "TestRepoName",
+          head_sha: "abc123",
+          issue_number: 789,
+          run_id: 456,
+        });
+
+        expect(github.rest.search.issuesAndPullRequests).toHaveBeenCalled();
+      } else if (numPullRequests === 1) {
+        await expect(
+          extractInputs(github, context, createMockCore()),
+        ).resolves.toEqual({
           owner: "TestRepoOwnerLogin",
           repo: "TestRepoName",
           head_sha: "abc123",
@@ -246,7 +277,9 @@ describe("extractInputs", () => {
           run_id: 456,
         });
       } else {
-        await expect(inputsPromise).rejects.toThrow();
+        await expect(
+          extractInputs(github, context, createMockCore()),
+        ).rejects.toThrow("Unexpected number of pull requests");
       }
 
       expect(


Reviews:
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T05:11:32Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T05:11:47Z

Review comments:
diff_hunk: @@ -101,6 +101,7 @@ export async function updateLabelsImpl({
     (labelsToAdd.length > 0 || labelsToRemove.length > 0) &&
     Number.isNaN(issue_number)
   ) {
+    // TODO: Add call to search API
path: .github/workflows/src/update-labels.js
user: login: mikeharder
body: ```suggestion
```
created_at: 2025-03-23T05:11:32Z
author_association: MEMBER
diff_hunk: @@ -144,6 +144,8 @@ export async function getLabelActionImpl({
     }
   }
 
+  // TODO: If issue_number is empty, call issues:getIssueNumber() to find it (or move into context for m
+
path: .github/workflows/src/arm-auto-signoff-preview.js
user: login: mikeharder
body: ```suggestion
```
created_at: 2025-03-23T05:11:47Z
author_association: MEMBER

PR comments:
user: login: openapi-pipeline-app[bot]
created_at: 2025-03-22T10:01:53Z
body: <h2>Next Steps to Merge</h2>Next steps that must be taken to merge this PR: <br/><ul><li>❌ The required check named <code>Protected Files</code> has failed. Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the <a href="https://aka.ms/ci-fix">aka.ms/ci-fix</a> guide</li></ul>
author_association: NONE
user: login: openapi-pipeline-app[bot]
created_at: 2025-03-22T10:01:54Z
body: PR validation pipeline restarted successfully. If there is ApiView generated, it will be updated in this comment.
author_association: NONE