From 76a26b29d33775ed1de4549d60272947670dae81 Mon Sep 17 00:00:00 2001 From: Patrick Lewis <4015312+locus313@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:46:39 -0700 Subject: [PATCH 1/4] feat: add dual-auth support and gh_api_paginate to shared lib MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Auto-resolve GITHUB_TOKEN from active gh CLI session at source time so curl-based scripts work with either a PAT or gh CLI auth - Add configure_gh_auth() to bridge GITHUB_TOKEN→GH_TOKEN for scripts that use the gh CLI directly - Add gh_api_paginate() — paginated REST helper that follows Link headers, streams items through a jq filter, handles 404/422 silently, and supports a custom API version header for Copilot endpoints Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/github-common.sh | 115 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/lib/github-common.sh b/lib/github-common.sh index 2742cd1..9e4aaff 100644 --- a/lib/github-common.sh +++ b/lib/github-common.sh @@ -14,10 +14,17 @@ # print_status / print_success / print_warning / print_error # require_env_var [description] — exit if variable is unset/empty # require_command — exit if command not found +# configure_gh_auth [scope_hint] — bridge GITHUB_TOKEN→GH_TOKEN or verify gh session # validate_github_token [bearer] — verify token via /user endpoint +# +# Token auto-resolution (at source time): +# If GITHUB_TOKEN is unset and gh CLI is available, the token is automatically +# resolved from the active gh auth session so curl-based scripts work with +# either a GITHUB_TOKEN env var or a gh CLI session. # validate_token — verify a secondary token variable # validate_slug [label] — exit if value contains unsafe chars # gh_api [curl args...] — Bearer-auth REST helper with retry +# gh_api_paginate [filter] [version] — paginated REST, follows Link headers # _paginate_orgs_endpoint — page through an org list # _graphql_enterprise_orgs — GraphQL cursor-based enterprise orgs # get_enterprise_orgs — three-tier enterprise org resolver @@ -68,6 +75,29 @@ require_command() { fi } +### +## configure_gh_auth [scope_hint] +## Bridges GITHUB_TOKEN into the gh CLI so scripts can accept either a token +## or an active gh auth session interchangeably. +## +## When GITHUB_TOKEN is set: exports it as GH_TOKEN (gh CLI reads this env var). +## When GITHUB_TOKEN is not set: verifies gh auth status and exits with an error +## if no session is active. scope_hint is appended to the error message. +## +## Usage: +## configure_gh_auth "gh auth login" +## configure_gh_auth 'gh auth refresh --scopes "read:enterprise,manage_billing:enterprise"' +### +configure_gh_auth() { + local scope_hint="${1:-gh auth login}" + if [[ -n "${GITHUB_TOKEN:-}" ]]; then + export GH_TOKEN="$GITHUB_TOKEN" + elif ! gh auth status >/dev/null 2>&1; then + print_error "Not authenticated. Set GITHUB_TOKEN or run: ${scope_hint}" + exit 1 + fi +} + ### ## validate_token [bearer] ## Validates the token stored in the named variable by calling the /user endpoint. @@ -174,6 +204,73 @@ gh_api() { return 1 } +### +## gh_api_paginate [jq_filter] [api_version] +## Paginated GitHub REST API helper using Link-header following. +## Outputs each page's items (filtered by jq_filter) to stdout, one item per +## line. Pipe the output to: jq -s '.' to get a JSON array of all items. +## jq -s '. // []' to get [] when the endpoint 404s. +## jq_filter defaults to .[] (one item per array element). +## api_version defaults to 2022-11-28. +## Returns 0 silently on 404/422 (empty output); returns 1 after 5 failed attempts. +### +gh_api_paginate() { + local url="$1" + local jq_filter="${2:-.[]}" + local api_version="${3:-2022-11-28}" + [[ "${url}" == http* ]] || url="${API_URL_PREFIX}${url}" + + local _tmp_headers _tmp_body _http_code _attempt _next_url + _tmp_headers=$(mktemp) + _tmp_body=$(mktemp) + + while [[ -n "${url}" ]]; do + _http_code="" + for _attempt in 1 2 3 4 5; do + _http_code=$(curl -s \ + -D "${_tmp_headers}" \ + -o "${_tmp_body}" \ + -w "%{http_code}" \ + -H "Authorization: Bearer ${GITHUB_TOKEN}" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: ${api_version}" \ + "${url}") + case "${_http_code}" in + 200) break ;; + 404|422) + rm -f "${_tmp_headers}" "${_tmp_body}" + return 0 + ;; + 403|429) + print_warning "Rate limited (HTTP ${_http_code}). Sleeping 60s before retry ${_attempt}/5..." + sleep 60 + ;; + *) + print_warning "HTTP ${_http_code} for ${url} (attempt ${_attempt}/5)" + sleep 5 + ;; + esac + done + + if [[ "${_http_code}" != "200" ]]; then + rm -f "${_tmp_headers}" "${_tmp_body}" + print_error "Failed to fetch ${url} after 5 attempts" + return 1 + fi + + jq -rc "${jq_filter}" "${_tmp_body}" 2>/dev/null || true + + # Follow Link: ; rel="next" to the next page + _next_url=$(grep -i "^link:" "${_tmp_headers}" \ + | grep -o '<[^>]*>; rel="next"' \ + | sed 's/<\([^>]*\)>.*/\1/' \ + || true) + url="${_next_url}" + done + + rm -f "${_tmp_headers}" "${_tmp_body}" +} + ### ## _paginate_orgs_endpoint ## Internal helper: pages through an org-list REST endpoint, printing one @@ -293,3 +390,21 @@ get_enterprise_orgs() { '.[].login' \ "/user/orgs?per_page=100&page=PAGE" } + +### +## Token auto-resolution (runs once at source time) +## If GITHUB_TOKEN is not set, attempt to derive it from an active gh CLI +## auth session. This allows scripts that use GITHUB_TOKEN with curl to work +## with gh CLI auth as an alternative to an explicit token. +## Scripts should still call require_env_var GITHUB_TOKEN or +## validate_github_token to fail fast with a clear message if neither source +## provides a token. +### +if [[ -z "${GITHUB_TOKEN:-}" ]] && command -v gh &>/dev/null; then + _gh_resolved_token=$(gh auth token 2>/dev/null) || true + if [[ -n "${_gh_resolved_token:-}" ]]; then + GITHUB_TOKEN="$_gh_resolved_token" + export GITHUB_TOKEN + fi + unset _gh_resolved_token +fi From 36717f9dd24a9ecb4a8c9d90e627ee7750aba258 Mon Sep 17 00:00:00 2001 From: Patrick Lewis <4015312+locus313@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:46:48 -0700 Subject: [PATCH 2/4] feat: make github-token optional in all action.yml files - Change github-token from required to optional in all 16 existing action.yml files; env mapping uses inputs.github-token || github.token so the built-in Actions token is used automatically when no PAT is provided - Add new action.yml for github-repo-permissions-report - Add new action.yml for github-copilot-report (with no-entra, credits, enterprise, and upn-domain inputs) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../action.yml | 4 +- .../github-dockerfile-discovery/action.yml | 4 +- .../github-get-consumed-licenses/action.yml | 4 +- enterprise/github-get-public-repos/action.yml | 4 +- .../action.yml | 4 +- .../github-add-repo-permissions/action.yml | 4 +- org-admin/github-archive-old-repos/action.yml | 4 +- .../github-auto-repo-creation/action.yml | 4 +- .../action.yml | 4 +- org-admin/github-enable-issues/action.yml | 4 +- org-admin/github-get-repo-list/action.yml | 4 +- org-admin/github-import-repo/action.yml | 4 +- .../action.yml | 4 +- .../github-repo-from-template/action.yml | 4 +- reporting/github-copilot-report/action.yml | 40 +++++++++++++++++++ .../github-monthly-issues-report/action.yml | 4 +- .../github-repo-permissions-report/action.yml | 29 ++++++++++++++ 17 files changed, 99 insertions(+), 30 deletions(-) create mode 100644 reporting/github-copilot-report/action.yml create mode 100644 reporting/github-repo-permissions-report/action.yml diff --git a/enterprise/github-add-enterprise-team-read-permissions/action.yml b/enterprise/github-add-enterprise-team-read-permissions/action.yml index 83ad3d3..0571e52 100644 --- a/enterprise/github-add-enterprise-team-read-permissions/action.yml +++ b/enterprise/github-add-enterprise-team-read-permissions/action.yml @@ -3,7 +3,7 @@ description: 'Grant read permissions to an enterprise team across all organizati inputs: github-token: description: 'PAT with admin:enterprise scope' - required: true + required: false enterprise: description: 'GitHub Enterprise slug' required: true @@ -24,7 +24,7 @@ runs: - name: Add enterprise team read permissions shell: bash env: - GITHUB_TOKEN: ${{ inputs.github-token }} + GITHUB_TOKEN: ${{ inputs.github-token || github.token }} ENTERPRISE: ${{ inputs.enterprise }} ENTERPRISE_TEAM_SLUG: ${{ inputs.enterprise-team-slug }} ALL_REPO_READ_ROLE_NAME: ${{ inputs.all-repo-read-role-name }} diff --git a/enterprise/github-dockerfile-discovery/action.yml b/enterprise/github-dockerfile-discovery/action.yml index fc241e5..0c8f990 100644 --- a/enterprise/github-dockerfile-discovery/action.yml +++ b/enterprise/github-dockerfile-discovery/action.yml @@ -3,7 +3,7 @@ description: 'Discover Dockerfiles and extract FROM instructions across all ente inputs: github-token: description: 'PAT with read:org and repo scope' - required: true + required: false enterprise: description: 'GitHub Enterprise slug' required: true @@ -41,7 +41,7 @@ runs: - name: Discover Dockerfiles across enterprise shell: bash env: - GITHUB_TOKEN: ${{ inputs.github-token }} + GITHUB_TOKEN: ${{ inputs.github-token || github.token }} ENTERPRISE: ${{ inputs.enterprise }} REPORT_DIR: ${{ inputs.report-dir }} ORGS: ${{ inputs.orgs }} diff --git a/enterprise/github-get-consumed-licenses/action.yml b/enterprise/github-get-consumed-licenses/action.yml index 6e4ddb1..0aac518 100644 --- a/enterprise/github-get-consumed-licenses/action.yml +++ b/enterprise/github-get-consumed-licenses/action.yml @@ -3,7 +3,7 @@ description: 'Report GitHub Enterprise consumed licence seat counts' inputs: github-token: description: 'PAT with manage_billing:enterprise scope' - required: true + required: false enterprise: description: 'GitHub Enterprise slug' required: true @@ -17,7 +17,7 @@ runs: - name: Get consumed licenses shell: bash env: - GITHUB_TOKEN: ${{ inputs.github-token }} + GITHUB_TOKEN: ${{ inputs.github-token || github.token }} ENTERPRISE: ${{ inputs.enterprise }} API_URL_PREFIX: ${{ inputs.api-url-prefix }} run: ${{ github.action_path }}/github-get-consumed-licenses.sh diff --git a/enterprise/github-get-public-repos/action.yml b/enterprise/github-get-public-repos/action.yml index b408794..6e8108b 100644 --- a/enterprise/github-get-public-repos/action.yml +++ b/enterprise/github-get-public-repos/action.yml @@ -3,7 +3,7 @@ description: 'List all public repositories across all organizations in a GitHub inputs: github-token: description: 'PAT with read:org and repo scope' - required: true + required: false enterprise: description: 'GitHub Enterprise slug' required: true @@ -33,7 +33,7 @@ runs: - name: Get public repositories shell: bash env: - GITHUB_TOKEN: ${{ inputs.github-token }} + GITHUB_TOKEN: ${{ inputs.github-token || github.token }} ENTERPRISE: ${{ inputs.enterprise }} REPORT_DIR: ${{ inputs.report-dir }} ORGS: ${{ inputs.orgs }} diff --git a/org-admin/github-add-repo-collaborators-by-pattern/action.yml b/org-admin/github-add-repo-collaborators-by-pattern/action.yml index cf688dd..b538fe4 100644 --- a/org-admin/github-add-repo-collaborators-by-pattern/action.yml +++ b/org-admin/github-add-repo-collaborators-by-pattern/action.yml @@ -3,7 +3,7 @@ description: 'Add individual collaborators to repositories whose names match a r inputs: github-token: description: 'PAT with repo and admin:org scope' - required: true + required: false org: description: 'GitHub organization name' required: true @@ -31,7 +31,7 @@ runs: - name: Add collaborators to matching repositories shell: bash env: - GITHUB_TOKEN: ${{ inputs.github-token }} + GITHUB_TOKEN: ${{ inputs.github-token || github.token }} ORG: ${{ inputs.org }} COLLABORATORS: ${{ inputs.collaborators }} REPO_NAME_REGEX: ${{ inputs.repo-name-regex }} diff --git a/org-admin/github-add-repo-permissions/action.yml b/org-admin/github-add-repo-permissions/action.yml index 3390941..d6d90c4 100644 --- a/org-admin/github-add-repo-permissions/action.yml +++ b/org-admin/github-add-repo-permissions/action.yml @@ -3,7 +3,7 @@ description: 'Grant team permissions across all repositories in a GitHub organiz inputs: github-token: description: 'PAT with admin:org scope' - required: true + required: false org: description: 'GitHub organization name' required: true @@ -41,7 +41,7 @@ runs: - name: Grant team permissions to repositories shell: bash env: - GITHUB_TOKEN: ${{ inputs.github-token }} + GITHUB_TOKEN: ${{ inputs.github-token || github.token }} ORG: ${{ inputs.org }} REPO_NAME_FILTER: ${{ inputs.repo-name-filter }} REPO_ADMIN: ${{ inputs.repo-admin }} diff --git a/org-admin/github-archive-old-repos/action.yml b/org-admin/github-archive-old-repos/action.yml index 26f053b..43aacf8 100644 --- a/org-admin/github-archive-old-repos/action.yml +++ b/org-admin/github-archive-old-repos/action.yml @@ -3,7 +3,7 @@ description: 'Archive repositories that have not been updated within a configura inputs: github-token: description: 'PAT with repo scope' - required: true + required: false org: description: 'GitHub organization name' required: true @@ -29,7 +29,7 @@ runs: - name: Archive old repositories shell: bash env: - GITHUB_TOKEN: ${{ inputs.github-token }} + GITHUB_TOKEN: ${{ inputs.github-token || github.token }} ORG: ${{ inputs.org }} YEARS_THRESHOLD: ${{ inputs.years-threshold }} REPORT_DIR: ${{ inputs.report-dir }} diff --git a/org-admin/github-auto-repo-creation/action.yml b/org-admin/github-auto-repo-creation/action.yml index 5e23814..565dd95 100644 --- a/org-admin/github-auto-repo-creation/action.yml +++ b/org-admin/github-auto-repo-creation/action.yml @@ -3,7 +3,7 @@ description: 'Create private repositories with branch protection, CODEOWNERS, an inputs: github-token: description: 'PAT with repo and admin:org scope' - required: true + required: false org: description: 'GitHub organization name' required: true @@ -27,7 +27,7 @@ runs: - name: Create repositories shell: bash env: - GITHUB_TOKEN: ${{ inputs.github-token }} + GITHUB_TOKEN: ${{ inputs.github-token || github.token }} ORG: ${{ inputs.org }} REPO_NAMES: ${{ inputs.repo-names }} REPO_OWNERS: ${{ inputs.repo-owners }} diff --git a/org-admin/github-close-archived-repo-security-alerts/action.yml b/org-admin/github-close-archived-repo-security-alerts/action.yml index 4de241b..74659b2 100644 --- a/org-admin/github-close-archived-repo-security-alerts/action.yml +++ b/org-admin/github-close-archived-repo-security-alerts/action.yml @@ -3,7 +3,7 @@ description: 'Dismiss all open Dependabot, code-scanning, and secret-scanning al inputs: github-token: description: 'PAT with security_events and repo scope' - required: true + required: false org: description: 'GitHub organization name' required: true @@ -37,7 +37,7 @@ runs: - name: Close archived repo security alerts shell: bash env: - GITHUB_TOKEN: ${{ inputs.github-token }} + GITHUB_TOKEN: ${{ inputs.github-token || github.token }} ORG: ${{ inputs.org }} DEPENDABOT_REASON: ${{ inputs.dependabot-reason }} CODE_SCANNING_REASON: ${{ inputs.code-scanning-reason }} diff --git a/org-admin/github-enable-issues/action.yml b/org-admin/github-enable-issues/action.yml index e0f271a..a3b3fea 100644 --- a/org-admin/github-enable-issues/action.yml +++ b/org-admin/github-enable-issues/action.yml @@ -3,7 +3,7 @@ description: 'Enable the Issues feature on all repositories where it is currentl inputs: github-token: description: 'PAT with repo or admin:org scope' - required: true + required: false org: description: 'GitHub organization name' required: true @@ -21,7 +21,7 @@ runs: - name: Enable Issues on repositories shell: bash env: - GITHUB_TOKEN: ${{ inputs.github-token }} + GITHUB_TOKEN: ${{ inputs.github-token || github.token }} ORG: ${{ inputs.org }} API_URL_PREFIX: ${{ inputs.api-url-prefix }} run: | diff --git a/org-admin/github-get-repo-list/action.yml b/org-admin/github-get-repo-list/action.yml index 9d31438..8ae705f 100644 --- a/org-admin/github-get-repo-list/action.yml +++ b/org-admin/github-get-repo-list/action.yml @@ -3,7 +3,7 @@ description: 'Output a CSV list of all repositories in a GitHub organization to inputs: github-token: description: 'PAT with read:org and repo scope' - required: true + required: false org: description: 'GitHub organization name' required: true @@ -17,7 +17,7 @@ runs: - name: Get repository list shell: bash env: - GITHUB_TOKEN: ${{ inputs.github-token }} + GITHUB_TOKEN: ${{ inputs.github-token || github.token }} ORG: ${{ inputs.org }} API_URL_PREFIX: ${{ inputs.api-url-prefix }} run: ${{ github.action_path }}/github-get-repo-list.sh diff --git a/org-admin/github-import-repo/action.yml b/org-admin/github-import-repo/action.yml index 13cf45b..4015ee9 100644 --- a/org-admin/github-import-repo/action.yml +++ b/org-admin/github-import-repo/action.yml @@ -3,7 +3,7 @@ description: 'Mirror-clone a repository and push it to a new private repository inputs: github-token: description: 'PAT with repo scope' - required: true + required: false org: description: 'GitHub organization name' required: true @@ -30,7 +30,7 @@ runs: - name: Import repository shell: bash env: - GITHUB_TOKEN: ${{ inputs.github-token }} + GITHUB_TOKEN: ${{ inputs.github-token || github.token }} ORG: ${{ inputs.org }} OWNER_USERNAME: ${{ inputs.owner-username }} API_URL_PREFIX: ${{ inputs.api-url-prefix }} diff --git a/org-admin/github-migrate-internal-repos-to-private/action.yml b/org-admin/github-migrate-internal-repos-to-private/action.yml index 96899c5..3c20468 100644 --- a/org-admin/github-migrate-internal-repos-to-private/action.yml +++ b/org-admin/github-migrate-internal-repos-to-private/action.yml @@ -3,7 +3,7 @@ description: 'Convert all internal-visibility repositories in an organization to inputs: github-token: description: 'PAT with repo scope' - required: true + required: false org: description: 'GitHub organization name' required: true @@ -17,7 +17,7 @@ runs: - name: Migrate internal repos to private shell: bash env: - GITHUB_TOKEN: ${{ inputs.github-token }} + GITHUB_TOKEN: ${{ inputs.github-token || github.token }} ORG: ${{ inputs.org }} API_URL_PREFIX: ${{ inputs.api-url-prefix }} run: ${{ github.action_path }}/github-migrate-internal-repos-to-private.sh diff --git a/org-admin/github-repo-from-template/action.yml b/org-admin/github-repo-from-template/action.yml index 4d38054..713a2ea 100644 --- a/org-admin/github-repo-from-template/action.yml +++ b/org-admin/github-repo-from-template/action.yml @@ -3,7 +3,7 @@ description: 'Create a private repository from a template with team permissions inputs: github-token: description: 'PAT with repo scope' - required: true + required: false org: description: 'GitHub organization name' required: true @@ -37,7 +37,7 @@ runs: - name: Create repository from template shell: bash env: - GITHUB_TOKEN: ${{ inputs.github-token }} + GITHUB_TOKEN: ${{ inputs.github-token || github.token }} ORG: ${{ inputs.org }} TEMPLATE_REPO: ${{ inputs.template-repo }} CD_USERNAME: ${{ inputs.cd-username }} diff --git a/reporting/github-copilot-report/action.yml b/reporting/github-copilot-report/action.yml new file mode 100644 index 0000000..135646d --- /dev/null +++ b/reporting/github-copilot-report/action.yml @@ -0,0 +1,40 @@ +name: 'Copilot Report' +description: 'GitHub Copilot Enterprise licence and AI credit usage report, optionally enriched with Entra ID department data' +inputs: + github-token: + description: 'PAT with read:enterprise and manage_billing:enterprise scopes. Cannot be the built-in GITHUB_TOKEN.' + required: true + enterprise: + description: 'GitHub Enterprise slug' + required: true + upn-domain: + description: 'Email domain for Entra ID lookup when GitHub carries no email address (e.g. example.com)' + required: false + default: '' + credits: + description: 'Override credits-per-seat value if your portal shows a different pool size' + required: false + default: '' + output: + description: 'Output CSV file path (default: copilot-report-YYYYMMDD.csv)' + required: false + default: '' + no-entra: + description: 'Skip Entra ID department lookup' + required: false + default: 'false' +runs: + using: composite + steps: + - name: Generate Copilot report + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github-token }} + GITHUB_ENTERPRISE: ${{ inputs.enterprise }} + UPN_DOMAIN: ${{ inputs.upn-domain }} + CREDITS_PER_SEAT_OVERRIDE: ${{ inputs.credits }} + run: | + ARGS=() + [[ -n "${{ inputs.output }}" ]] && ARGS+=(--output "${{ inputs.output }}") + [[ "${{ inputs.no-entra }}" == "true" ]] && ARGS+=(--no-entra) + "${{ github.action_path }}/github-copilot-report.sh" "${ARGS[@]}" diff --git a/reporting/github-monthly-issues-report/action.yml b/reporting/github-monthly-issues-report/action.yml index 93b0612..11311e4 100644 --- a/reporting/github-monthly-issues-report/action.yml +++ b/reporting/github-monthly-issues-report/action.yml @@ -3,7 +3,7 @@ description: 'Generate an HTML report of issues created within a date range' inputs: github-token: description: 'PAT with repo scope' - required: true + required: false org: description: 'GitHub organization name' required: true @@ -30,7 +30,7 @@ runs: - name: Generate monthly issues report shell: bash env: - GITHUB_TOKEN: ${{ inputs.github-token }} + GITHUB_TOKEN: ${{ inputs.github-token || github.token }} ORG: ${{ inputs.org }} REPO: ${{ inputs.repo }} MONTH_START: ${{ inputs.month-start }} diff --git a/reporting/github-repo-permissions-report/action.yml b/reporting/github-repo-permissions-report/action.yml new file mode 100644 index 0000000..e69abe0 --- /dev/null +++ b/reporting/github-repo-permissions-report/action.yml @@ -0,0 +1,29 @@ +name: 'Repo Permissions Report' +description: 'Export repository collaborator/team permissions and branch-approval bypass actors to CSV' +inputs: + github-token: + description: 'GitHub token with repo and read:org scopes. Defaults to the built-in GITHUB_TOKEN.' + required: false + repo: + description: 'Target repository in OWNER/REPO format' + required: true + branch: + description: 'Branch to evaluate (default: repository default branch)' + required: false + default: '' + output: + description: 'Output CSV file path' + required: false + default: '' +runs: + using: composite + steps: + - name: Generate repository permissions report + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github-token || github.token }} + run: | + ARGS=(-r "${{ inputs.repo }}") + [[ -n "${{ inputs.branch }}" ]] && ARGS+=(-b "${{ inputs.branch }}") + [[ -n "${{ inputs.output }}" ]] && ARGS+=(-o "${{ inputs.output }}") + "${{ github.action_path }}/github-repo-permissions-report.sh" "${ARGS[@]}" From 53d580786d129d4d62aaa2403c448bd01c504c34 Mon Sep 17 00:00:00 2001 From: Patrick Lewis <4015312+locus313@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:47:05 -0700 Subject: [PATCH 3/4] feat: convert gh-CLI-only scripts to support GITHUB_TOKEN + curl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit github-organize-stars: - Now sources lib/github-common.sh for shared helpers - Calls configure_gh_auth to bridge GITHUB_TOKEN→GH_TOKEN; kept as gh-CLI-based since it uses GraphQL mutations not available via REST github-repo-permissions-report: - Removed gh CLI dependency; now uses curl via lib's gh_api and gh_api_paginate for all API calls - Branch protection and ruleset 404s handled via __404__ sentinel github-copilot-report: - Removed gh CLI dependency; added local _copilot_api() using curl with X-GitHub-Api-Version: 2026-03-10 - fetch_seats() now uses gh_api_paginate with the Copilot API version - az CLI is now truly optional: if not installed or not logged in, Entra ID enrichment is silently skipped (--no-entra also works on runners without az installed) - Added missing API_URL_PREFIX default to fix validate_github_token Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../github-organize-stars.sh | 12 ++- .../github-copilot-report.sh | 80 +++++++++++++------ .../github-repo-permissions-report.sh | 50 +++++------- 3 files changed, 87 insertions(+), 55 deletions(-) diff --git a/personal/github-organize-stars/github-organize-stars.sh b/personal/github-organize-stars/github-organize-stars.sh index f750f1a..7c84c27 100644 --- a/personal/github-organize-stars/github-organize-stars.sh +++ b/personal/github-organize-stars/github-organize-stars.sh @@ -14,16 +14,22 @@ # ./github-organize-stars.sh --no-cache # Force re-fetch stars from GitHub # # Environment variables: -# CACHE_FILE Optional. Path to the stars cache file +# GITHUB_TOKEN Optional. PAT or token used to authenticate gh CLI. +# When set, takes precedence over any active gh auth session. +# CACHE_FILE Optional. Path to the stars cache file # (default: ~/.cache/gh-star-organizer/stars.json) # # Requirements: -# - gh (GitHub CLI, authenticated) +# - gh (GitHub CLI, authenticated via GITHUB_TOKEN or gh auth login) # - jq # ============================================================================= set -uo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../../lib/github-common.sh +source "${SCRIPT_DIR}/../../lib/github-common.sh" + # ---- Dependency check ------------------------------------------------------- for cmd in gh jq; do if ! command -v "$cmd" &>/dev/null; then @@ -55,6 +61,8 @@ for arg in "$@"; do esac done +configure_gh_auth "gh auth login" + # ============================================================================= # CATEGORY RULES # Format of each element: "List Name|LANGUAGES|TOPICS|NAME_KEYWORDS" diff --git a/reporting/github-copilot-report/github-copilot-report.sh b/reporting/github-copilot-report/github-copilot-report.sh index 342bbc1..ff5cd32 100755 --- a/reporting/github-copilot-report/github-copilot-report.sh +++ b/reporting/github-copilot-report/github-copilot-report.sh @@ -5,10 +5,14 @@ # GitHub Copilot Enterprise licence & usage report, optionally enriched with # Entra ID department data. Output: CSV file + console summary. # -# Authentication — no tokens or secrets required in flags: -# GitHub : gh CLI (run 'gh auth refresh --scopes "read:enterprise,manage_billing:enterprise"' -# as enterprise owner or billing manager) -# Entra : az CLI (run 'az login' once; needs User.Read.All) +# Authentication — no secrets required in flags: +# GitHub : GITHUB_TOKEN env var (PAT with read:enterprise and +# manage_billing:enterprise scopes) +# OR provided automatically from an active gh auth session +# with the required scopes +# Entra : az CLI (optional; run 'az login' once; needs User.Read.All) +# Skipped automatically if az is not installed or not logged in. +# Use --no-entra to suppress Entra lookups explicitly. # # What this reports: # • Every user with a Copilot seat, their plan type, and their pool contribution @@ -36,6 +40,8 @@ source "${SCRIPT_DIR}/../../lib/github-common.sh" # ── Defaults ────────────────────────────────────────────────────────────────── GITHUB_ENTERPRISE="${GITHUB_ENTERPRISE:-}" +GITHUB_TOKEN="${GITHUB_TOKEN:-}" +API_URL_PREFIX="${API_URL_PREFIX:-https://api.github.com}" UPN_DOMAIN="${UPN_DOMAIN:-}" CREDITS_PER_SEAT_OVERRIDE="${CREDITS_PER_SEAT_OVERRIDE:-}" OUTPUT_CSV="copilot-report-$(date +%Y%m%d).csv" @@ -109,8 +115,9 @@ Usage: github-copilot-report.sh [OPTIONS] GitHub Copilot Enterprise licence + usage report, optionally enriched with Entra ID department information. -Authentication (no secrets in flags — both CLIs handle auth): - GitHub → gh auth refresh --scopes "read:enterprise,manage_billing:enterprise" +Authentication (no secrets in flags): + GitHub → export GITHUB_TOKEN=ghp_yourtoken (read:enterprise,manage_billing:enterprise) + OR resolved automatically from an active gh auth session Entra → az login Options: @@ -124,7 +131,7 @@ Options: --no-entra Skip Entra ID department lookup -h, --help Show this message -Required gh CLI scopes: +Required GITHUB_TOKEN scopes: read:enterprise manage_billing:enterprise (requires enterprise owner or billing manager) @@ -150,15 +157,17 @@ done [[ -z "$GITHUB_ENTERPRISE" ]] && \ err "GitHub Enterprise slug is required (-e / \$GITHUB_ENTERPRISE)" -require_command gh -require_command az require_command jq -gh auth status &>/dev/null || err "gh CLI is not authenticated. Run: gh auth refresh --scopes \"read:enterprise,manage_billing:enterprise\"" +require_env_var GITHUB_TOKEN +validate_github_token "bearer" # ── Acquire Microsoft Graph token via az CLI ────────────────────────────────── if [[ "$NO_ENTRA" == "true" ]]; then warn "Entra ID lookup disabled (--no-entra). Department column will be N/A." +elif ! command -v az &>/dev/null; then + warn "az CLI is not installed — department/division grouping will be skipped." + warn "Install the Azure CLI and run 'az login' to enable Entra ID enrichment, or pass --no-entra." elif az account show &>/dev/null 2>&1; then info "Acquiring Microsoft Graph token via az CLI..." GRAPH_TOKEN=$(az account get-access-token \ @@ -176,14 +185,36 @@ else warn "Run 'az login' to enable Entra ID enrichment, or pass --no-entra." fi -# ── GitHub API via gh CLI ───────────────────────────────────────────────────── -# Thin wrapper that injects the standard Accept and version headers. -# Pass any extra 'gh api' flags (e.g. --paginate, --jq, --input) as arguments. -gh_api() { - gh api \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2026-03-10" \ - "$@" +# ── GitHub API via curl with Copilot API version ────────────────────────────── +# The Copilot usage-metrics endpoints require API version 2026-03-10. +# Uses the same retry/rate-limit logic as lib's gh_api. +_copilot_api() { + local url="$1" + shift + [[ "${url}" == http* ]] || url="${API_URL_PREFIX:-https://api.github.com}${url}" + + local _attempt _http_code _body + for _attempt in 1 2 3 4 5; do + _body=$(curl -s -w "\n%{http_code}" \ + -H "Authorization: Bearer ${GITHUB_TOKEN}" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2026-03-10" \ + "$@" "${url}") + _http_code=$(echo "${_body}" | tail -1) + _body=$(echo "${_body}" | head -n -1) + case "${_http_code}" in + 200) echo "${_body}"; return 0 ;; + 404) echo "__404__"; return 0 ;; + 422) echo "__422__"; return 0 ;; + 403|429) + warn "Rate limited (HTTP ${_http_code}). Sleeping 60s before retry ${_attempt}/5..." + sleep 60 ;; + *) + warn "HTTP ${_http_code} from GitHub API (attempt ${_attempt}/5)" + sleep 5 ;; + esac + done + err "Failed to reach ${url} after 5 attempts" } # fetch_usage_ndjson REPORT_PATH @@ -193,7 +224,8 @@ gh_api() { fetch_usage_ndjson() { local path="$1" local resp links - resp=$(gh_api "${path}" 2>/dev/null) || return 0 + resp=$(_copilot_api "${path}" 2>/dev/null) || return 0 + [[ "${resp}" == "__404__" || "${resp}" == "__422__" ]] && return 0 links=$(echo "$resp" | jq -r '.download_links[]? // empty' 2>/dev/null) || return 0 [[ -z "$links" ]] && return 0 while IFS= read -r url; do @@ -208,9 +240,7 @@ TOTAL_ASSIGNED_SEATS=0 fetch_seats() { local slug="$1" - gh_api --paginate \ - --jq '.seats[]' \ - "/enterprises/${slug}/copilot/billing/seats" \ + gh_api_paginate "/enterprises/${slug}/copilot/billing/seats" '.seats[]' '2026-03-10' \ | jq -s '.' } @@ -294,10 +324,10 @@ while IFS= read -r _login; do [[ -z "$_login" ]] && continue _login_i=$(( _login_i + 1 )) printf '\r [%d/%d] %s ' "$_login_i" "$_LOGIN_COUNT" "$_login" >&2 - _resp=$(gh_api \ + _resp=$(_copilot_api \ "/enterprises/${GITHUB_ENTERPRISE}/settings/billing/ai_credit/usage?user=${_login}&year=${_BILLING_YEAR}&month=${_BILLING_MONTH}" \ 2>/dev/null) || _resp="" - if [[ -n "$_resp" ]]; then + if [[ -n "$_resp" && "${_resp}" != "__404__" && "${_resp}" != "__422__" ]]; then _credits=$(echo "$_resp" | jq '[.usageItems[]?.grossQuantity // 0] | add // 0 | round' 2>/dev/null || echo "0") # Store per-model breakdown while IFS=$'\t' read -r _model _qty; do @@ -317,7 +347,7 @@ if [[ "$_billing_ok" == "true" ]]; then info "Loaded billing data for ${#USER_CREDITS_USED[@]} user(s)." else warn "Some billing API calls failed — credits may show 0 for affected users." - warn "Ensure manage_billing:enterprise scope: gh auth refresh --scopes \"read:enterprise,manage_billing:enterprise\"" + warn "Ensure manage_billing:enterprise scope: set GITHUB_TOKEN with the required scopes" fi # Build sorted list of all model names seen across all users diff --git a/reporting/github-repo-permissions-report/github-repo-permissions-report.sh b/reporting/github-repo-permissions-report/github-repo-permissions-report.sh index 933b0f4..e1c54dd 100755 --- a/reporting/github-repo-permissions-report/github-repo-permissions-report.sh +++ b/reporting/github-repo-permissions-report/github-repo-permissions-report.sh @@ -10,8 +10,13 @@ # - permission : collaborators/teams and whether they can bypass approvals # - bypass_actor : explicit bypass actors from branch protection and rulesets # +# Environment variables: +# GITHUB_TOKEN Required. PAT with repo and read:org scope +# (or provided automatically from an active gh auth session) +# API_URL_PREFIX Optional. GitHub API base URL (default: https://api.github.com) +# # Requirements: -# - gh CLI authenticated with access to the target repository +# - curl # - jq # ============================================================================= @@ -21,7 +26,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=../../lib/github-common.sh source "${SCRIPT_DIR}/../../lib/github-common.sh" -API_VERSION="2022-11-28" +API_URL_PREFIX=${API_URL_PREFIX:-'https://api.github.com'} REPO="" BRANCH="" OUTPUT_CSV="" @@ -75,10 +80,10 @@ done [[ -z "$REPO" ]] && err "Repository is required. Use -r OWNER/REPO" -require_command gh require_command jq -gh auth status >/dev/null 2>&1 || err "gh is not authenticated. Run: gh auth login" +require_env_var GITHUB_TOKEN +validate_github_token TMP_DIR="$(mktemp -d)" trap 'rm -rf "$TMP_DIR"' EXIT @@ -90,15 +95,10 @@ BP_JSON="${TMP_DIR}/branch_protection.json" RULESETS_JSON="${TMP_DIR}/rulesets.json" BYPASS_JSON="${TMP_DIR}/bypass.json" -gh_api() { - gh api \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: ${API_VERSION}" \ - "$@" -} - info "Fetching repository metadata for ${REPO}..." -gh_api "/repos/${REPO}" > "$REPO_JSON" +_repo_resp=$(gh_api "/repos/${REPO}") +[[ "${_repo_resp}" == "__404__" ]] && err "Repository ${REPO} not found or not accessible" +echo "${_repo_resp}" > "$REPO_JSON" DEFAULT_BRANCH="$(jq -r '.default_branch // empty' "$REPO_JSON")" [[ -z "$DEFAULT_BRANCH" ]] && err "Unable to determine default branch for ${REPO}" @@ -111,30 +111,24 @@ if [[ -z "$OUTPUT_CSV" ]]; then fi info "Fetching collaborators..." -gh_api --paginate \ - "/repos/${REPO}/collaborators?affiliation=all&per_page=100" \ - --jq '.[]' | jq -s '.' > "$COLLAB_JSON" +gh_api_paginate "/repos/${REPO}/collaborators?affiliation=all&per_page=100" '.[]' \ + | jq -s '.' > "$COLLAB_JSON" info "Fetching teams with repository access..." -gh_api --paginate \ - "/repos/${REPO}/teams?per_page=100" \ - --jq '.[]' | jq -s '.' > "$TEAMS_JSON" +gh_api_paginate "/repos/${REPO}/teams?per_page=100" '.[]' \ + | jq -s '.' > "$TEAMS_JSON" info "Fetching branch protection for ${BRANCH} (if configured)..." -if gh_api "/repos/${REPO}/branches/${BRANCH}/protection" > "$BP_JSON" 2>/dev/null; then - : -else +_bp_resp=$(gh_api "/repos/${REPO}/branches/${BRANCH}/protection") +if [[ "${_bp_resp}" == "__404__" || "${_bp_resp}" == "__422__" ]]; then echo '{}' > "$BP_JSON" +else + echo "${_bp_resp}" > "$BP_JSON" fi info "Fetching repository rulesets (if configured)..." -if gh_api --paginate \ - "/repos/${REPO}/rulesets?includes_parents=true&per_page=100" \ - --jq '.[]' | jq -s '.' > "$RULESETS_JSON" 2>/dev/null; then - : -else - echo '[]' > "$RULESETS_JSON" -fi +gh_api_paginate "/repos/${REPO}/rulesets?includes_parents=true&per_page=100" '.[]' \ + | jq -s '. // []' > "$RULESETS_JSON" info "Building bypass actor list (branch protection + applicable rulesets)..." jq -n \ From 31cedf47e7bf5801ae9c595c895e16182885ea9f Mon Sep 17 00:00:00 2001 From: Patrick Lewis <4015312+locus313@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:47:16 -0700 Subject: [PATCH 4/4] docs: update all docs to reflect dual-auth support - README: prerequisites for repo-permissions-report and copilot-report updated from gh CLI to curl; az marked as optional; available actions table includes both new scripts; usage examples updated - AGENTS.md: shared library table adds configure_gh_auth and gh_api_paginate; tech stack row for gh CLI updated to only list github-organize-stars; error handling sequence documents the auto-resolution side-effect - copilot-instructions.md: script descriptions for repo-permissions-report and copilot-report updated to reflect curl usage; copilot-report az requirement noted as optional/runtime-checked Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 6 +++--- AGENTS.md | 6 +++++- README.md | 28 +++++++++++++++------------- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 36f1306..bb90561 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -193,7 +193,7 @@ Generates an HTML report of issues created in a date range (`MONTH_START`/`MONTH ### `github-organize-stars` -Uses `gh` CLI GraphQL (not `curl`) to fetch all starred repos. Categorizes by primary language, GitHub topics, and name keywords using a `RULES` array (pipe-delimited: `List Name|LANGUAGES|TOPICS|NAME_KEYWORDS`; first matching rule wins). Caches stars at `~/.cache/gh-star-organizer/stars.json`. Supports `--dry-run`, `-y` (skip confirm), `--show-repos`, and `--no-cache`. Adds repos to Lists in batches of 25. +Uses `gh` CLI GraphQL (not `curl`) to fetch all starred repos. Accepts `GITHUB_TOKEN` env var or an active `gh` auth session for authentication. Categorizes by primary language, GitHub topics, and name keywords using a `RULES` array (pipe-delimited: `List Name|LANGUAGES|TOPICS|NAME_KEYWORDS`; first matching rule wins). Caches stars at `~/.cache/gh-star-organizer/stars.json`. Supports `--dry-run`, `-y` (skip confirm), `--show-repos`, and `--no-cache`. Adds repos to Lists in batches of 25. ### `github-repo-from-template` @@ -201,11 +201,11 @@ Creates a private repo from `TEMPLATE_REPO` (including all branches), assigns ad ### `github-repo-permissions-report` -Uses `gh` CLI (not `curl`/`GITHUB_TOKEN`) for all API calls. Accepts `-r OWNER/REPO`, `-b BRANCH`, and `-o FILE` flags. Outputs a CSV with two record types: `permission` (all users/teams) and `bypass_actor` (explicit PR approval bypass entries). Reports both branch protection rules and repository rulesets. Requires `gh auth login` before running — no `GITHUB_TOKEN` env var needed. +Uses `curl` for all API calls. Accepts `GITHUB_TOKEN` env var or an active `gh` auth session (token auto-resolved at lib source time) for authentication. Accepts `-r OWNER/REPO`, `-b BRANCH`, and `-o FILE` flags. Outputs a CSV with two record types: `permission` (all users/teams) and `bypass_actor` (explicit PR approval bypass entries). Reports both branch protection rules and repository rulesets. ### `github-copilot-report` -Uses `gh` CLI (not `curl`/`GITHUB_TOKEN`) for all GitHub API calls; requires `gh auth refresh --scopes "read:enterprise,manage_billing:enterprise"`. Also requires `az` to be **installed** (not just logged in) — the script calls `require_command az` unconditionally before checking `--no-entra`. When `az` is logged in, enriches each user with Entra ID department and job title via `az rest`. +Uses `curl` for all GitHub API calls. Accepts `GITHUB_TOKEN` env var (PAT with `read:enterprise` and `manage_billing:enterprise` scopes) or an active `gh` auth session (token auto-resolved at lib source time) for authentication. Also requires `az` to be **installed** when Entra ID enrichment is needed — `az` is checked at runtime only if `--no-entra` is not set and `az` is not already skipped. If `az` is not installed or not logged in, Entra lookup is silently skipped. When `az` is logged in, enriches each user with Entra ID department and job title via `az rest`. Auto-detects credits per seat from a promo/standard table keyed on plan type and today's date (promo period Jun 1 – Sep 1, 2026); override with `--credits N` or `$CREDITS_PER_SEAT_OVERRIDE`. Credits are pooled enterprise-wide, not per-user buckets. Code completions are not billed in AI credits. diff --git a/AGENTS.md b/AGENTS.md index b5b5a12..4482c7b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,7 +47,7 @@ github-api-scripts/ | bash 4+ | All scripts | | curl | GitHub REST and GraphQL API calls | | jq | JSON parsing and transformation | -| gh CLI | Used in `github-organize-stars` and `github-repo-permissions-report` | +| gh CLI | Used in `github-organize-stars` | | base64 | Used in `github-auto-repo-creation` for CODEOWNERS encoding | | git | Used in `github-import-repo` for bare clone + mirror push | | shellcheck | Linting (pre-commit hook + CI) | @@ -142,9 +142,11 @@ Always use `SCRIPT_DIR` to build the path to `lib/github-common.sh`. The library | `print_status` / `print_success` / `print_warning` / `print_error` | Colored output | | `require_env_var ` | Exit with message if variable unset/empty | | `require_command ` | Exit if binary not in PATH | +| `configure_gh_auth [scope_hint]` | Bridge GITHUB_TOKEN→GH_TOKEN or verify gh auth session | | `validate_github_token [bearer]` | Verify GITHUB_TOKEN via /user endpoint | | `validate_slug