Skip to content

github-repo-stats

github-repo-stats #176

name: github-repo-stats
on:
schedule:
# Run this once per day, towards the end of the day for keeping the most
# recent data point most meaningful (hours are interpreted in UTC).
- cron: "0 23 * * *"
workflow_dispatch: # Allow for running this manually.
permissions:
contents: read
jobs:
j1:
name: github-repo-stats
runs-on: ubuntu-latest
steps:
- name: generate app token
id: app-token
uses: actions/create-github-app-token@v3
with:
app-id: 3189942
private-key: ${{ secrets.POWER_PLATFORM_SKILLS_APP_PRIVATE_KEY }}
permission-administration: read
permission-contents: write
permission-metadata: read
- name: verify traffic API access
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
set -euo pipefail
response_headers="$(mktemp)"
trap 'rm -f "${response_headers}"' EXIT
status="$(
curl -sS -o /dev/null -D "${response_headers}" -w "%{http_code}" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/${{ github.repository }}/traffic/popular/referrers"
)"
if [ "${status}" != "200" ] && [ "${status}" != "204" ]; then
echo "GitHub traffic API preflight failed with HTTP ${status}."
echo "The github-repo-stats action needs an installation token with repository Administration: read access."
awk 'BEGIN { IGNORECASE = 1 } /^x-accepted-github-permissions:|^x-oauth-scopes:|^x-github-api-version-selected:|^x-ratelimit-/ { print }' "${response_headers}"
exit 1
fi
- name: checkout repository
uses: actions/checkout@v4
- name: checkout github-repo-stats action
uses: actions/checkout@v4
with:
repository: jgehrcke/github-repo-stats
ref: RELEASE
path: .github/actions/github-repo-stats
- name: patch github-repo-stats stargazer fetch
shell: bash
run: |
python3 <<'PY'
from pathlib import Path
path = Path(".github/actions/github-repo-stats/fetch.py")
text = path.read_text()
import_line = "from github import Github, Repository # type: ignore"
if import_line not in text:
raise RuntimeError("Expected PyGithub import line was not found")
text = text.replace(
import_line,
"from github import Github, Repository, GithubException # type: ignore",
)
old_stargazer_fetch = "\n".join(
[
" # TODO for addressing the 10ks challenge: save state to disk, and refresh",
" # using reverse order iteration. See for repo in user.get_repos().reversed",
" for count, gazer in enumerate(repo.get_stargazers_with_dates(), 1):",
" # Store `PullRequest` object with integer key in dictionary.",
" gazers.append(gazer)",
" if count % 200 == 0:",
' log.info("%s gazers fetched", count)',
]
) + "\n"
if old_stargazer_fetch not in text:
raise RuntimeError("Expected REST stargazer fetch block was not found")
new_stargazer_fetch = "\n".join(
[
" # GitHub restricted the REST stargazers listing in July 2026 to admins and",
" # collaborators. Installation tokens with repository Administration access",
" # can still read the same timestamp data through GraphQL, so keep the",
" # upstream REST path for normal tokens and fall back only for that specific",
" # restriction.",
" # See:",
" # - https://docs.github.com/en/rest/activity/starring#list-stargazers",
" # - https://github.blog/changelog/2026-06-30-upcoming-access-restrictions-to-public-api-endpoints-and-ui-views/",
" try:",
" # TODO for addressing the 10ks challenge: save state to disk, and refresh",
" # using reverse order iteration. See for repo in user.get_repos().reversed",
" for count, gazer in enumerate(repo.get_stargazers_with_dates(), 1):",
" gazers.append(gazer)",
" if count % 200 == 0:",
' log.info("%s gazers fetched", count)',
" except GithubException as exc:",
' if exc.status != 403 or "permission to view the stargazers" not in str(exc):',
" raise",
' log.warning("REST stargazer endpoint denied access; retry via GraphQL")',
" gazers = get_stargazers_with_dates_graphql(repo.full_name)",
]
) + "\n"
text = text.replace(old_stargazer_fetch, new_stargazer_fetch)
graphql_helper = r'''
def get_stargazers_with_dates_graphql(repo_full_name):
owner, name = repo_full_name.split("/", 1)
query = """
query($owner: String!, $name: String!, $cursor: String) {
repository(owner: $owner, name: $name) {
stargazers(
first: 100
after: $cursor
orderBy: {field: STARRED_AT, direction: ASC}
) {
pageInfo {
hasNextPage
endCursor
}
edges {
starredAt
}
}
}
}
"""
class StargazerEvent:
def __init__(self, starred_at):
# Match PyGithub's REST shape here: get_stargazers_with_dates()
# exposes starred_at as a UTC timestamp without tzinfo. The caller
# localizes it to UTC before building the pandas index.
self.starred_at = starred_at
gazers = []
cursor = None
while True:
response = requests.post(
"https://api.github.com/graphql",
headers={
"Authorization": f"Bearer {os.environ['GHRS_GITHUB_API_TOKEN'].strip()}",
"Content-Type": "application/json",
},
json={"query": query, "variables": {"owner": owner, "name": name, "cursor": cursor}},
timeout=60,
)
response.raise_for_status()
payload = response.json()
if payload.get("errors"):
raise RuntimeError(f"GitHub GraphQL stargazer fetch failed: {payload['errors']}")
stargazers = payload["data"]["repository"]["stargazers"]
for edge in stargazers["edges"]:
starred_at = datetime.fromisoformat(
edge["starredAt"].replace("Z", "+00:00")
).replace(tzinfo=None)
gazers.append(StargazerEvent(starred_at))
if len(gazers) % 200 == 0:
log.info("%s gazers fetched", len(gazers))
if not stargazers["pageInfo"]["hasNextPage"]:
return gazers
cursor = stargazers["pageInfo"]["endCursor"]
def handle_rate_limit_error(exc):
'''
helper_insert_point = "\ndef handle_rate_limit_error(exc):\n"
if helper_insert_point not in text:
raise RuntimeError("Expected helper insertion point was not found")
text = text.replace(helper_insert_point, "\n" + graphql_helper.strip() + "\n")
path.write_text(text)
PY
- name: run-ghrs
# Run the checked-out RELEASE action after applying the stargazer fallback patch above.
uses: ./.github/actions/github-repo-stats
with:
repository: microsoft/power-platform-skills
ghtoken: ${{ steps.app-token.outputs.token }}
databranch: github-repo-stats
ghpagesprefix: https://microsoft.github.io/power-platform-skills