-
Notifications
You must be signed in to change notification settings - Fork 153
193 lines (174 loc) · 9.87 KB
/
Copy pathgithub-repo-stats.yml
File metadata and controls
193 lines (174 loc) · 9.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
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@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
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@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: checkout github-repo-stats action
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
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