fix!: migrate to the current Copilot metrics API, harden CI, correct docs - #326
fix!: migrate to the current Copilot metrics API, harden CI, correct docs#326austenstone wants to merge 12 commits into
Conversation
|
Post-merge checklistThree things I can't do from here: 1. Rotate Needs 2. Tag 3. The 12 open Dependabot PRs are all superseded by the dependency upgrade in here (#325, #324, #321, #318, #316, #311, #309, #284, #282, #266, #229, #131). Every one targets a package this PR takes past the version it proposes, so they should close themselves on merge. I left them alone rather than spamming close comments first. Unverifiable with my credentials
|
|
Two more commits after the checklist above, both addressing item 2 from it.
This means the only manual step left post-merge is cutting the |
run() is async, so the surrounding synchronous try/catch could never observe a rejection. Any API failure surfaced as an unhandled promise rejection instead of a clean setFailed message. ts-jest, @types/jest and ts-loader were unused (this repo builds with ncc and tests with vitest). ts-loader pulled in webpack, which accounted for most of the npm audit findings. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The legacy metrics endpoints were sunset on 2026-04-02 and now return 404:
GET /orgs/{org}/copilot/metrics
GET /orgs/{org}/copilot/usage
GET /orgs/{org}/team/{team_slug}/copilot/metrics
They are replaced by report endpoints that return signed download links to
NDJSON files, with an entirely different schema.
- add src/report.ts to resolve download links and parse NDJSON
- add src/types.ts with types derived from real report data
- rewrite the usage job summary against the new schema
- wire up the enterprise input, which action.yml declared but never read
- rebuild team support by joining the user report against user-teams
- normalize dirty IDE labels and drop zero-value pie slices
- replace the test fixture with a real 28-day report
BREAKING CHANGE: output shape follows the new report schema. Team metrics are
derived from user-level reports, which need additional token permissions.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
node20 is deprecated and is being removed from the runners this fall, which would break the action outright. - action.yml: runs.using node20 -> node24 - pin third-party actions by commit SHA - add timeout-minutes to every job - add a dist/ sync check so a stale bundle fails CI - stop ignoring lint failures now that lint is clean - pin the CI node version and cache npm - drop the duplicate scorecards.yml and grant scorecard.yml the permissions it needs - cap the seat assignments table, which could otherwise exceed the 1MiB job summary limit and take the whole report down with it Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- link the current metrics docs instead of the sunset usage endpoints - document the real token scopes per docs, and note GITHUB_TOKEN will not work - list the endpoints the action actually calls - add the missing enterprise, json and artifact-name inputs - add the missing result-org-details, result-seats and days outputs - add an enterprise example now that the input is wired up - add a v6 migration section covering the renamed result fields - bump the pinned version in every example Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Majors: @actions/core 1->3, @actions/github 6->9, @actions/artifact 2->6, eslint 9->10, @eslint/js 9->10, globals 16->17, dotenv 16->17, @types/node 22->26, vitest 3->4. npm audit goes from 25 vulnerabilities (2 critical, 12 high) to 0. Breaking changes handled: - @actions/core 3 moved the summary export to the package root - @actions/artifact 6 is now ESM-only; verified ncc still bundles it by running the built dist end to end rather than trusting unit tests Raise the TypeScript target to ES2022 so ErrorOptions resolves. That type is what @octokit/request-error needs, and the ES2020 target was the actual cause rather than a broken dependency, so no skipLibCheck is required. Stay on TypeScript 5.9: 7.0 compiles but breaks both typescript-eslint and the ncc ts-loader, and buys nothing at runtime. Bump to 6.0.0 for the metrics API migration. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…rrors A failing run reported 'Bad credentials', which tells nobody which token or scope is wrong. Map 401, 403 and 404 to messages that name the scope required, call out that GITHUB_TOKEN cannot read Copilot metrics, and detect the disabled 'Copilot usage metrics' policy specifically. Verified against a real 401 from the live API, not only via unit tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Covers what Dependabot had open: codeql-action v3 -> v4, dependency-review-action v4.7.1 -> v5.0.0 (node24 runtime only, no config change), harden-runner v2.12.0 -> v2.21.0, plus the checkout, upload-artifact and scorecard-action pins that were still on v4-era SHAs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Verified all nine inputs the workflow passes still exist in v18 before bumping; the intervening majors are dependency maintenance releases. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
ESLint 10's preserve-caught-error caught a rethrow that dropped the underlying error. The rule only fired in CI because 'eslint src/**/*.ts' expands to nothing locally without globstar, so the lint script was silently checking zero files. Point it at the directory instead; that now covers all seven source files. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was no release automation at all, and the missing v5 tag is what that cost: v1-v4 exist, v5.0-v5.2 exist, but nothing consumers could pin as @v5. Doing this by hand fails quietly and nobody notices until someone's workflow can't resolve the ref. Rebuilding dist/ before moving the tag means a stale bundle can't get published under a major version, and prereleases are skipped so an rc never hijacks the stable tag. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 74820ff6-756f-483f-afd2-c1cf19a1eb19
Anyone who tries @v5 gets an unresolvable ref with no explanation. Say why it does not exist and point them at v6. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 74820ff6-756f-483f-afd2-c1cf19a1eb19
The team path derives day totals from the user report, and that aggregation only ever set daily_active_users. Everything reading weekly_ or monthly_active_users therefore got undefined and rendered zero, so a team summary claimed "Active Users (28 day) 0" while the daily figures sat between 8 and 23, and the weekly line on the active users chart was flat along the axis. These are trailing windows rather than per-day counts, so they need the whole series before they can be resolved. Windowing is done by date instead of array position because a day with no activity is simply absent from the report, and counting positions would stretch the window across those gaps. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 74820ff6-756f-483f-afd2-c1cf19a1eb19
d0e3909 to
a80f835
Compare
The problem
This action has been broken for every user since GitHub sunset the Copilot usage/metrics endpoints.
I verified this against a live org (
octodemo) with a token that has full access, so these are sunsets rather than permission errors:GET /orgs/{org}/copilot/usage404GET /orgs/{org}/copilot/metrics404GET /orgs/{org}/team/{team_slug}/copilot/metrics404The replacement is a different shape entirely: report endpoints that return signed download links to NDJSON files rather than returning metrics inline.
What this does
Migrates the data layer to the report endpoints. New
src/report.tshandles fetch + download + NDJSON parse; newsrc/types.tsholds types derived from real downloaded payloads rather than from the docs.Wires up
enterprise. It has been declared inaction.ymland silently ignored. It now works.Rebuilds team support. The new API has no team endpoint, so team figures are derived by joining the user report against the user-teams report.
Fixes an async error-handling bug.
src/index.tswrapped an asyncrun()in a synchronoustry/catch, so every rejection escaped as an unhandled rejection instead of failing the step. Confirmed with a standalone repro, and confirmed fixed during an end-to-end run.Fixes a latent 1MiB overflow. In my e2e run the seat table was 318KB of a 327KB summary at ~1500 seats. GitHub caps job summaries at 1MiB, so a ~5000-seat org would have lost the entire report including the 8KB of usage data. Capped at 1000 rows with a notice pointing at the
jsoninput.Moves to
node24. Node 20 is EOL as of April 2026 and node20 removal from the runners is estimated for fall 2026, at which pointnode20actions fail outright.Hardens CI. Third-party actions pinned by SHA,
timeout-minuteseverywhere, least-privilegepermissions:, and a new check that fails if the committeddist/is out of sync with source. I confirmednccoutput is byte-for-byte deterministic before adding that gate so it will not be flaky.Corrects the README, which documented dead endpoints, wrong token scopes, and was missing three inputs and three outputs.
Verification
Beyond
tsc/ lint / 13 passing tests, I ran the builtdist/index.jsend-to-end against the live API with real credentials. It fetched 28 real days, wrote the job summary, and emitted all six outputs. Tests run against a real 460KB NDJSON report captured from the API rather than a hand-written mock.Some things real data taught me that the docs did not:
day_totalsarrives unsorted, so sorting is required for correctness, not cosmetics.totals_by_languageortotals_by_model, only the paired variants. The unpaired names returnnull.totals_by_idecontains dirty values in production ("1.133.0:"next to"vscode"), now bucketed tounknown.user-teams-28-day/latestdoes not exist. Only the 1-day variant does.Breaking
This warrants a v6. The
result/csv/xml/jsonfield names follow the new schema (dateis nowday,total_suggestions_countis nowcode_generation_activity_count, and so on), andteamnow requires a token that can read user-level metrics. Migration notes are in the README.Not verified
I could not exercise the enterprise path end-to-end — my token lacks
admin:enterpriseandenterprise-28-dayreturns403. It is written defensively but untested against a live enterprise.Added after opening
All dependencies to latest;
npm audit25 → 0. Majors:@actions/core1→3,@actions/github6→9,@actions/artifact2→6, eslint 9→10, vitest 3→4,@types/node22→26, globals 16→17, dotenv 16→17.@actions/artifactv6 is ESM-only now, which unit tests would not have caught, so I re-ran the builtdist/index.jsend to end to confirm ncc still bundles it.I did not take TypeScript 7. It compiles, but breaks both
typescript-eslintand ncc'sts-loader, and buys nothing at runtime. Staying on 5.9.The dependency agent had reached for
skipLibCheck: trueto silence a@octokit/request-errortype error. That was a misdiagnosis:ErrorOptionsships inlib.es2022, and the real cause was atarget: ES2020that predates it. Raising the target fixes it properly without disabling lib checks repo-wide.Actionable error messages. A run that fails auth reported
Bad credentials, which tells nobody which token or scope is wrong. 401/403/404 now name the required scope, call out thatGITHUB_TOKENcannot read Copilot metrics, and specifically detect the disabled Copilot usage metrics policy. Verified against a real 401.The lint script was checking zero files.
eslint src/**/*.tsexpands to nothing locally without globstar, so lint passed vacuously. Pointing it atsrclinted all seven files and immediately surfaced a real rethrow that dropped itscause. Both fixed.Remaining action pins updated, covering what Dependabot had open: codeql-action v3→v4, dependency-review-action v4.7.1→v5.0.0, harden-runner v2.12.0→v2.21.0, action-send-mail v5→v18 (verified all nine inputs still exist first), plus checkout/upload-artifact/scorecard pins still on v4-era SHAs.
Supersedes
#325, #324, #321, #318, #316, #311, #309, #284, #282, #266, #229, #131 — every one is either upgraded past or made moot here (
ts-jestin #311 was removed outright as it was unused).Why one PR rather than several
The batches are interdependent: the dist-sync gate needs the rebuilt bundle, which needs the dependency upgrade, which needs the ES2022 target. Splitting produces intermediate commits with failing CI. Each commit is individually focused and reviewable.
Needs you
secrets.TOKENon this repo is expired — the Usage workflow fails with a 401 independent of these changes. That is why the Usage check is red. Rotating it is the one thing I cannot do.