Skip to content

Commit 35a40a1

Browse files
committed
feat: add GitHub API helpers for organization management and enhance existing functions
1 parent d7e0c44 commit 35a40a1

5 files changed

Lines changed: 173 additions & 390 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,10 @@ All scripts can leverage a shared utility library for common operations like val
804804
**API helpers:**
805805
- `get_repo_page_count <url>` — Get total page count from paginated REST endpoint
806806
- `validate_slug <value> <label>` — Exit if value contains characters other than alphanumeric, hyphen, or underscore
807+
- `gh_api <path|url> [curl args...]` — Bearer-auth REST helper with automatic rate-limit retry (up to 5 attempts); returns `__404__` / `__422__` for those status codes instead of failing
808+
- `_paginate_orgs_endpoint <jq_filter> <url_template>` — Page through an org-list REST endpoint, printing one login per line; use `PAGE` as a placeholder in the URL template
809+
- `_graphql_enterprise_orgs` — Cursor-based GraphQL pagination for all orgs in `ENTERPRISE`; prints one login per line
810+
- `get_enterprise_orgs` — Three-tier enterprise org resolver: tries REST `/enterprises/{slug}/organizations`, falls back to GraphQL, then falls back to `/user/orgs`
807811

808812
### Using the Shared Library in Scripts
809813

enterprise/github-add-enterprise-team-read-permissions/github-add-enterprise-team-read-permissions.sh

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -53,45 +53,6 @@ print_status "Enterprise: ${ENTERPRISE}"
5353
print_status "Enterprise team slug: ${ENTERPRISE_TEAM_SLUG}"
5454
print_status "Org role to assign: ${ALL_REPO_READ_ROLE_NAME}"
5555

56-
###
57-
## Fetch all organization logins in the enterprise via GraphQL (cursor-based pagination)
58-
###
59-
get_enterprise_orgs () {
60-
local orgs=()
61-
local cursor=""
62-
local has_next_page="true"
63-
64-
while [ "${has_next_page}" = "true" ]; do
65-
local after_clause=""
66-
if [ -n "${cursor}" ]; then
67-
after_clause=", after: \\\"${cursor}\\\""
68-
fi
69-
70-
local query="{\"query\":\"{ enterprise(slug: \\\"${ENTERPRISE}\\\") { organizations(first: 100${after_clause}) { nodes { login } pageInfo { hasNextPage endCursor } } } }\"}"
71-
72-
local response
73-
response=$(curl -s \
74-
-H "Authorization: token ${GITHUB_TOKEN}" \
75-
-H "Content-Type: application/json" \
76-
"${API_URL_PREFIX}/graphql" \
77-
-d "${query}")
78-
79-
local batch
80-
batch=$(echo "${response}" | jq -r '.data.enterprise.organizations.nodes[].login // empty' 2>/dev/null)
81-
82-
[ -z "${batch}" ] && break
83-
84-
while IFS= read -r org; do
85-
orgs+=("${org}")
86-
done <<< "${batch}"
87-
88-
has_next_page=$(echo "${response}" | jq -r '.data.enterprise.organizations.pageInfo.hasNextPage')
89-
cursor=$(echo "${response}" | jq -r '.data.enterprise.organizations.pageInfo.endCursor')
90-
done
91-
92-
printf '%s\n' "${orgs[@]}"
93-
}
94-
9556
###
9657
## Look up the numeric role_id for the all_repo_read built-in org role
9758
###

enterprise/github-dockerfile-discovery/github-dockerfile-discovery.sh

Lines changed: 1 addition & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -80,52 +80,6 @@ check_prerequisites() {
8080
print_success "Prerequisites OK"
8181
}
8282

83-
###
84-
## GitHub API helper – handles rate-limit retries
85-
## Usage: gh_api <path_or_url> [extra curl args...]
86-
###
87-
gh_api() {
88-
local url="$1"
89-
shift
90-
# Prepend base URL if path starts with /
91-
[[ "${url}" == http* ]] || url="${API_URL_PREFIX}${url}"
92-
93-
local attempt
94-
for attempt in 1 2 3 4 5; do
95-
local http_code
96-
local body
97-
body=$(curl -s -w "\n%{http_code}" \
98-
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
99-
-H "Accept: application/vnd.github+json" \
100-
-H "X-GitHub-Api-Version: 2022-11-28" \
101-
"$@" "${url}")
102-
http_code=$(echo "${body}" | tail -1)
103-
body=$(echo "${body}" | head -n -1)
104-
105-
if [[ "${http_code}" == "200" ]]; then
106-
echo "${body}"
107-
return 0
108-
elif [[ "${http_code}" == "403" || "${http_code}" == "429" ]]; then
109-
local retry_after=60
110-
print_warning "Rate limited (HTTP ${http_code}). Sleeping ${retry_after}s before retry ${attempt}/5..."
111-
sleep "${retry_after}"
112-
elif [[ "${http_code}" == "404" ]]; then
113-
echo "__404__"
114-
return 0
115-
elif [[ "${http_code}" == "422" ]]; then
116-
# Code search not available for this org
117-
echo "__422__"
118-
return 0
119-
else
120-
print_warning "HTTP ${http_code} for ${url} (attempt ${attempt}/5)"
121-
sleep 5
122-
fi
123-
done
124-
125-
print_error "Failed to GET ${url} after 5 attempts"
126-
return 1
127-
}
128-
12983
###
13084
## gh_api_link – like gh_api but also returns the Link header for pagination
13185
## Writes body to stdout, sets global RESPONSE_LINK
@@ -168,140 +122,6 @@ gh_api_link() {
168122
return 1
169123
}
170124

171-
###
172-
## Validate token
173-
###
174-
validate_token() {
175-
print_status "Validating GitHub token..."
176-
local resp
177-
resp=$(gh_api "/user")
178-
local login
179-
login=$(echo "${resp}" | jq -r '.login // empty')
180-
if [ -z "${login}" ]; then
181-
print_error "GITHUB_TOKEN is invalid or lacks required scopes."
182-
exit 1
183-
fi
184-
print_success "Token validated."
185-
}
186-
187-
###
188-
## _paginate_orgs_endpoint <jq_filter> <url_template_with_PAGE_placeholder>
189-
## Internal helper: paginates an orgs list endpoint, prints one login per line.
190-
###
191-
_paginate_orgs_endpoint() {
192-
local jq_filter="$1"
193-
local url_template="$2" # must contain the literal string PAGE
194-
local page=1
195-
while true; do
196-
local url resp orgs_on_page count
197-
url="${url_template/PAGE/${page}}"
198-
resp=$(gh_api "${url}")
199-
if [[ "${resp}" == "__404__" || "${resp}" == "__422__" || -z "${resp}" ]]; then
200-
break
201-
fi
202-
orgs_on_page=$(echo "${resp}" | jq -r "${jq_filter}" 2>/dev/null || true)
203-
if [ -z "${orgs_on_page}" ]; then
204-
break
205-
fi
206-
echo "${orgs_on_page}"
207-
count=$(echo "${orgs_on_page}" | wc -l)
208-
if [ "${count}" -lt 100 ]; then
209-
break
210-
fi
211-
page=$(( page + 1 ))
212-
done
213-
}
214-
215-
###
216-
## _graphql_enterprise_orgs
217-
## Queries the GraphQL API for enterprise orgs (works for enterprise members,
218-
## not just owners). Prints one org login per line.
219-
###
220-
_graphql_enterprise_orgs() {
221-
local cursor="null"
222-
while true; do
223-
local query
224-
query=$(printf '{ "query": "{ enterprise(slug: \\"%s\\") { organizations(first: 100, after: %s) { nodes { login } pageInfo { hasNextPage endCursor } } } }" }' \
225-
"${ENTERPRISE}" "${cursor}")
226-
227-
local resp
228-
resp=$(curl -s -w "\n%{http_code}" \
229-
-X POST \
230-
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
231-
-H "Content-Type: application/json" \
232-
-H "X-GitHub-Api-Version: 2022-11-28" \
233-
"${API_URL_PREFIX}/graphql" \
234-
-d "${query}")
235-
236-
local http_code body
237-
http_code=$(echo "${resp}" | tail -1)
238-
body=$(echo "${resp}" | head -n -1)
239-
240-
if [[ "${http_code}" != "200" ]]; then
241-
return 1
242-
fi
243-
244-
# Surface any GraphQL-level errors (e.g. token lacks enterprise access)
245-
local gql_errors
246-
gql_errors=$(echo "${body}" | jq -r '.errors // empty' 2>/dev/null || true)
247-
if [ -n "${gql_errors}" ]; then
248-
return 1
249-
fi
250-
251-
echo "${body}" | jq -r '.data.enterprise.organizations.nodes[].login' 2>/dev/null || true
252-
253-
local has_next end_cursor
254-
has_next=$(echo "${body}" | jq -r '.data.enterprise.organizations.pageInfo.hasNextPage')
255-
end_cursor=$(echo "${body}" | jq -r '.data.enterprise.organizations.pageInfo.endCursor')
256-
257-
if [[ "${has_next}" != "true" ]]; then
258-
break
259-
fi
260-
cursor="\"${end_cursor}\""
261-
done
262-
}
263-
264-
###
265-
## get_enterprise_orgs
266-
## Prints one org login per line.
267-
## Strategy:
268-
## 1. Try /enterprises/{slug}/organizations REST endpoint (enterprise-owner token)
269-
## 2. Try GraphQL enterprise(slug).organizations (enterprise member token)
270-
## 3. Fall back to /user/orgs (any read:org token)
271-
###
272-
get_enterprise_orgs() {
273-
print_status "Fetching organisations for enterprise '${ENTERPRISE}'..."
274-
275-
# -- Attempt 1: REST enterprise endpoint --------------------------------
276-
local probe
277-
probe=$(gh_api "/enterprises/${ENTERPRISE}/organizations?per_page=1&page=1")
278-
if [[ "${probe}" != "__404__" && "${probe}" != "__422__" && -n "${probe}" ]]; then
279-
print_status "Using REST enterprise API endpoint."
280-
_paginate_orgs_endpoint \
281-
'.organizations[].login' \
282-
"/enterprises/${ENTERPRISE}/organizations?per_page=100&page=PAGE"
283-
return 0
284-
fi
285-
286-
# -- Attempt 2: GraphQL enterprise query --------------------------------
287-
print_warning "REST enterprise endpoint unavailable — trying GraphQL enterprise query..."
288-
local gql_orgs
289-
gql_orgs=$(_graphql_enterprise_orgs 2>/dev/null || true)
290-
if [ -n "${gql_orgs}" ]; then
291-
print_status "Using GraphQL enterprise endpoint."
292-
echo "${gql_orgs}"
293-
return 0
294-
fi
295-
296-
# -- Attempt 3: /user/orgs fallback -------------------------------------
297-
print_warning "GraphQL enterprise query unavailable — falling back to /user/orgs."
298-
print_warning "Set ORG_FILTER env var to restrict results to enterprise orgs only."
299-
print_status " Example: export ORG_FILTER='^my-enterprise-prefix'"
300-
_paginate_orgs_endpoint \
301-
'.[].login' \
302-
"/user/orgs?per_page=100&page=PAGE"
303-
}
304-
305125
###
306126
## search_dockerfiles_in_org <org>
307127
## Appends TSV rows (org TAB repo_full_name TAB path TAB html_url) to REFS_TEMP
@@ -564,7 +384,7 @@ build_text_summary() {
564384
###
565385
main() {
566386
check_prerequisites
567-
validate_token
387+
validate_github_token
568388

569389
mkdir -p "${REPORT_DIR}"
570390

0 commit comments

Comments
 (0)