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
70 changes: 43 additions & 27 deletions apps/github.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,54 @@
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.
// 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)
}
}
Comment thread
cb-jarunmadhesh marked this conversation as resolved.

return protectedBranchNames
}

async function listStagingBranchNames(context) {
const { owner, repo } = context.repo()
const branchNames = []
let cursor = null
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
}
}
}
}`

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
}
}
}
}
}`,
{ owner, name: repo, cursor }
)
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 && ref.name && ref.name.startsWith("staging/")) {
branchNames.push(ref.name)
}
}
Expand Down
70 changes: 43 additions & 27 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,40 +28,56 @@ 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).
// Query staging/* refs and treat as protected if classic branchProtectionRule or
// any active repo/org ruleset rule (Ref.rules) applies.
// 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
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
}
}
}
}`

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
}
}
}
}
}`,
{ owner, name: repo, cursor }
)
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 && ref.name && ref.name.startsWith("staging/")) {
branchNames.push(ref.name)
}
}
Expand Down