From e5618b216bbe9df799045ffac66f0d5ccb38c641 Mon Sep 17 00:00:00 2001 From: cb-anomitromunshi Date: Wed, 12 Aug 2026 22:39:23 +0530 Subject: [PATCH 1/2] Avoid Administration-gated branchProtectionRule in GraphQL fetch. The app token lacks Administration, so that field returns FORBIDDEN and octokit.graphql aborts despite usable Ref.rules data. Use rules plus refUpdateRule, and read GraphQL via POST so partial errors are ignored. Co-authored-by: Cursor --- apps/github.js | 64 ++++++++++++++++++++++++++++++-------------------- dist/index.js | 64 ++++++++++++++++++++++++++++++-------------------- 2 files changed, 78 insertions(+), 50 deletions(-) diff --git a/apps/github.js b/apps/github.js index 85da145..77aae16 100644 --- a/apps/github.js +++ b/apps/github.js @@ -1,38 +1,52 @@ 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). - // Query staging/* refs and treat as protected if classic branchProtectionRule or - // any active repo/org ruleset rule (Ref.rules) applies. + // Do not query branchProtectionRule: it requires Administration and the Distributed + // Git Flow app only has Contents / Metadata / Pull Requests. Use Ref.rules (active + // repo/org rulesets) and refUpdateRule (classic protection visible without admin). + // POST /graphql via request so partial field errors do not abort the whole page. 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 - } - rules(first: 1) { - totalCount - } - } + const query = `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 + refUpdateRule { + pattern + } + rules(first: 1) { + totalCount } } - }`, - { owner, name: repo, cursor } - ) + } + } + }` + + while (true) { + const response = await context.octokit.request("POST /graphql", { + query, + variables: { owner, name: repo, cursor } + }) + const repository = response.data && response.data.data && response.data.data.repository + if (!repository || !repository.refs) { + const messages = ((response.data && response.data.errors) || []) + .map((error) => error.message) + .join("; ") + throw new Error(`Failed to list staging refs via GraphQL: ${messages || "unknown error"}`) + } for (const ref of repository.refs.nodes) { - if (ref.branchProtectionRule || ref.rules.totalCount > 0) { + if (!ref) { + continue + } + const hasRulesetProtection = ref.rules && ref.rules.totalCount > 0 + if (ref.refUpdateRule || hasRulesetProtection) { branchNames.push(ref.name) } } diff --git a/dist/index.js b/dist/index.js index bb4b4fe..82a48d4 100644 --- a/dist/index.js +++ b/dist/index.js @@ -30,38 +30,52 @@ 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). - // Query staging/* refs and treat as protected if classic branchProtectionRule or - // any active repo/org ruleset rule (Ref.rules) applies. + // Do not query branchProtectionRule: it requires Administration and the Distributed + // Git Flow app only has Contents / Metadata / Pull Requests. Use Ref.rules (active + // repo/org rulesets) and refUpdateRule (classic protection visible without admin). + // POST /graphql via request so partial field errors do not abort the whole page. 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 - } - rules(first: 1) { - totalCount - } - } + const query = `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 + refUpdateRule { + pattern + } + rules(first: 1) { + totalCount } } - }`, - { owner, name: repo, cursor } - ) + } + } + }` + + while (true) { + const response = await context.octokit.request("POST /graphql", { + query, + variables: { owner, name: repo, cursor } + }) + const repository = response.data && response.data.data && response.data.data.repository + if (!repository || !repository.refs) { + const messages = ((response.data && response.data.errors) || []) + .map((error) => error.message) + .join("; ") + throw new Error(`Failed to list staging refs via GraphQL: ${messages || "unknown error"}`) + } for (const ref of repository.refs.nodes) { - if (ref.branchProtectionRule || ref.rules.totalCount > 0) { + if (!ref) { + continue + } + const hasRulesetProtection = ref.rules && ref.rules.totalCount > 0 + if (ref.refUpdateRule || hasRulesetProtection) { branchNames.push(ref.name) } } From 662bad2ca360b834a4e241ed4334b4beb00d34e1 Mon Sep 17 00:00:00 2001 From: cb-anomitromunshi Date: Wed, 12 Aug 2026 22:58:42 +0530 Subject: [PATCH 2/2] Detect classic and ruleset protection via per-branch REST checks. List staging/* names with GraphQL, then use GET /branches/{branch}.protected so both classic branch protection and rulesets work without Administration. Co-authored-by: Cursor --- apps/github.js | 36 +++++++++++++++++++----------------- dist/index.js | 36 +++++++++++++++++++----------------- 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/apps/github.js b/apps/github.js index 77aae16..59294f5 100644 --- a/apps/github.js +++ b/apps/github.js @@ -1,10 +1,22 @@ 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). - // Do not query branchProtectionRule: it requires Administration and the Distributed - // Git Flow app only has Contents / Metadata / Pull Requests. Use Ref.rules (active - // repo/org rulesets) and refUpdateRule (classic protection visible without admin). - // POST /graphql via request so partial field errors do not abort the whole page. + // Avoid REST GET /branches?protected=true: on large repos GitHub evaluates protection + // across every branch and can 504. Instead: + // 1) GraphQL lists staging/* ref names (no Administration-gated fields) + // 2) REST GET /branches/{branch} checks `protected` for classic rules and rulesets + const stagingBranchNames = await listStagingBranchNames(context) + const protectedBranchNames = [] + + for (const branchName of stagingBranchNames) { + const branch = await context.octokit.repos.getBranch(context.repo({ branch: branchName })) + if (branch.data.protected) { + protectedBranchNames.push(branchName) + } + } + + return protectedBranchNames +} + +async function listStagingBranchNames(context) { const { owner, repo } = context.repo() const branchNames = [] let cursor = null @@ -17,12 +29,6 @@ async function fetchProtectedBranchNames(context) { } nodes { name - refUpdateRule { - pattern - } - rules(first: 1) { - totalCount - } } } } @@ -42,11 +48,7 @@ async function fetchProtectedBranchNames(context) { } for (const ref of repository.refs.nodes) { - if (!ref) { - continue - } - const hasRulesetProtection = ref.rules && ref.rules.totalCount > 0 - if (ref.refUpdateRule || hasRulesetProtection) { + if (ref && ref.name && ref.name.startsWith("staging/")) { branchNames.push(ref.name) } } diff --git a/dist/index.js b/dist/index.js index 82a48d4..ca0abbd 100644 --- a/dist/index.js +++ b/dist/index.js @@ -28,12 +28,24 @@ module.exports = (app) => { /***/ ((module) => { 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). - // Do not query branchProtectionRule: it requires Administration and the Distributed - // Git Flow app only has Contents / Metadata / Pull Requests. Use Ref.rules (active - // repo/org rulesets) and refUpdateRule (classic protection visible without admin). - // POST /graphql via request so partial field errors do not abort the whole page. + // Avoid REST GET /branches?protected=true: on large repos GitHub evaluates protection + // across every branch and can 504. Instead: + // 1) GraphQL lists staging/* ref names (no Administration-gated fields) + // 2) REST GET /branches/{branch} checks `protected` for classic rules and rulesets + const stagingBranchNames = await listStagingBranchNames(context) + const protectedBranchNames = [] + + for (const branchName of stagingBranchNames) { + const branch = await context.octokit.repos.getBranch(context.repo({ branch: branchName })) + if (branch.data.protected) { + protectedBranchNames.push(branchName) + } + } + + return protectedBranchNames +} + +async function listStagingBranchNames(context) { const { owner, repo } = context.repo() const branchNames = [] let cursor = null @@ -46,12 +58,6 @@ async function fetchProtectedBranchNames(context) { } nodes { name - refUpdateRule { - pattern - } - rules(first: 1) { - totalCount - } } } } @@ -71,11 +77,7 @@ async function fetchProtectedBranchNames(context) { } for (const ref of repository.refs.nodes) { - if (!ref) { - continue - } - const hasRulesetProtection = ref.rules && ref.rules.totalCount > 0 - if (ref.refUpdateRule || hasRulesetProtection) { + if (ref && ref.name && ref.name.startsWith("staging/")) { branchNames.push(ref.name) } }