From 1f71938913f9e4d368ce41b3377967dc79686c94 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:35:03 -0400 Subject: [PATCH] ci: emit the api-calls notice from ci-status, post-merge-dispatch and intake-reenter The hub ranks GITHUB_TOKEN spend by each job's api-calls notice, and these three jobs called the API without one. - api-budget.bash gains api_budget_emit_calls, the api-calls line the log-api-budget action computed inline; the action now calls it. - ci-status carries api-budget.bash verbatim, so it gets the function and emits the line beside its api-budget notice. - post-merge-dispatch (pr-merge.yml) and intake-reenter name API_BUDGET_TOKEN: github-token and end with the log-api-budget step (if: always()), as the other heavy lane jobs do. The step reads only $RUNNER_TEMP files and sends no API call. Co-Authored-By: Claude Opus 5.5 --- .github/actions/log-api-budget/action.yml | 11 ++++------- .github/actions/log-api-budget/api-budget.bash | 15 +++++++++++++++ .../log-api-budget/log-api-budget.test.mjs | 6 ++++++ .github/scripts/pr-merge/merge-job.test.mjs | 1 + .github/workflows/intake-reenter.yml | 12 ++++++++++++ .github/workflows/pr-merge.yml | 13 +++++++++++++ .github/workflows/pr-require-checks.yml | 17 ++++++++++++++++- 7 files changed, 67 insertions(+), 8 deletions(-) diff --git a/.github/actions/log-api-budget/action.yml b/.github/actions/log-api-budget/action.yml index 303fb80e546..d0863463f1a 100644 --- a/.github/actions/log-api-budget/action.yml +++ b/.github/actions/log-api-budget/action.yml @@ -43,13 +43,10 @@ runs: printf '%s\n' "$lines" sed 's/^::notice title=api-budget::/api-budget: /' <<<"$lines" >>"$GITHUB_STEP_SUMMARY" # This job's own calls, one reading each: unlike cost, no other job's. - # A notice, as the hub reads annotations, not the log; its message - # escaped as workflow commands require. - calls=$(awk '$1 != 304 { n++ } $1 == 304 { m++ } END { printf "%s=%d not-modified=%d", token, n, m }' token="$name" "$RUNNER_TEMP/api-budget/$name") - notice=${calls//%/%25} - notice=${notice//$'\r'/%0D} - echo "::notice title=api-calls::${notice//$'\n'/%0A}" - echo "api-calls $calls" >>"$GITHUB_STEP_SUMMARY" + # A notice, as the hub reads annotations, not the log. + calls=$(api_budget_emit_calls "$name") + printf '%s\n' "$calls" + echo "api-calls ${calls#*title=api-calls::}" >>"$GITHUB_STEP_SUMMARY" cost=${lines##*cost=} costs+=("$name-cost=${cost/na/unknown}") done diff --git a/.github/actions/log-api-budget/api-budget.bash b/.github/actions/log-api-budget/api-budget.bash index f777f289a70..776124bdad7 100644 --- a/.github/actions/log-api-budget/api-budget.bash +++ b/.github/actions/log-api-budget/api-budget.bash @@ -70,6 +70,11 @@ # with the same reset and resource, else na # api_budget_emit_content the notice for the job's content-creating # writes: count and most in any 60 s +# api_budget_emit_calls ... +# the hub's api-calls notice for each token with +# a reading, `=N not-modified=M`: N is +# the job's own calls, M its 304 answers, which +# GitHub does not count # # pr-require-checks.yml's ci-status job carries this file verbatim in its # API_BUDGET_SH env (it has no checkout of this directory); @@ -367,6 +372,16 @@ api_budget_emit_content() { return 0 } +api_budget_emit_calls() { + local token file + for token; do + file=${RUNNER_TEMP:-}/api-budget/$token + [[ -n ${RUNNER_TEMP:-} && $token =~ ^[a-z][a-z0-9-]{0,39}$ && -s $file ]] || continue + awk '$1 != 304 { n++ } $1 == 304 { m++ } END { printf "::notice title=api-calls::%s=%d not-modified=%d\n", token, n, m }' token="$token" "$file" + done + return 0 +} + api_budget_emit() { local token file where workflow=${GITHUB_WORKFLOW_REF:-} workflow=${workflow%%@*} diff --git a/.github/actions/log-api-budget/log-api-budget.test.mjs b/.github/actions/log-api-budget/log-api-budget.test.mjs index 489ec2f6ac5..052d03be5a2 100644 --- a/.github/actions/log-api-budget/log-api-budget.test.mjs +++ b/.github/actions/log-api-budget/log-api-budget.test.mjs @@ -615,6 +615,8 @@ test("each heavy lane job names its API_BUDGET_TOKEN and ends with the log-api-b // label-unblocked's ACTIONS_TOKEN client records under github-token, so // that job logs both: [API_BUDGET_TOKEN, the tokens it logs]. "work-unblock.yml": { "wip-state": "github-token", "label-unblocked": ["lanes-app", "lanes-app github-token"] }, + "pr-merge.yml": { "post-merge-dispatch": "github-token" }, + "intake-reenter.yml": { reenter: "github-token" }, }; for (const [file, tokens] of Object.entries(jobs)) { const text = readFileSync(new URL(`../../workflows/${file}`, import.meta.url), "utf8"); @@ -672,6 +674,10 @@ test("ci-status carries api-budget.bash verbatim in its API_BUDGET_SH env", () = assert.equal(carried, readFileSync(LIB, "utf8")); }); +test("ci-status emits the api-calls notice beside its api-budget notice", () => { + assert.match(CI, /api_budget_emit github-token; api_budget_emit_calls github-token; /); +}); + // pr-feedback-dispatch.yml's post-verify-notes and rerun-ci-status jobs post // no other notice, so without these lines their calls go unattributed. // post-verify-notes' notice covers only its artifact listing, the one call diff --git a/.github/scripts/pr-merge/merge-job.test.mjs b/.github/scripts/pr-merge/merge-job.test.mjs index 25e3b18b80a..a81c3ffce16 100644 --- a/.github/scripts/pr-merge/merge-job.test.mjs +++ b/.github/scripts/pr-merge/merge-job.test.mjs @@ -58,6 +58,7 @@ test("the re-dispatch for earlier lane merges waits on neither the listing nor t "Record the dispatched run", "Re-run a red lane-dispatched full run", "Record the re-run", + "Log the API budget at the end", ], ); assert.doesNotMatch(early, /steps\.candidates|steps\.gate|needs\.merge/); diff --git a/.github/workflows/intake-reenter.yml b/.github/workflows/intake-reenter.yml index 559d732fb24..f5327894cf8 100644 --- a/.github/workflows/intake-reenter.yml +++ b/.github/workflows/intake-reenter.yml @@ -76,6 +76,10 @@ jobs: concurrency: group: intake-reenter-${{ github.event.issue.number }} queue: max + # Each guarded API call's rate-limit reading, for the job-end api-budget + # notice (.github/actions/log-api-budget). + env: + API_BUDGET_TOKEN: github-token steps: - name: Check out the default branch tip uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -114,3 +118,11 @@ jobs: TRUSTED_ACTORS_PATH: .base/.github/standards/trusted-actors/trusted-actors.json KILL_SWITCH: proceed run: node .base/.github/scripts/intake-reenter/reenter.mjs + + - name: Log the API budget at the end + if: always() + continue-on-error: true + timeout-minutes: 1 + uses: ./.base/.github/actions/log-api-budget # zizmor: ignore[self-repository] + with: + tokens: github-token diff --git a/.github/workflows/pr-merge.yml b/.github/workflows/pr-merge.yml index f5e418eb808..9aa0ccc8489 100644 --- a/.github/workflows/pr-merge.yml +++ b/.github/workflows/pr-merge.yml @@ -128,6 +128,11 @@ jobs: # The pool and reset a rate limit stopped this job's GITHUB_TOKEN calls on, # if one did: the merge and enable-auto-merge jobs then send nothing. rate-limited: ${{ steps.post-merge.outputs.rate-limited || steps.dispatch.outputs.stop || steps.rerun.outputs.stop }} + # Each guarded API call's rate-limit reading, for the job-end api-budget + # notice (.github/actions/log-api-budget). The full-run dispatch's own + # `gh api -i` POST leaves none. + env: + API_BUDGET_TOKEN: github-token steps: - name: Check out the default branch tip uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -501,6 +506,14 @@ jobs: retention-days: 90 if-no-files-found: error + - name: Log the API budget at the end + if: always() + continue-on-error: true + timeout-minutes: 1 + uses: ./.base/.github/actions/log-api-budget # zizmor: ignore[self-repository] + with: + tokens: github-token + merge: # On the schedule and a hand-started run it waits for post-merge-dispatch # and reuses its report (the post-merge check below). Where that job is diff --git a/.github/workflows/pr-require-checks.yml b/.github/workflows/pr-require-checks.yml index 1e9c3968420..91813955143 100644 --- a/.github/workflows/pr-require-checks.yml +++ b/.github/workflows/pr-require-checks.yml @@ -3346,6 +3346,11 @@ jobs: # with the same reset and resource, else na # api_budget_emit_content the notice for the job's content-creating # writes: count and most in any 60 s + # api_budget_emit_calls ... + # the hub's api-calls notice for each token with + # a reading, `=N not-modified=M`: N is + # the job's own calls, M its 304 answers, which + # GitHub does not count # # pr-require-checks.yml's ci-status job carries this file verbatim in its # API_BUDGET_SH env (it has no checkout of this directory); @@ -3643,6 +3648,16 @@ jobs: return 0 } + api_budget_emit_calls() { + local token file + for token; do + file=${RUNNER_TEMP:-}/api-budget/$token + [[ -n ${RUNNER_TEMP:-} && $token =~ ^[a-z][a-z0-9-]{0,39}$ && -s $file ]] || continue + awk '$1 != 304 { n++ } $1 == 304 { m++ } END { printf "::notice title=api-calls::%s=%d not-modified=%d\n", token, n, m }' token="$token" "$file" + done + return 0 + } + api_budget_emit() { local token file where workflow=${GITHUB_WORKFLOW_REF:-} workflow=${workflow%%@*} @@ -5211,7 +5226,7 @@ jobs: env: LANES: ${{ toJSON(needs) }} run: | - [ -z "${API_BUDGET_SH:-}" ] || { eval "$API_BUDGET_SH"; api_budget_emit github-token; api_budget_emit_content; } + [ -z "${API_BUDGET_SH:-}" ] || { eval "$API_BUDGET_SH"; api_budget_emit github-token; api_budget_emit_calls github-token; api_budget_emit_content; } results=$(jq -r ' .["select-tests"] as $s | to_entries