From b4564f4b4b9c8798388a6105d3384fea053e4c07 Mon Sep 17 00:00:00 2001 From: cb-anomitromunshi Date: Wed, 12 Aug 2026 18:13:23 +0530 Subject: [PATCH 1/2] Fetch protected staging branches via GraphQL instead of REST. Avoids GitHub 504s on large repos where GET /branches?protected=true times out while evaluating protection across thousands of branches. Co-authored-by: Cursor --- apps/github.js | 43 +++++++++++++++++++++++++++++++++++++++++-- dist/index.js | 43 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/apps/github.js b/apps/github.js index 97d2303..7fd1b3a 100644 --- a/apps/github.js +++ b/apps/github.js @@ -1,6 +1,45 @@ async function fetchProtectedBranchNames(context) { - let branches = await context.octokit.repos.listBranches(context.repo({protected : true, per_page : 100, request: { timeout: 120_000 }})) - return branches.data.map(branch => branch.name) + // Prefer GraphQL over REST GET /branches?protected=true. On large repos the REST + // filter evaluates protection across every branch and can 504 (~10s gateway limit). + // Querying staging/* refs and reading branchProtectionRule avoids that scan. + const { owner, repo } = context.repo() + const branchNames = [] + let cursor = null + + while (true) { + const { repository } = await context.octokit.graphql( + `query($owner: String!, $name: String!, $cursor: String) { + repository(owner: $owner, name: $name) { + refs(refPrefix: "refs/heads/", query: "staging/", first: 100, after: $cursor) { + pageInfo { + hasNextPage + endCursor + } + nodes { + name + branchProtectionRule { + id + } + } + } + } + }`, + { owner, name: repo, cursor } + ) + + for (const ref of repository.refs.nodes) { + if (ref.branchProtectionRule) { + branchNames.push(ref.name) + } + } + + if (!repository.refs.pageInfo.hasNextPage) { + break + } + cursor = repository.refs.pageInfo.endCursor + } + + return branchNames } async function createPr(context, from, to, title) { diff --git a/dist/index.js b/dist/index.js index de6ae26..dab2bc7 100644 --- a/dist/index.js +++ b/dist/index.js @@ -28,8 +28,47 @@ module.exports = (app) => { /***/ ((module) => { async function fetchProtectedBranchNames(context) { - let branches = await context.octokit.repos.listBranches(context.repo({protected : true, per_page : 100, request: { timeout: 120_000 }})) - return branches.data.map(branch => branch.name) + // Prefer GraphQL over REST GET /branches?protected=true. On large repos the REST + // filter evaluates protection across every branch and can 504 (~10s gateway limit). + // Querying staging/* refs and reading branchProtectionRule avoids that scan. + const { owner, repo } = context.repo() + const branchNames = [] + let cursor = null + + while (true) { + const { repository } = await context.octokit.graphql( + `query($owner: String!, $name: String!, $cursor: String) { + repository(owner: $owner, name: $name) { + refs(refPrefix: "refs/heads/", query: "staging/", first: 100, after: $cursor) { + pageInfo { + hasNextPage + endCursor + } + nodes { + name + branchProtectionRule { + id + } + } + } + } + }`, + { owner, name: repo, cursor } + ) + + for (const ref of repository.refs.nodes) { + if (ref.branchProtectionRule) { + branchNames.push(ref.name) + } + } + + if (!repository.refs.pageInfo.hasNextPage) { + break + } + cursor = repository.refs.pageInfo.endCursor + } + + return branchNames } async function createPr(context, from, to, title) { From 51ff54b8f4f46d2797ec8c8e2beb8c694a8b6b01 Mon Sep 17 00:00:00 2001 From: cb-anomitromunshi Date: Wed, 12 Aug 2026 21:12:15 +0530 Subject: [PATCH 2/2] Include ruleset-protected staging branches in the GraphQL fetch. branchProtectionRule only covers classic protection; Ref.rules also picks up active repository and organization rulesets. Co-authored-by: Cursor --- apps/github.js | 8 ++++++-- dist/index.js | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/github.js b/apps/github.js index 7fd1b3a..85da145 100644 --- a/apps/github.js +++ b/apps/github.js @@ -1,7 +1,8 @@ async function fetchProtectedBranchNames(context) { // Prefer GraphQL over REST GET /branches?protected=true. On large repos the REST // filter evaluates protection across every branch and can 504 (~10s gateway limit). - // Querying staging/* refs and reading branchProtectionRule avoids that scan. + // Query staging/* refs and treat as protected if classic branchProtectionRule or + // any active repo/org ruleset rule (Ref.rules) applies. const { owner, repo } = context.repo() const branchNames = [] let cursor = null @@ -20,6 +21,9 @@ async function fetchProtectedBranchNames(context) { branchProtectionRule { id } + rules(first: 1) { + totalCount + } } } } @@ -28,7 +32,7 @@ async function fetchProtectedBranchNames(context) { ) for (const ref of repository.refs.nodes) { - if (ref.branchProtectionRule) { + if (ref.branchProtectionRule || ref.rules.totalCount > 0) { branchNames.push(ref.name) } } diff --git a/dist/index.js b/dist/index.js index dab2bc7..bb4b4fe 100644 --- a/dist/index.js +++ b/dist/index.js @@ -30,7 +30,8 @@ module.exports = (app) => { async function fetchProtectedBranchNames(context) { // Prefer GraphQL over REST GET /branches?protected=true. On large repos the REST // filter evaluates protection across every branch and can 504 (~10s gateway limit). - // Querying staging/* refs and reading branchProtectionRule avoids that scan. + // Query staging/* refs and treat as protected if classic branchProtectionRule or + // any active repo/org ruleset rule (Ref.rules) applies. const { owner, repo } = context.repo() const branchNames = [] let cursor = null @@ -49,6 +50,9 @@ async function fetchProtectedBranchNames(context) { branchProtectionRule { id } + rules(first: 1) { + totalCount + } } } } @@ -57,7 +61,7 @@ async function fetchProtectedBranchNames(context) { ) for (const ref of repository.refs.nodes) { - if (ref.branchProtectionRule) { + if (ref.branchProtectionRule || ref.rules.totalCount > 0) { branchNames.push(ref.name) } }