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
22 changes: 11 additions & 11 deletions .github/workflows/smoke-copilot-aoai-entra.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 12 additions & 7 deletions actions/setup/js/check_daily_aic_workflow_guardrail.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,11 @@ async function main() {
const candidateRuns = [];
let page = 1;
let truncatedByRateLimit = false;
// listWorkflowRuns returns runs in descending creation order (newest first).
// The first run whose created_at falls before the cutoff means all remaining
// runs on this page and every subsequent page are also outside the window, so
// we can stop paginating immediately rather than exhausting the page budget.
let reachedCutoff = false;
while (page <= MAX_WORKFLOW_RUN_PAGES) {
logDailyGuardrail("Querying completed workflow runs", {
workflowId: currentRun.data.workflow_id,
Expand All @@ -401,7 +406,8 @@ async function main() {
logDailyGuardrail("Received workflow runs page", {
page,
runCount: runs.length,
runIds: runs.map(run => run?.id).filter(Boolean),
firstRunId: runs[0]?.id ?? null,
lastRunId: runs[runs.length - 1]?.id ?? null,
});
if (runs.length === 0) {
break;
Expand All @@ -412,15 +418,18 @@ async function main() {
}
const createdAtMs = Date.parse(run.created_at || "");
if (!Number.isFinite(createdAtMs) || createdAtMs < cutoffMs) {
continue;
// Runs are newest-first; any run older than the cutoff means all
// remaining runs (and pages) are also outside the 24h window.
reachedCutoff = true;
break;
Comment on lines 420 to +424
}
candidateRuns.push(run);
if (candidateRuns.length >= maxInspectableRuns) {
truncatedByRateLimit = true;
break;
}
}
if (candidateRuns.length >= maxInspectableRuns || runs.length < 100) {
if (reachedCutoff || candidateRuns.length >= maxInspectableRuns || runs.length < 100) {
break;
}
page += 1;
Expand All @@ -437,10 +446,6 @@ async function main() {
/** @type {Array<{id:number, html_url:string, created_at:string, conclusion:string, aic:number}>} */
const countedRuns = [];
for (const run of candidateRuns) {
if (countedRuns.length >= maxInspectableRuns) {
truncatedByRateLimit = true;
break;
}
try {
const runAIC = await module.exports.getRunAIC(artifactClient, run.id, token, owner, repo);
if (runAIC <= 0) {
Expand Down
96 changes: 96 additions & 0 deletions actions/setup/js/check_daily_aic_workflow_guardrail.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,102 @@ describe("check_daily_aic_workflow_guardrail", () => {
}
});

it("main() stops paginating as soon as a run older than the 24h cutoff is found", async () => {
const nowIso = new Date().toISOString();
const staleIso = new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString();

let listCallCount = 0;
const mockGithub = {
rest: {
rateLimit: {
get: async () => ({
data: {
resources: {
core: { limit: 5000, remaining: 4990, used: 10, reset: Math.floor(Date.now() / 1000) + 3600 },
},
},
headers: {},
}),
},
actions: {
getWorkflowRun: async () => ({
data: { workflow_id: 999, actor: { login: "bot" }, triggering_actor: { login: "bot" } },
headers: {},
}),
listWorkflowRuns: async ({ page }) => {
listCallCount += 1;
// Page 1: one recent run followed by a stale run.
// If early-exit works, page 2 should never be requested.
if (page === 1) {
return {
data: {
workflow_runs: [
{ id: 10, html_url: "https://example.test/runs/10", created_at: nowIso, conclusion: "success" },
// A stale run: encountering this should immediately stop pagination.
{ id: 9, html_url: "https://example.test/runs/9", created_at: staleIso, conclusion: "success" },
],
},
headers: {},
};
}
// Should never reach page 2.
return { data: { workflow_runs: [] }, headers: {} };
},
},
},
};

const getRunAICSpy = vi.spyOn(exports, "getRunAIC").mockResolvedValue(50);

const coreOutputs = {};
const mockCore = {
setOutput: (key, value) => {
coreOutputs[key] = value;
},
info: () => {},
warning: () => {},
summary: {
addDetails: function () {
return this;
},
write: async () => {},
},
};

const mockContext = { repo: { owner: "test-owner", repo: "test-repo" }, runId: 42 };

global.core = mockCore;
global.github = mockGithub;
global.context = mockContext;

process.env.GH_AW_MAX_DAILY_AI_CREDITS = "1000";
process.env.GH_AW_GITHUB_TOKEN = "fake-token";
process.env.GITHUB_EVENT_NAME = "pull_request";

try {
await expect(exports.main()).resolves.toBeUndefined();

// Only page 1 should have been fetched; the stale run should have
// terminated pagination before page 2 was requested.
expect(listCallCount).toBe(1);

// Only the recent run (id: 10) should have been inspected.
expect(getRunAICSpy).toHaveBeenCalledTimes(1);
expect(getRunAICSpy.mock.calls[0][1]).toBe(10);

// Guardrail not exceeded (50 < 1000).
expect(coreOutputs["daily_ai_credits_exceeded"]).toBe("false");
} finally {
delete global.core;
delete global.github;
delete global.context;
delete process.env.GH_AW_MAX_DAILY_AI_CREDITS;
delete process.env.GH_AW_GITHUB_TOKEN;
delete process.env.GITHUB_EVENT_NAME;
getRunAICSpy.mockRestore();
}
});

it("main() marks the step failed when the daily AI Credits guardrail is exceeded", async () => {
const getRunAICSpy = vi.spyOn(exports, "getRunAIC").mockResolvedValue(200);

Expand Down
16 changes: 8 additions & 8 deletions pkg/workflow/action_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,18 +163,18 @@
return ref, nil
}
resolverLog.Printf("Resolving --gh-aw-ref %q to commit SHA via GitHub API", ref)
apiPath := fmt.Sprintf("/repos/github/gh-aw/commits/%s", ref)

Check failure on line 166 in pkg/workflow/action_resolver.go

View workflow job for this annotation

GitHub Actions / lint-go

string-format: fmt.Sprintf can be replaced with string concatenation (perfsprint)

Check failure on line 166 in pkg/workflow/action_resolver.go

View workflow job for this annotation

GitHub Actions / lint-go

string-format: fmt.Sprintf can be replaced with string concatenation (perfsprint)
callCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
cmd := ExecGHContext(callCtx, "api", apiPath, "--jq", ".sha")
output, err := cmd.CombinedOutput()
if err != nil {
msg := strings.TrimSpace(string(output))
if msg != "" {
return "", fmt.Errorf("failed to resolve gh-aw ref %q to SHA: %s: %w", ref, msg, err)
cmd := ExecGHContext(callCtx, "api", apiPath, "--jq", ".sha")
output, err := cmd.CombinedOutput()
if err != nil {
msg := strings.TrimSpace(string(output))
if msg != "" {
return "", fmt.Errorf("failed to resolve gh-aw ref %q to SHA: %s: %w", ref, msg, err)
}
return "", fmt.Errorf("failed to resolve gh-aw ref %q to SHA: %w", ref, err)
}
return "", fmt.Errorf("failed to resolve gh-aw ref %q to SHA: %w", ref, err)
}
sha := strings.TrimSpace(string(output))
if !gitutil.IsValidFullSHA(sha) {
return "", fmt.Errorf("unexpected response resolving gh-aw ref %q: got %q (expected 40-char hex SHA)", ref, sha)
Expand Down
Loading