mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 07:18:03 +01:00
ci-audit: the cost note names minutes on runners with no price row
They counted as $0 inside a total that read as complete; the note now gives their billed minutes and labels and points at --prices. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
a68f971360
commit
90c61bc057
2 files changed
+16
-2
No files matched your search
@@ -37,13 +37,17 @@ export function buildReport(data: AuditData, m: Measures, ls: Lesson[], st: Stat
|
||||
if (!counted) for (const r of data.runs) runsPerWorkflow.set(r.path, (runsPerWorkflow.get(r.path) ?? 0) + 1);
|
||||
const sampledPerWorkflow = new Map<string, Set<number>>();
|
||||
for (const j of m.jobs) sampledPerWorkflow.set(j.workflow, (sampledPerWorkflow.get(j.workflow) ?? new Set()).add(j.runId));
|
||||
const unpriced = new Map<string, number>();
|
||||
const jobs: JobRow[] = m.groups.map(g => {
|
||||
const p = priceRunner(g.labels, data.isPrivate, prices, family);
|
||||
const scale = (runsPerWorkflow.get(g.workflow) ?? 0) / Math.max(1, sampledPerWorkflow.get(g.workflow)?.size ?? 1);
|
||||
const billed = (g.billedMinutes * scale) / weeks;
|
||||
if (p.kind === 'unknown') unpriced.set(p.label, (unpriced.get(p.label) ?? 0) + billed);
|
||||
return { key: g.key, runner: p.label, vcpu: p.vcpu, n: g.n, wait: g.wait, setup: g.setup, run: g.run, billedMinutesPerWeek: Math.round(billed), usdPerMonth: round(billed * WEEKS_PER_MONTH * p.usdPerMinute), price: `${p.usdPerMinute}/min: ${p.source}${p.assumed ? ' (assumed)' : ''}` };
|
||||
});
|
||||
const billedMinutesPerWeek = jobs.reduce((a, j) => a + j.billedMinutesPerWeek, 0);
|
||||
const unpricedMinutes = Math.round([...unpriced.values()].reduce((a, b) => a + b, 0));
|
||||
const unpricedNote = unpriced.size ? `; ${unpricedMinutes} billed min/week on runners with no price row (${[...unpriced.keys()].slice(0, 5).join(', ')}) count as $0; pass --prices to price them` : '';
|
||||
const familyNote = !family && jobs.some(j => /^ubicloud/.test(j.runner)) ? '; Ubicloud VM family not read from a job log, so Ubicloud jobs are priced as standard (assumed); --logs 5 reads it, --ubicloud-family sets it' : '';
|
||||
return {
|
||||
schema: SCHEMA, ok: true, repo: data.repo, generatedAt: new Date(now).toISOString(),
|
||||
@@ -52,7 +56,7 @@ export function buildReport(data: AuditData, m: Measures, ls: Lesson[], st: Stat
|
||||
pushToGreen: { ...m.cohort.total, excluded: m.cohort.excluded, bottlenecks: m.cohort.bottlenecks.slice(0, 5).map(b => ({ workflow: b.workflow, share: round(b.share) })) },
|
||||
timeToFirstFailure: m.cohort.failures, preJobWait: m.preJob, sampledRuns: m.sampledRuns,
|
||||
jobs, lessons: ls, fixes: rankFixes(ls),
|
||||
cost: { billedMinutesPerWeek, usdPerMonth: round(jobs.reduce((a, j) => a + j.usdPerMonth, 0)), note: `billed minutes scaled from the sampled runs to ${counted ? `every run of each workflow since ${counted.since}` : 'every run in the runs list'}; each job rounded up to a whole minute; workflows with no sampled PR or push run are not priced` + familyNote },
|
||||
cost: { billedMinutesPerWeek, usdPerMonth: round(jobs.reduce((a, j) => a + j.usdPerMonth, 0)), note: `billed minutes scaled from the sampled runs to ${counted ? `every run of each workflow since ${counted.since}` : 'every run in the runs list'}; each job rounded up to a whole minute; workflows with no sampled PR or push run are not priced` + unpricedNote + familyNote },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+11
-1
@@ -9,7 +9,7 @@ import { concurrencyFlag, lessonRunners } from '../lib/ci-audit/lessons';
|
||||
import { cohort, measure, phases } from '../lib/ci-audit/measure';
|
||||
import { priceRunner } from '../lib/ci-audit/prices';
|
||||
import { ISSUE_MARKER, ISSUE_TITLE, applyIssue, findIssue, planIssue } from '../lib/ci-audit/publish';
|
||||
import type { AuditReport } from '../lib/ci-audit/report';
|
||||
import { buildReport, type AuditReport } from '../lib/ci-audit/report';
|
||||
import { CI_SHAPING, MIN_COHORT, comparable, judgeHead, staticChecks } from '../lib/ci-audit/ship-check';
|
||||
import { parseWorkflow, readStatic } from '../lib/ci-audit/static';
|
||||
import { scoreReply, writePlantedRepo } from './helpers/ci-audit-planted';
|
||||
@@ -217,6 +217,16 @@ describe('cost accounting inputs', () => {
|
||||
expect((await listJobs(client as never, 'o/n', { id: 9, run_attempt: 1 } as Run))?.length).toBe(101);
|
||||
});
|
||||
|
||||
test('minutes on runners with no price row are disclosed in the cost note, not passed off as $0', () => {
|
||||
const d = { p50: 60, p90: 60, n: 1 };
|
||||
const data = { repo: 'o/n', runs: [{ path: 'w.yml' }], partial: [], notes: [], calls: 1, since: '2026-10-24T00:00:00Z', until: '2026-10-31T00:00:00Z', isPrivate: true, runCounts: null };
|
||||
const m = { windowDays: 7, jobs: [{ workflow: 'w.yml', runId: 1 }], groups: [{ key: 'build', labels: 'ubuntu-latest-16-cores', workflow: 'w.yml', billedMinutes: 70, n: 1, wait: d, setup: d, run: d }],
|
||||
cohort: { total: { n: 0, p50: null, p90: null }, excluded: {}, bottlenecks: [], failures: { n: 0, p50: null, p90: null } }, preJob: { n: 0, p50: null, p90: null }, sampledRuns: 1 };
|
||||
const r = buildReport(data as never, m as never, [], null);
|
||||
expect(r.cost.usdPerMonth).toBe(0);
|
||||
expect(r.cost.note).toContain('70 billed min/week on runners with no price row (ubuntu-latest-16-cores)');
|
||||
});
|
||||
|
||||
test('larger macOS runners are not priced as free standard runners', () => {
|
||||
expect(priceRunner('macos-14', false).usdPerMinute).toBe(0);
|
||||
const xl = priceRunner('macos-14-xlarge', false);
|
||||
|
||||
Reference in new issue
Block a user