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
6 changes: 3 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,19 +193,19 @@ 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`

Creates a private repo from `TEMPLATE_REPO` (including all branches), assigns admin permissions to space-separated `REPO_ADMIN` teams, write permissions to `REPO_WRITE` teams, then invites `CD_USERNAME` as a collaborator using `CD_GITHUB_TOKEN` and auto-accepts the invitation. All slug values are validated before use.

### `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.

Expand Down
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down Expand Up @@ -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 <VAR>` | Exit with message if variable unset/empty |
| `require_command <cmd>` | 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 <value> <label>` | Reject values with non-alphanumeric/hyphen/underscore chars |
| `gh_api <path> [curl args...]` | Bearer-auth REST helper with 5-retry rate-limit handling |
| `gh_api_paginate <path> [filter] [version]` | Paginated REST helper, follows Link headers, streams items |
| `get_enterprise_orgs` | Three-tier enterprise org resolver (REST → GraphQL → /user/orgs) |
| `get_repo_page_count <url>` | Returns total pages for a paginated endpoint |

Expand All @@ -155,6 +157,8 @@ Always use `SCRIPT_DIR` to build the path to `lib/github-common.sh`. The library
3. Validate additional inputs with `validate_slug` where needed
4. Proceed with main logic

> **Note on token auto-resolution:** Sourcing `lib/github-common.sh` automatically populates `GITHUB_TOKEN` from an active `gh` auth session if the variable is unset. This means `require_env_var GITHUB_TOKEN` may pass even when no explicit token was provided by the caller — the token was silently resolved from `gh auth token`. Script headers should document `GITHUB_TOKEN` as "Required (or provided by an active gh auth session)". Scripts that use the `gh` CLI instead of `curl` should call `configure_gh_auth` instead of step 2 above.

### Pagination (REST)

```bash
Expand Down
28 changes: 15 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -516,11 +516,12 @@ cd reporting/github-monthly-issues-report
Exports repository user/team permissions to CSV and identifies who can bypass pull request approval requirements, from both branch protection rules and repository rulesets.

**Prerequisites:**
- **[gh](https://cli.github.com)** — GitHub CLI (authenticated via `gh auth login`)
- **[curl](https://curl.se)** — HTTP client
- **[jq](https://stedolan.github.io/jq)** — JSON processor

**Usage:**
```bash
export GITHUB_TOKEN=ghp_yourtoken # or resolved automatically from an active gh auth session
cd reporting/github-repo-permissions-report
./github-repo-permissions-report.sh -r OWNER/REPO
./github-repo-permissions-report.sh -r OWNER/REPO -b main -o report.csv
Expand All @@ -540,9 +541,6 @@ cd reporting/github-repo-permissions-report
- Identifies every principal that can bypass PR approval requirements
- Produces a CSV with two record types: `permission` (all users/teams) and `bypass_actor` (explicit bypass entries)

> [!NOTE]
> Uses the `gh` CLI for all API calls. Authenticate via `gh auth login` before running.

---

### Copilot Enterprise Report
Expand All @@ -552,14 +550,18 @@ cd reporting/github-repo-permissions-report
Generates a GitHub Copilot Enterprise licence and usage report. Shows every licensed user, their plan type, pool credit contribution, and actual AI credit consumption for the current billing month. Optionally enriches data with Entra ID department information.

**Prerequisites:**
- **[gh](https://cli.github.com)** — GitHub CLI authenticated with `read:enterprise` and `manage_billing:enterprise` scopes
- **[az](https://learn.microsoft.com/en-us/cli/azure/)** — Azure CLI (must be installed; login required only for Entra ID department enrichment — pass `--no-entra` to skip the login requirement)
- **[curl](https://curl.se)** — HTTP client
- **[az](https://learn.microsoft.com/en-us/cli/azure/)** — Azure CLI (optional; required only for Entra ID department enrichment — pass `--no-entra` or omit az to skip)
- **[jq](https://stedolan.github.io/jq)** — JSON processor

**Required variables:**
```bash
export GITHUB_ENTERPRISE="your-enterprise-slug"

# GitHub auth — use one of:
export GITHUB_TOKEN=ghp_yourtoken # PAT with read:enterprise and manage_billing:enterprise scopes
# OR: token is resolved automatically from an active gh auth session with the required scopes

# Optional: Entra ID UPN domain for users without a public GitHub email
export UPN_DOMAIN="example.com" # e.g. 'john_example' → john@example.com

Expand All @@ -571,9 +573,7 @@ export CREDITS_PER_SEAT_OVERRIDE="1900"
```bash
cd reporting/github-copilot-report

# Authenticate first
gh auth refresh --scopes "read:enterprise,manage_billing:enterprise"
az login # required to be installed; login needed only for department enrichment
az login # optional; needed only for Entra ID department enrichment

./github-copilot-report.sh -e YOUR-ENTERPRISE
./github-copilot-report.sh -e YOUR-ENTERPRISE -d example.com
Expand All @@ -599,10 +599,10 @@ az login # required to be installed; login needed only for department enrichme
- Outputs a CSV and a formatted console summary with department breakdown and model usage tables

> [!IMPORTANT]
> Requires an enterprise owner or billing manager token. Run `gh auth refresh --scopes "read:enterprise,manage_billing:enterprise"` before executing.
> Requires a PAT with `read:enterprise` and `manage_billing:enterprise` scopes — the built-in `GITHUB_TOKEN` cannot grant enterprise-level access. Set `GITHUB_TOKEN` before executing (or have an active `gh` auth session with those scopes so the lib can auto-resolve the token).

> [!NOTE]
> Uses the `gh` CLI and (optionally) the `az` CLI. The Entra ID enrichment is skipped automatically if `az` is not logged in, or can be disabled with `--no-entra`.
> The Entra ID enrichment is skipped automatically if `az` is not logged in, or can be disabled with `--no-entra`.

---

Expand Down Expand Up @@ -802,7 +802,7 @@ cd enterprise/github-get-public-repos
Fetches all your starred repositories and organizes them into GitHub Lists using customizable categorization rules.

**Prerequisites:**
- **[gh](https://cli.github.com)** - GitHub CLI (authenticated via `gh auth login`)
- **[gh](https://cli.github.com)** - GitHub CLI (authenticated via `GITHUB_TOKEN` env var or `gh auth login`)
- **[jq](https://stedolan.github.io/jq)** - Command-line JSON processor

**Usage:**
Expand Down Expand Up @@ -835,7 +835,7 @@ Edit the `RULES` array in the script. Each rule is a `|`-delimited string:
The **first matching rule wins**, so order matters. Place more specific rules (e.g., AI) before general ones (e.g., Security).

> [!NOTE]
> This script uses the `gh` CLI for all API calls (GraphQL) rather than `curl`. Ensure you are authenticated via `gh auth login` before running.
> This script uses the `gh` CLI for all API calls (GraphQL) rather than `curl`. Authenticate by setting `GITHUB_TOKEN` or by running `gh auth login`.

## Shared Library: `lib/github-common.sh`

Expand Down Expand Up @@ -921,6 +921,8 @@ Each script is published as a **composite action**, so you can reference it dire
| `org-admin/github-migrate-internal-repos-to-private` | Convert all internal-visibility repositories to private |
| `org-admin/github-repo-from-template` | Create a repository from a template with team permissions and a CI/CD collaborator |
| `reporting/github-monthly-issues-report` | Generate an HTML report of issues created within a date range |
| `reporting/github-repo-permissions-report` | Export repository collaborator/team permissions and branch-approval bypass actors to CSV |
| `reporting/github-copilot-report` | GitHub Copilot Enterprise licence and AI credit usage report, optionally enriched with Entra ID department data |

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }}
Expand Down
4 changes: 2 additions & 2 deletions enterprise/github-dockerfile-discovery/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }}
Expand Down
4 changes: 2 additions & 2 deletions enterprise/github-get-consumed-licenses/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
4 changes: 2 additions & 2 deletions enterprise/github-get-public-repos/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }}
Expand Down
115 changes: 115 additions & 0 deletions lib/github-common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,17 @@
# print_status / print_success / print_warning / print_error
# require_env_var <VAR> [description] — exit if variable is unset/empty
# require_command <cmd> — 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 <VAR_NAME> — verify a secondary token variable
# validate_slug <value> [label] — exit if value contains unsafe chars
# gh_api <path|url> [curl args...] — Bearer-auth REST helper with retry
# gh_api_paginate <path> [filter] [version] — paginated REST, follows Link headers
# _paginate_orgs_endpoint <filter> <url_tpl> — page through an org list
# _graphql_enterprise_orgs — GraphQL cursor-based enterprise orgs
# get_enterprise_orgs — three-tier enterprise org resolver
Expand Down Expand Up @@ -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 <TOKEN_VAR_NAME> [bearer]
## Validates the token stored in the named variable by calling the /user endpoint.
Expand Down Expand Up @@ -174,6 +204,73 @@ gh_api() {
return 1
}

###
## gh_api_paginate <path_or_url> [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: <next-url>; 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 <jq_filter> <url_template>
## Internal helper: pages through an org-list REST endpoint, printing one
Expand Down Expand Up @@ -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
4 changes: 2 additions & 2 deletions org-admin/github-add-repo-collaborators-by-pattern/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }}
Expand Down
Loading