You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add two new group-by axes to gh-aw-fleet consumption — --by actor and --by trigger — that attribute AI-credit (AIC) spend to what caused each run
rather than which repo/profile/workflow it ran under. --by actor groups by the
run's triggering_actor (e.g. dependabot[bot], a human, github-actions[bot]); --by trigger groups by the run's GitHub event (schedule, push, workflow_dispatch, pull_request, …). Together they answer "what started
blowing up my bill?" — dependabot churn vs. scheduled cron vs. manual dispatch —
in one command instead of eyeballing every repo.
User Story
As a fleet operator under usage-based Copilot billing,
I want to group consumption by the actor or event that triggered each run,
So that when the bill jumps I can name the cause (dependabot, a runaway
schedule, a manual dispatch) instead of only knowing which repo/workflow spent.
Problem Statement
consumption today groups along four axes — repo | profile | cost-center | workflow (cmd/consumption.go:80). All four tell you where spend landed;
none tell you what triggered it. A workflow that runs on both schedule and push rolls both into one --by workflow row, so a dependabot-driven spike is
invisible — it hides inside whatever workflow dependabot's PRs re-run.
This is the sibling half of #198 (--compare). Both ideas came from the same
fleet-observability announcement thread. #198 captured the temporal lens
("why did spend move?" — this period vs. prior, keyed by an existing axis).
This issue captures the orthogonal dimensional lens ("what is spending?" —
a new grouping key). They compose: once these axes exist, a future --compare --by actor can rank which actor's spend moved.
Proposed Solution
Technical Approach
Two new GroupByKind values, plus a run-level enrichment step. Unlike the four
existing axes — which aggregate at the workflow summary level — actor and
event vary per run, so these two axes must bucket at the individual-run
grain (each logsRun already carries its own aic, so per-run summation is the
natural fit).
New data, joined by run_id. The gh aw logs --json payload the rollup
reads (logsRun, internal/fleet/consumption_logs.go:83) carries run_id/conclusion/aic/created_at but noevent or triggering_actor. Add a new seam ghRunsMetaAPI (mirroring ghWorkflowsAPI
/ ghLogsAPI) that lists gh api repos/{repo}/actions/runs?per_page=100 once per repo and returns a run_id → {event, triggering_actor.login}
map. Join it into the already-window-filtered runs by run_id — one extra
list call per repo, not per run, keeping it in the same cost class as the
existing fan-out (relates to consumption --source logs: bounded concurrency + no-download fast path for the per-workflow fan-out #113).
Enum + vocabulary. Add GroupByActor / GroupByTrigger to the GroupByKind iota (internal/fleet/consumption.go:120) and their canonical
names to groupByNames (consumption.go:154); ParseGroupBy and --by validation pick them up for free.
Aggregation. Extend the group-by switch (consumption.go:~762) with a
per-run bucketing path for the two run-grained axes: sum run.AIC / github_api_calls by the run's actor or event, preserving the existing
nil-until-positive AIC semantics per bucket (a bucket whose runs are all
failed/nil rolls up to nil AIC, same nilAICDiag rule).
Rendering. Add ACTOR / TRIGGER headers to byColumnHeader
(cmd/consumption.go:256) and the axis to the --by help + Long usage text.
Files Likely Affected
internal/fleet/consumption.go — GroupByKind enum, groupByNames, the
aggregation switch (new per-run bucketing branch).
internal/fleet/consumption_logs.go — new ghRunsMetaAPI seam; enrich repoRunData/logsRun join with event + actor by run_id.
cmd/consumption.go — --by help text, Long usage block, byColumnHeader.
internal/fleet/testdata/{logs,consumption}/ — new offline fixtures for the
runs-metadata list response.
Acceptance Criteria
consumption --by actor groups AIC/API_CALLS/COST by triggering_actor.login; dependabot[bot] surfaces as its own row.
consumption --by trigger groups by GitHub event
(schedule/push/workflow_dispatch/pull_request/…).
--by actor|trigger is accepted by ParseGroupBy and rejected values
still error (extends the existing FR-005 invalid---by test).
Both axes work under all three temporal modes (--latest / --trailing Nd / --since) and honor --budget highlighting.
Per-run buckets preserve the nil-until-positive AIC rule (all-failed
bucket → nil AIC, not 0.00).
The runs-metadata fetch is one list call per repo (not per run); a
failure surfaces as a diagnostic, not a silent zero.
JSON envelope carries the new axis additively (no cmd.SchemaVersion bump).
make ci passes (fmt, vet, lint, full test suite).
Out of Scope
A blended/curated --by source taxonomy (a single axis folding actor+event
into dependabot|scheduled|manual|release buckets). We deliberately chose two
honest, composable axes over one axis with invented precedence rules.
A distinct "release" bucket.--by trigger surfaces the raw event; a
release shows as release only if the workflow uses on: release, else as push (tag push). No synthetic release classification.
Run-grained vs. workflow-grained is the core divergence. The existing four
axes sum pre-aggregated workflow summaries; actor/event are per-run, so these
two axes need the raw logsRun set (which the fan-out already collects and
window-filters) rather than the WorkflowConsumption summary.
triggering_actor vs actor. Use triggering_actor.login — for re-runs
it names who caused this run, which is the billing-relevant actor.
Offline-testable. All gh/gh api paths go through injection seams; the
new ghRunsMetaAPI follows suit so tests run against testdata/ fixtures
with no network (per the package's established pattern).
Overview
Add two new group-by axes to
gh-aw-fleet consumption—--by actorand--by trigger— that attribute AI-credit (AIC) spend to what caused each runrather than which repo/profile/workflow it ran under.
--by actorgroups by therun's
triggering_actor(e.g.dependabot[bot], a human,github-actions[bot]);--by triggergroups by the run's GitHub event (schedule,push,workflow_dispatch,pull_request, …). Together they answer "what startedblowing up my bill?" — dependabot churn vs. scheduled cron vs. manual dispatch —
in one command instead of eyeballing every repo.
User Story
As a fleet operator under usage-based Copilot billing,
I want to group consumption by the actor or event that triggered each run,
So that when the bill jumps I can name the cause (dependabot, a runaway
schedule, a manual dispatch) instead of only knowing which repo/workflow spent.
Problem Statement
consumptiontoday groups along four axes —repo | profile | cost-center | workflow(cmd/consumption.go:80). All four tell you where spend landed;none tell you what triggered it. A workflow that runs on both
scheduleandpushrolls both into one--by workflowrow, so a dependabot-driven spike isinvisible — it hides inside whatever workflow dependabot's PRs re-run.
This is the sibling half of #198 (
--compare). Both ideas came from the samefleet-observability announcement thread. #198 captured the temporal lens
("why did spend move?" — this period vs. prior, keyed by an existing axis).
This issue captures the orthogonal dimensional lens ("what is spending?" —
a new grouping key). They compose: once these axes exist, a future
--compare --by actorcan rank which actor's spend moved.Proposed Solution
Technical Approach
Two new
GroupByKindvalues, plus a run-level enrichment step. Unlike the fourexisting axes — which aggregate at the workflow summary level — actor and
event vary per run, so these two axes must bucket at the individual-run
grain (each
logsRunalready carries its ownaic, so per-run summation is thenatural fit).
run_id. Thegh aw logs --jsonpayload the rollupreads (
logsRun,internal/fleet/consumption_logs.go:83) carriesrun_id/conclusion/aic/created_atbut noeventortriggering_actor. Add a new seamghRunsMetaAPI(mirroringghWorkflowsAPI/
ghLogsAPI) that listsgh api repos/{repo}/actions/runs?per_page=100once per repo and returns a
run_id → {event, triggering_actor.login}map. Join it into the already-window-filtered runs by
run_id— one extralist call per repo, not per run, keeping it in the same cost class as the
existing fan-out (relates to consumption
--source logs: bounded concurrency + no-download fast path for the per-workflow fan-out #113).GroupByActor/GroupByTriggerto theGroupByKindiota (internal/fleet/consumption.go:120) and their canonicalnames to
groupByNames(consumption.go:154);ParseGroupByand--byvalidation pick them up for free.consumption.go:~762) with aper-run bucketing path for the two run-grained axes: sum
run.AIC/github_api_callsby the run's actor or event, preserving the existingnil-until-positive AIC semantics per bucket (a bucket whose runs are all
failed/nil rolls up to nil AIC, same
nilAICDiagrule).ACTOR/TRIGGERheaders tobyColumnHeader(
cmd/consumption.go:256) and the axis to the--byhelp +Longusage text.Files Likely Affected
internal/fleet/consumption.go—GroupByKindenum,groupByNames, theaggregation switch (new per-run bucketing branch).
internal/fleet/consumption_logs.go— newghRunsMetaAPIseam; enrichrepoRunData/logsRunjoin withevent+ actor byrun_id.cmd/consumption.go—--byhelp text,Longusage block,byColumnHeader.internal/fleet/testdata/{logs,consumption}/— new offline fixtures for theruns-metadata list response.
Acceptance Criteria
consumption --by actorgroups AIC/API_CALLS/COST bytriggering_actor.login;dependabot[bot]surfaces as its own row.consumption --by triggergroups by GitHub event(
schedule/push/workflow_dispatch/pull_request/…).--by actor|triggeris accepted byParseGroupByand rejected valuesstill error (extends the existing FR-005 invalid-
--bytest).--latest/--trailing Nd/--since) and honor--budgethighlighting.bucket → nil AIC, not
0.00).failure surfaces as a diagnostic, not a silent zero.
cmd.SchemaVersionbump).make cipasses (fmt, vet, lint, full test suite).Out of Scope
--by sourcetaxonomy (a single axis folding actor+eventinto
dependabot|scheduled|manual|releasebuckets). We deliberately chose twohonest, composable axes over one axis with invented precedence rules.
--by triggersurfaces the raw event; arelease shows as
releaseonly if the workflow useson: release, else aspush(tag push). No synthetic release classification.--compare --by actorcross-composition (that lands with feat(consumption): --compare mode — period-over-period spend delta by workflow ("why did spend change?") #198; this issueonly adds the axes).
per_page=100— shares the samebounded-discovery gap tracked in Paginate Actions workflow discovery for logs consumption source #119/consumption
--source logs: bounded concurrency + no-download fast path for the per-workflow fan-out #113; note it, don't solve it here.Technical Notes
axes sum pre-aggregated workflow summaries; actor/event are per-run, so these
two axes need the raw
logsRunset (which the fan-out already collects andwindow-filters) rather than the
WorkflowConsumptionsummary.triggering_actorvsactor. Usetriggering_actor.login— for re-runsit names who caused this run, which is the billing-relevant actor.
gh/gh apipaths go through injection seams; thenew
ghRunsMetaAPIfollows suit so tests run againsttestdata/fixtureswith no network (per the package's established pattern).
Testing Strategy
ParseGroupByacceptsactor/trigger, rejects unknowns.with mixed actors/events in one workflow; asserts nil-AIC handling.
ghRunsMetaAPIjoin byrun_id(in-window runs only; missingmetadata → diagnostic).
byColumnHeaderreturnsACTOR/TRIGGER.consumption --by actor --trailing 7dagainstoffline fixtures rendering the expected table.
Related
consumption --compare— the temporal "why did spend move"lens; this is the dimensional "what is spending" lens; both from the
fleet-observability announcement thread).
--source logs: bounded concurrency + no-download fast path for the per-workflow fan-out #113 (bounded concurrency + no-downloadfast path), Paginate Actions workflow discovery for logs consumption source #119 (paginate Actions workflow discovery).
ROADMAP.md:158).