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
19 changes: 19 additions & 0 deletions .github/actions/log-api-budget/log-api-budget.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,7 @@ test("each heavy lane job names its API_BUDGET_TOKEN and ends with the log-api-b
"pr-feedback-dispatch.yml": { "post-verify-notes": "github-token", dispatch: "github-token", "rerun-ci-status": "github-token" },
"post-merge-test-mutation.yml": { report: "github-token" },
"pr-refine.yml": { "fix-docs-head": "github-token" },
"post-merge-demotion-alert.yml": { alert: "github-token", "report-red-main": "github-token" },
};
for (const [file, tokens] of Object.entries(jobs)) {
const text = readFileSync(new URL(`../../workflows/${file}`, import.meta.url), "utf8");
Expand All @@ -815,6 +816,23 @@ test("each heavy lane job names its API_BUDGET_TOKEN and ends with the log-api-b
}
});

// A job's shell steps call gh through api-budget.bash's wrapper, so the
// job-end step has their readings to count; a step that sends gh before it
// wraps would leave those calls out.
test("each shell step in the demotion alert job that calls gh wraps it first", () => {
const text = readFileSync(new URL("../../workflows/post-merge-demotion-alert.yml", import.meta.url), "utf8");
const block = /\n alert:\n([\s\S]*?)(?=\n [a-z0-9-]+:\n|$)/.exec(text)?.[1] ?? "";
const wrap = "{ . .base/.github/actions/log-api-budget/api-budget.bash 2>/dev/null && api_budget_wrap_gh github-token; } || true";
const calling = block.split(/\n {6}- name: /).filter((step) => /^ {10}[^#\n]*\bgh api\b/m.test(step));
assert.ok(calling.length >= 2, "the alert job's gh-calling steps are found");
for (const step of calling) {
const run = step.indexOf(" run: |\n");
assert.ok(run > 0, `${step.split("\n")[0]} is a run step`);
const at = step.indexOf(wrap);
assert.ok(at > run && at < step.search(/^ {10}[^#\n]*\bgh api\b/m), `${step.split("\n")[0]} wraps gh before its first call`);
}
});

// pr-review-main's record job has no checkout of the action, and its only
// calls are one check-run POST per verdict file, so it counts them itself and
// ends with the notice in api_budget_emit_calls' shape.
Expand Down Expand Up @@ -847,6 +865,7 @@ test("each job that holds the job token and calls no guarded client ends with th
"pr-test-windows.yml": Object.fromEntries(windows.map((job) => [job, "./"])),
"post-merge-detect-silent-revert.yml": { "detect-silent-revert": "./" },
"release-publish-plugins.yml": { merge: "./", build: "./" },
"dependabot-fragments.yml": { write: "./" },
};
for (const [file, byJob] of Object.entries(jobs)) {
const text = readFileSync(new URL(`../../workflows/${file}`, import.meta.url), "utf8");
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/dependabot-fragments.yml
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,12 @@ jobs:
gh pr edit "$number" --repo "$GITHUB_REPOSITORY" --body-file "$body"
fi
gh pr merge "$number" --repo "$GITHUB_REPOSITORY" --auto --squash
# The steps above call GitHub on the release App's token, not this one,
# so the line reads 0 unless a step starts spending the job token.
- name: Log the API budget at the end
if: always()
continue-on-error: true
timeout-minutes: 1
uses: ./.github/actions/log-api-budget # zizmor: ignore[self-repository]
with:
tokens: github-token
21 changes: 21 additions & 0 deletions .github/workflows/post-merge-demotion-alert.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ jobs:
# 'true' when every newly failing suite was outside the merges'
# affected set or quarantined: no record, and no red-main report.
excused: ${{ steps.file.outputs.excused }}
# Each guarded API call's rate-limit reading (the gh wrapper below and
# babysit_gh.py under the judge), for the job-end api-budget notice
# (.github/actions/log-api-budget).
env:
API_BUDGET_TOKEN: github-token
steps:
# main's tip, for babysit_postmerge.py --judge-run and the plan script it
# runs in a worktree of the last green SHA; full history for that worktree.
Expand Down Expand Up @@ -185,6 +190,10 @@ jobs:
RUN_TRIGGERING_ACTOR: ${{ github.event.workflow_run.triggering_actor.login }}
RUN_COMMIT_MESSAGE: ${{ github.event.workflow_run.head_commit.message }}
run: |
# Each `gh api` call keeps its rate-limit reading for the job-end
# api-budget notice (.github/actions/log-api-budget).
# shellcheck source=/dev/null
{ . .base/.github/actions/log-api-budget/api-budget.bash 2>/dev/null && api_budget_wrap_gh github-token; } || true
if [[ "$EVENT_NAME" == workflow_dispatch ]]; then
if [[ ! "$INPUT_RUN_ID" =~ ^[1-9][0-9]{0,14}$ ]]; then
echo "::error::run-id must be a run id (digits only)"
Expand Down Expand Up @@ -278,6 +287,10 @@ jobs:
RUN_TRIGGERING_ACTOR: ${{ steps.run.outputs.triggering-actor }}
RUN_COMMIT_MESSAGE: ${{ steps.run.outputs.commit-message }}
run: |
# Each `gh api` call keeps its rate-limit reading for the job-end
# api-budget notice (.github/actions/log-api-budget).
# shellcheck source=/dev/null
{ . .base/.github/actions/log-api-budget/api-budget.bash 2>/dev/null && api_budget_wrap_gh github-token; } || true
case "$RUN_WORKFLOW:$RUN_EVENT" in
pr-require-checks:schedule) signal=schedule ;;
pr-require-checks:workflow_dispatch) signal=dispatch ;;
Expand Down Expand Up @@ -868,6 +881,14 @@ jobs:
exit 1
fi

- 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

# The red-main report. It runs after the alert job, so a re-run the alert
# started is already under way, and only for a full run on main that
# concluded failure: a cancelled, timed-out or not-started run, and the
Expand Down
10 changes: 6 additions & 4 deletions scripts/post-merge-demotion-alert.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,14 @@ if [[ -n "$alert_job" ]] && grep -q '^permissions: {}$' "$WORKFLOW" &&
else
fail "the alert job's permissions are not exactly issues: write, actions: write, checks: read, contents: read, pull-requests: read"
fi
uses="$(grep -E '^[[:space:]]*-?[[:space:]]*uses:' <<<"$alert_job" | sed -E 's/^[[:space:]]*-?[[:space:]]*uses:[[:space:]]*//; s/@.*//')"
# Beside the checkout, only the job-end log-api-budget action, run from that
# checkout of main's tip.
uses="$(grep -E '^[[:space:]]*-?[[:space:]]*uses:' <<<"$alert_job" | sed -E 's/^[[:space:]]*-?[[:space:]]*uses:[[:space:]]*//; s/@.*//; s/[[:space:]]+#.*//')"
checkout="$(awk '/- name: Check out the default branch tip/ { on = 1 } on && /^ - name: Read/ { exit } on' <<<"$alert_job")"
if [[ "$uses" == actions/checkout && "$checkout" == *"ref: refs/heads/main"* && "$checkout" == *"persist-credentials: false"* ]]; then
ok "the alert job's one action checks out main's tip without credentials"
if [[ "$uses" == $'actions/checkout\n./.base/.github/actions/log-api-budget' && "$checkout" == *"ref: refs/heads/main"* && "$checkout" == *"persist-credentials: false"* ]]; then
ok "the alert job checks out main's tip without credentials and runs no other action but that checkout's log-api-budget"
else
fail "the alert job runs an action other than one credential-less checkout of main: '$uses'"
fail "the alert job runs an action other than one credential-less checkout of main and its log-api-budget: '$uses'"
fi
if grep -qF "\${{" "$TMP_ROOT/body.sh"; then
fail "the step body interpolates an expression; pass it through env"
Expand Down
Loading