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
16 changes: 14 additions & 2 deletions .github/workflows/archive-traffic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,21 @@ jobs:

- name: Collect traffic
env:
GH_TRAFFIC_TOKEN: ${{ secrets.TRAFFIC_ARCHIVE_TOKEN || github.token }}
# Traffic endpoints reject the Actions GITHUB_TOKEN ("Resource not
# accessible by integration") — a PAT from a user with push access
# is required: classic with repo scope, or fine-grained with read
# access to Administration on this repository.
GH_TRAFFIC_TOKEN: ${{ secrets.TRAFFIC_ARCHIVE_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: node scripts/archive-github-traffic.mjs --archive-dir "${TRAFFIC_ARCHIVE_DIR}"
run: |
set -euo pipefail

if [ -z "${GH_TRAFFIC_TOKEN}" ]; then
echo "::error::No traffic-capable token configured. Set the TRAFFIC_ARCHIVE_TOKEN secret to a PAT with push access (classic: repo scope; fine-grained: Administration read)."
exit 1
fi

node scripts/archive-github-traffic.mjs --archive-dir "${TRAFFIC_ARCHIVE_DIR}"

- name: Commit archive
run: |
Expand Down
9 changes: 8 additions & 1 deletion scripts/archive-github-traffic.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,14 @@ const fetchJson = async ({ repo, token, pathname, fetchImpl }) => {
if (!response.ok) {
const body = await response.text().catch(() => '');
const suffix = body ? ` ${body.slice(0, 500)}` : '';
throw new Error(`GitHub traffic API request failed for ${repo}: ${url.pathname}${url.search} returned ${response.status}.${suffix}`);
const lacksPushAccess = response.status === 403
&& /resource not accessible|must have push access/i.test(body);
const hint = lacksPushAccess
? ' Traffic endpoints require a token with push access to the repository; the Actions GITHUB_TOKEN is always rejected. Use a classic PAT with the repo scope or a fine-grained PAT with read access to Administration.'
: response.status === 401
? ' The token was rejected as invalid — it may be expired or revoked. Rotate the TRAFFIC_ARCHIVE_TOKEN secret.'
Comment on lines +326 to +329

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The invalid-token hint is gated only on response.status === 401, but GitHub can return 403/404 for auth failures too — should we key this off the error body (for example Bad credentials) or include those cases as well?

Severity web_search

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
scripts/archive-github-traffic.mjs around lines 321-332 inside the `if (!response.ok)`
error handling, the token-invalid/expired hint is currently only shown when
`response.status === 401` (lines ~328-330). Refactor the `hint` selection so it also
covers GitHub’s documented auth/permission failure variants (notably 403 and 404) by
keying off the response body text (e.g., look for “Bad credentials” /
authentication-failure phrases) rather than status alone, while keeping the existing
dedicated “missing push access” hint for 403 with the “resource not
accessible/must have push access” messages. Ensure the body-based checks are used in
the right precedence order so unrelated 401s don’t incorrectly get the expired-token
message, and revoked/expired tokens get the correct hint for 401/403/404 shapes.

: '';
throw new Error(`GitHub traffic API request failed for ${repo}: ${url.pathname}${url.search} returned ${response.status}.${suffix}${hint}`);
}

return response.json();
Expand Down
37 changes: 36 additions & 1 deletion scripts/test-github-traffic-archive.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,40 @@ test('fetchGitHubTraffic requests the daily GitHub traffic endpoints with auth',
assert.deepEqual(snapshot.clones.clones, responses[`/repos/${TEST_REPOSITORY}/traffic/clones?per=day`].clones);
});

test('fetchGitHubTraffic explains traffic token requirements on 403', async () => {
const fetchImpl = async () => new globalThis.Response(
JSON.stringify({ message: 'Resource not accessible by integration' }),
{ status: 403 },
);

await assert.rejects(
fetchGitHubTraffic({
repo: TEST_REPOSITORY,
token: 'installation-token',
capturedAt,
fetchImpl,
}),
/returned 403\..*push access/,
);
});

test('fetchGitHubTraffic flags invalid tokens on 401', async () => {
const fetchImpl = async () => new globalThis.Response(
JSON.stringify({ message: 'Bad credentials' }),
{ status: 401 },
);

await assert.rejects(
fetchGitHubTraffic({
repo: TEST_REPOSITORY,
token: 'expired-token',
capturedAt,
fetchImpl,
}),
/returned 401\..*expired or revoked/,
);
});

test('mergeTrafficArchive upserts daily views and clones without double-counting overlapping windows', () => {
const archive = mergeTrafficArchive(
{
Expand Down Expand Up @@ -232,7 +266,8 @@ test('traffic archive workflow uses a daily schedule and a dedicated archive bra

assert.match(workflow, /cron:\s+'17 3 \* \* \*'/);
assert.match(workflow, /TRAFFIC_ARCHIVE_BRANCH:\s+traffic-archive/);
assert.match(workflow, /TRAFFIC_ARCHIVE_TOKEN/);
assert.match(workflow, /GH_TRAFFIC_TOKEN:\s*\$\{\{\s*secrets\.TRAFFIC_ARCHIVE_TOKEN\b/);
assert.doesNotMatch(workflow, /GH_TRAFFIC_TOKEN:[^\n]*github\.token/);
assert.match(workflow, /node scripts\/archive-github-traffic\.mjs/);
assert.match(workflow, /git add traffic\/archive\.json traffic\/summary\.json/);
assert.match(workflow, /git rm --ignore-unmatch traffic\/README\.md/);
Expand Down
Loading