Skip to content

feat(consumption): add --by actor and --by trigger axes — attribute AIC to what caused each run #204

Description

@rshade

Overview

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 no event 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.goGroupByKind 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

Technical Notes

  • 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).

Testing Strategy

  • Unit: ParseGroupBy accepts actor/trigger, rejects unknowns.
  • Unit: per-run bucketing sums AIC by actor and by event against a fixture
    with mixed actors/events in one workflow; asserts nil-AIC handling.
  • Unit: ghRunsMetaAPI join by run_id (in-window runs only; missing
    metadata → diagnostic).
  • Unit: byColumnHeader returns ACTOR / TRIGGER.
  • Integration: end-to-end consumption --by actor --trailing 7d against
    offline fixtures rendering the expected table.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    costCost / billing-visibility item - reduces operator surprise on Copilot credit usageeffort/mediumMedium effort - half day to 1 dayenhancementNew feature or requestfinopsFinOps: AI-credit cost forecasting, guardrails, and attribution across the fleetroadmap/nextNext milestone - Near-Term Vision

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions