From 012f59ae6e13a906984d94941bf93ce41517ecbe Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu, 11 Jun 2026 23:59:50 +0000
Subject: [PATCH] chore: sync actions from gh-aw@v0.79.7
---
setup/action.yml | 2 +-
setup/index.js | 2 -
setup/js/artifact_client.cjs | 385 ++++++++++++++++++
setup/js/awf_reflect.cjs | 21 +-
.../js/check_daily_aic_workflow_guardrail.cjs | 76 ++--
setup/js/generate_aw_info.cjs | 64 ---
setup/js/handle_agent_failure.cjs | 54 ++-
setup/js/merge_awf_model_multipliers.cjs | 212 ----------
setup/js/messages_footer.cjs | 22 +-
setup/js/model_multipliers.json | 161 --------
setup/js/package.json | 1 -
setup/js/upload_artifact.cjs | 9 +-
setup/md/agent_failure_comment.md | 2 +-
setup/md/agent_failure_issue.md | 2 +-
setup/md/daily_workflow_aic_exceeded.md | 2 +-
setup/setup.sh | 35 --
16 files changed, 518 insertions(+), 532 deletions(-)
create mode 100644 setup/js/artifact_client.cjs
delete mode 100644 setup/js/merge_awf_model_multipliers.cjs
delete mode 100644 setup/js/model_multipliers.json
diff --git a/setup/action.yml b/setup/action.yml
index bf9b64e..d4aaa59 100644
--- a/setup/action.yml
+++ b/setup/action.yml
@@ -7,7 +7,7 @@ inputs:
description: 'Destination directory for activation files (default: ${RUNNER_TEMP}/gh-aw/actions)'
required: false
safe-output-artifact-client:
- description: 'Install @actions/artifact so upload_artifact.cjs can upload GitHub Actions artifacts via REST API directly'
+ description: 'Deprecated no-op (artifact support is now built into setup action scripts)'
required: false
default: 'false'
job-name:
diff --git a/setup/index.js b/setup/index.js
index 7a37e77..678ae89 100644
--- a/setup/index.js
+++ b/setup/index.js
@@ -12,7 +12,6 @@ const setupStartMs = Date.now();
// runner versions preserve the original hyphen form. getActionInput() handles
// both forms automatically.
const safeOutputCustomTokens = getActionInput("SAFE_OUTPUT_CUSTOM_TOKENS") || "false";
-const safeOutputArtifactClient = getActionInput("SAFE_OUTPUT_ARTIFACT_CLIENT") || "false";
const inputTraceId = getActionInput("TRACE_ID");
const inputParentSpanId = getActionInput("PARENT_SPAN_ID");
const inputJobName = getActionInput("JOB_NAME");
@@ -22,7 +21,6 @@ const result = spawnSync(path.join(__dirname, "setup.sh"), [], {
stdio: "inherit",
env: Object.assign({}, process.env, {
INPUT_SAFE_OUTPUT_CUSTOM_TOKENS: safeOutputCustomTokens,
- INPUT_SAFE_OUTPUT_ARTIFACT_CLIENT: safeOutputArtifactClient,
INPUT_TRACE_ID: inputTraceId,
INPUT_PARENT_SPAN_ID: inputParentSpanId,
INPUT_JOB_NAME: inputJobName,
diff --git a/setup/js/artifact_client.cjs b/setup/js/artifact_client.cjs
new file mode 100644
index 0000000..1b46333
--- /dev/null
+++ b/setup/js/artifact_client.cjs
@@ -0,0 +1,385 @@
+// @ts-check
+///
+
+const crypto = require("crypto");
+const fs = require("fs");
+const os = require("os");
+const path = require("path");
+const { Readable } = require("stream");
+const { pipeline } = require("stream/promises");
+const { spawnSync } = require("child_process");
+
+const DEFAULT_RETRY_ATTEMPTS = 5;
+const RETRY_DELAY_MS = 5000;
+const RESULTS_SCOPE_PREFIX = "Actions.Results:";
+const TWIRP_ARTIFACT_SERVICE = "github.actions.results.api.v1.ArtifactService";
+const MAX_ARTIFACTS = 1000;
+const PAGE_SIZE = 100;
+
+function sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+function decodeJWTPayload(token) {
+ const parts = String(token || "").split(".");
+ if (parts.length < 2 || !parts[1]) {
+ throw new Error("failed to decode ACTIONS_RUNTIME_TOKEN payload");
+ }
+ const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
+ const padded = payload + "=".repeat((4 - (payload.length % 4 || 4)) % 4);
+ return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
+}
+
+function getBackendIdsFromRuntimeToken() {
+ const token = process.env.ACTIONS_RUNTIME_TOKEN || "";
+ if (!token) {
+ throw new Error("ACTIONS_RUNTIME_TOKEN is required for artifact upload");
+ }
+ const payload = decodeJWTPayload(token);
+ const scope = String(payload?.scp || "");
+ for (const part of scope.split(" ")) {
+ if (!part.startsWith(RESULTS_SCOPE_PREFIX)) continue;
+ const ids = part.split(":");
+ if (ids.length !== 3 || !ids[1] || !ids[2]) {
+ break;
+ }
+ return {
+ workflowRunBackendId: ids[1],
+ workflowJobRunBackendId: ids[2],
+ };
+ }
+ throw new Error("failed to parse Actions.Results backend IDs from ACTIONS_RUNTIME_TOKEN");
+}
+
+function getResultsServiceOrigin() {
+ const url = process.env.ACTIONS_RESULTS_URL || "";
+ if (!url) {
+ throw new Error("ACTIONS_RESULTS_URL is required for artifact upload");
+ }
+ return new URL(url).origin;
+}
+
+async function twirpRequest(method, body) {
+ const runtimeToken = process.env.ACTIONS_RUNTIME_TOKEN || "";
+ if (!runtimeToken) {
+ throw new Error("ACTIONS_RUNTIME_TOKEN is required for artifact upload");
+ }
+ const url = new URL(`/twirp/${TWIRP_ARTIFACT_SERVICE}/${method}`, getResultsServiceOrigin()).toString();
+
+ let lastError;
+ for (let attempt = 1; attempt <= DEFAULT_RETRY_ATTEMPTS; attempt++) {
+ try {
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ Authorization: "Bearer " + runtimeToken,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(body),
+ });
+
+ if (response.ok) {
+ return await response.json();
+ }
+
+ const responseBody = await response.text();
+ const retryable = response.status >= 500 || response.status === 429;
+ if (!retryable || attempt === DEFAULT_RETRY_ATTEMPTS) {
+ throw new Error(`artifact twirp ${method} failed (${response.status}): ${responseBody || response.statusText}`);
+ }
+ await sleep(RETRY_DELAY_MS);
+ } catch (error) {
+ lastError = error;
+ if (attempt === DEFAULT_RETRY_ATTEMPTS) {
+ break;
+ }
+ await sleep(RETRY_DELAY_MS);
+ }
+ }
+
+ throw lastError || new Error(`artifact twirp ${method} failed`);
+}
+
+function artifactListFilterLatest(artifacts) {
+ const sorted = [...artifacts].sort((a, b) => Number(b.id || 0) - Number(a.id || 0));
+ const seen = new Set();
+ /** @type {any[]} */
+ const latest = [];
+ for (const artifact of sorted) {
+ if (seen.has(artifact.name)) continue;
+ seen.add(artifact.name);
+ latest.push(artifact);
+ }
+ return latest;
+}
+
+function parseFilenameFromContentDisposition(contentDisposition) {
+ if (!contentDisposition) return "artifact";
+ const filenameStar = contentDisposition.match(/filename\*\s*=\s*UTF-8''([^;\r\n]*)/i);
+ const filenamePlain = contentDisposition.match(/(? hash.update(chunk));
+ await pipeline(nodeStream, fs.createWriteStream(filePath));
+ return hash.digest("hex");
+}
+
+function ensureZipAvailable() {
+ const result = spawnSync("zip", ["-v"], { stdio: "ignore" });
+ if (result.status !== 0) {
+ throw new Error("zip command is required to upload artifacts (for example: apt-get install zip)");
+ }
+}
+
+function ensureUnzipAvailable() {
+ const result = spawnSync("unzip", ["-v"], { stdio: "ignore" });
+ if (result.status !== 0) {
+ throw new Error("unzip command is required to download artifacts (for example: apt-get install unzip)");
+ }
+}
+
+function createZipFromFiles(files, rootDirectory, outputPath) {
+ ensureZipAvailable();
+ const relativeFiles = files.map(file => path.relative(rootDirectory, file));
+ const invalid = relativeFiles.find(rel => !rel || rel.startsWith("..") || path.isAbsolute(rel));
+ if (invalid) {
+ throw new Error(`all upload artifact files must be under rootDirectory (invalid path: ${invalid})`);
+ }
+ const result = spawnSync("zip", ["-q", "-r", outputPath, ...relativeFiles], {
+ cwd: rootDirectory,
+ encoding: "utf8",
+ });
+ if (result.status !== 0) {
+ throw new Error(`zip command failed: ${result.stderr || result.stdout || "unknown error"}`);
+ }
+}
+
+async function uploadFileToSignedURL(filePath, signedUploadURL, contentType) {
+ const stats = fs.statSync(filePath);
+ const response = await fetch(signedUploadURL, {
+ method: "PUT",
+ headers: {
+ "Content-Type": contentType,
+ "Content-Length": String(stats.size),
+ "x-ms-blob-type": "BlockBlob",
+ },
+ body: fs.createReadStream(filePath),
+ duplex: "half",
+ });
+ if (!response.ok) {
+ const body = await response.text();
+ throw new Error(`artifact blob upload failed (${response.status}): ${body || response.statusText}`);
+ }
+ return stats.size;
+}
+
+async function hashFile(filePath) {
+ const hash = crypto.createHash("sha256");
+ const nodeStream = fs.createReadStream(filePath);
+ await pipeline(nodeStream, async function* (source) {
+ for await (const chunk of source) {
+ hash.update(chunk);
+ }
+ });
+ return hash.digest("hex");
+}
+
+function formatRetentionTimestamp(retentionDays) {
+ if (!Number.isFinite(retentionDays) || retentionDays <= 0) {
+ return "";
+ }
+ return new Date(Date.now() + retentionDays * 24 * 60 * 60 * 1000).toISOString();
+}
+
+class DefaultArtifactClient {
+ async listArtifacts(options = {}) {
+ const findBy = options.findBy;
+ if (!findBy?.token || !findBy?.repositoryOwner || !findBy?.repositoryName || !findBy?.workflowRunId) {
+ throw new Error("listArtifacts requires findBy.token, findBy.repositoryOwner, findBy.repositoryName, and findBy.workflowRunId");
+ }
+
+ const serverUrl = process.env.GITHUB_API_URL || "https://api.github.com";
+ /** @type {Array<{id:number,name:string,size:number,createdAt?:Date,digest?:string}>} */
+ const artifacts = [];
+
+ let page = 1;
+ const maxPages = Math.ceil(MAX_ARTIFACTS / PAGE_SIZE);
+ for (; page <= maxPages; page++) {
+ const url = new URL(`/repos/${findBy.repositoryOwner}/${findBy.repositoryName}/actions/runs/${findBy.workflowRunId}/artifacts`, serverUrl);
+ url.searchParams.set("per_page", String(PAGE_SIZE));
+ url.searchParams.set("page", String(page));
+ const response = await fetch(url.toString(), {
+ headers: {
+ Authorization: "Bearer " + findBy.token,
+ Accept: "application/vnd.github+json",
+ "User-Agent": "gh-aw-artifact-client",
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`failed to list artifacts (${response.status}): ${await response.text()}`);
+ }
+ /** @type {any} */
+ const payload = await response.json();
+ const pageArtifacts = Array.isArray(payload?.artifacts) ? payload.artifacts : [];
+ for (const item of pageArtifacts) {
+ artifacts.push({
+ id: Number(item.id),
+ name: String(item.name || ""),
+ size: Number(item.size_in_bytes || 0),
+ createdAt: item.created_at ? new Date(item.created_at) : undefined,
+ digest: typeof item.digest === "string" ? item.digest : undefined,
+ });
+ }
+ if (pageArtifacts.length < PAGE_SIZE) {
+ break;
+ }
+ }
+
+ return {
+ artifacts: options.latest ? artifactListFilterLatest(artifacts) : artifacts,
+ };
+ }
+
+ async downloadArtifact(artifactId, options = {}) {
+ const findBy = options.findBy;
+ if (!findBy?.token || !findBy?.repositoryOwner || !findBy?.repositoryName) {
+ throw new Error("downloadArtifact requires findBy.token, findBy.repositoryOwner, and findBy.repositoryName");
+ }
+
+ const destination = options.path || process.env.GITHUB_WORKSPACE || process.cwd();
+ fs.mkdirSync(destination, { recursive: true });
+
+ const apiUrl = new URL(`/repos/${findBy.repositoryOwner}/${findBy.repositoryName}/actions/artifacts/${artifactId}/zip`, process.env.GITHUB_API_URL || "https://api.github.com");
+ const redirectResponse = await fetch(apiUrl.toString(), {
+ headers: {
+ Authorization: "Bearer " + findBy.token,
+ Accept: "application/vnd.github+json",
+ "User-Agent": "gh-aw-artifact-client",
+ },
+ redirect: "manual",
+ });
+ if (![301, 302, 303, 307, 308].includes(redirectResponse.status)) {
+ throw new Error(`unable to download artifact: unexpected status ${redirectResponse.status}`);
+ }
+ const location = redirectResponse.headers.get("location");
+ if (!location) {
+ throw new Error("unable to download artifact: missing redirect location");
+ }
+
+ const blobResponse = await fetch(location);
+ if (!blobResponse.ok) {
+ throw new Error(`artifact blob download failed (${blobResponse.status})`);
+ }
+
+ let digest;
+ const contentType = blobResponse.headers.get("content-type") || "";
+ const zipLike = isZipResponse(location, contentType);
+ if (zipLike && !options.skipDecompress) {
+ ensureUnzipAvailable();
+ const tempZip = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "gh-aw-artifact-download-")), "artifact.zip");
+ digest = await streamToFile(blobResponse, tempZip);
+ const unzipResult = spawnSync("unzip", ["-q", tempZip, "-d", destination], { encoding: "utf8" });
+ if (unzipResult.status !== 0) {
+ throw new Error(`unzip failed: ${unzipResult.stderr || unzipResult.stdout || "unknown error"}`);
+ }
+ } else {
+ const fileName = parseFilenameFromContentDisposition(blobResponse.headers.get("content-disposition") || "");
+ const outputPath = path.join(destination, fileName);
+ digest = await streamToFile(blobResponse, outputPath);
+ }
+
+ const computed = digest ? `sha256:${digest}` : "";
+ return {
+ downloadPath: destination,
+ digestMismatch: !!(computed && options.expectedHash && options.expectedHash !== computed),
+ };
+ }
+
+ async uploadArtifact(name, files, rootDirectory, options = {}) {
+ if (!Array.isArray(files) || files.length === 0) {
+ throw new Error("uploadArtifact requires at least one file");
+ }
+
+ let artifactName = String(name || "").trim();
+ let uploadPath = "";
+ let contentType = "application/zip";
+
+ if (options.skipArchive) {
+ if (files.length !== 1) {
+ throw new Error("skipArchive option is only supported when uploading a single file");
+ }
+ uploadPath = files[0];
+ contentType = "application/octet-stream";
+ } else {
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "gh-aw-artifact-upload-"));
+ uploadPath = path.join(tmpDir, `${artifactName || "artifact"}.zip`);
+ createZipFromFiles(files, rootDirectory, uploadPath);
+ }
+
+ const { workflowRunBackendId, workflowJobRunBackendId } = getBackendIdsFromRuntimeToken();
+ const createRequest = {
+ workflow_run_backend_id: workflowRunBackendId,
+ workflow_job_run_backend_id: workflowJobRunBackendId,
+ name: artifactName,
+ version: 7,
+ mime_type: { value: contentType },
+ };
+ const expiresAt = formatRetentionTimestamp(options.retentionDays);
+ if (expiresAt) {
+ createRequest.expires_at = expiresAt;
+ }
+
+ /** @type {any} */
+ const createResponse = await twirpRequest("CreateArtifact", createRequest);
+ if (!createResponse?.ok || !createResponse?.signed_upload_url) {
+ throw new Error("CreateArtifact returned an invalid response");
+ }
+
+ const uploadSize = await uploadFileToSignedURL(uploadPath, createResponse.signed_upload_url, contentType);
+ const sha256 = await hashFile(uploadPath);
+
+ const finalizeRequest = {
+ workflow_run_backend_id: workflowRunBackendId,
+ workflow_job_run_backend_id: workflowJobRunBackendId,
+ name: artifactName,
+ size: String(uploadSize),
+ hash: { value: `sha256:${sha256}` },
+ };
+ /** @type {any} */
+ const finalizeResponse = await twirpRequest("FinalizeArtifact", finalizeRequest);
+ if (!finalizeResponse?.ok) {
+ throw new Error("FinalizeArtifact returned an invalid response");
+ }
+
+ return {
+ id: Number(finalizeResponse.artifact_id || 0) || undefined,
+ size: uploadSize,
+ digest: sha256,
+ };
+ }
+}
+
+module.exports = {
+ DefaultArtifactClient,
+};
diff --git a/setup/js/awf_reflect.cjs b/setup/js/awf_reflect.cjs
index b64ed72..7060ca4 100644
--- a/setup/js/awf_reflect.cjs
+++ b/setup/js/awf_reflect.cjs
@@ -19,7 +19,7 @@ require("./shim.cjs");
const fs = require("fs");
const path = require("path");
-const { withRetry } = require("./error_recovery.cjs");
+const { withRetry, sleep } = require("./error_recovery.cjs");
// AWF API proxy management endpoint for discovering configured LLM providers and available models.
// The api-proxy sidecar exposes /reflect on its management port (port 10000) inside the AWF
@@ -38,6 +38,9 @@ const AWF_MODELS_URL_MAX_ATTEMPTS = 5;
const AWF_MODELS_URL_RETRY_BASE_MS = 250;
// Cap for exponential backoff delay between retries.
const AWF_MODELS_URL_RETRY_MAX_MS = 2000;
+// Delay before the first models_url probe when using GitHub OIDC auth with the local api-proxy.
+// This reduces startup-race 503s while the proxy completes OIDC token exchange.
+const AWF_MODELS_URL_OIDC_INITIAL_DELAY_MS_DEFAULT = 5000;
// Gemini model name prefix stripped from model IDs in the Gemini models API response.
// Example: { name: "models/gemini-1.5-pro" } → "gemini-1.5-pro"
const GEMINI_MODEL_NAME_PREFIX = "models/";
@@ -93,6 +96,22 @@ function extractModelIds(json) {
* @returns {Promise}
*/
async function fetchModelsFromUrl(modelsUrl, timeoutMs, logger) {
+ let isInitialProbeDelayed = false;
+ try {
+ const modelsHost = new URL(modelsUrl).hostname.toLowerCase();
+ isInitialProbeDelayed = process.env.AWF_AUTH_TYPE === "github-oidc" && modelsHost === "api-proxy";
+ } catch {
+ // Ignore invalid URL parsing and proceed without startup delay.
+ }
+ if (isInitialProbeDelayed) {
+ const configuredDelay = Number.parseInt(process.env.AWF_MODELS_URL_OIDC_INITIAL_DELAY_MS || "", 10);
+ const initialProbeDelay = Number.isFinite(configuredDelay) && configuredDelay >= 0 ? configuredDelay : AWF_MODELS_URL_OIDC_INITIAL_DELAY_MS_DEFAULT;
+ if (initialProbeDelay > 0) {
+ logger(`awf-reflect: delaying initial models probe for ${modelsUrl} by ${initialProbeDelay}ms (AWF_AUTH_TYPE=github-oidc)`);
+ await sleep(initialProbeDelay);
+ }
+ }
+
let attemptCounter = 0;
const retryConfig = {
maxRetries: AWF_MODELS_URL_MAX_ATTEMPTS - 1,
diff --git a/setup/js/check_daily_aic_workflow_guardrail.cjs b/setup/js/check_daily_aic_workflow_guardrail.cjs
index a61a533..6896ffa 100644
--- a/setup/js/check_daily_aic_workflow_guardrail.cjs
+++ b/setup/js/check_daily_aic_workflow_guardrail.cjs
@@ -4,6 +4,7 @@
const fs = require("fs");
const os = require("os");
const path = require("path");
+const { DefaultArtifactClient } = require("./artifact_client.cjs");
const { calculateDailyAICStats, findJSONLFiles, formatAICCredits, sumAICFromUsageJSONLFiles } = require("./daily_aic_workflow_helpers.cjs");
const { parsePositiveCompactNumber } = require("./numeric_limits.cjs");
@@ -19,10 +20,9 @@ const ESTIMATED_API_OPERATIONS_PER_RUN = 2;
const INTEGER_FORMATTER = new Intl.NumberFormat("en-US");
/**
- * @returns {Promise}
+ * @returns {Promise}
*/
async function getArtifactClient() {
- const { DefaultArtifactClient } = await import("@actions/artifact");
return new DefaultArtifactClient();
}
@@ -55,6 +55,13 @@ function logDailyGuardrail(message, details) {
core.info(formatDailyGuardrailLogMessage(message, details));
}
+/**
+ * Event types that indicate a user-initiated slash command trigger.
+ * When aw_context.event_type is one of these, the workflow was triggered by a user
+ * typing a slash command in a comment, and the daily guardrail should not be skipped.
+ */
+const SLASH_COMMAND_EVENT_TYPES = ["issue_comment", "pull_request_review_comment", "discussion_comment"];
+
/**
* @returns {boolean}
*/
@@ -62,8 +69,24 @@ function shouldSkipDailyAICGuardrail() {
const eventName = process.env.GITHUB_EVENT_NAME || "";
const isWorkflowCall = eventName === "workflow_call";
const isRepositoryDispatch = eventName === "repository_dispatch";
- const hasDispatchContext = (process.env.GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT || "").trim() !== "";
- return isWorkflowCall || isRepositoryDispatch || (eventName === "workflow_dispatch" && hasDispatchContext);
+ const rawContext = (process.env.GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT || "").trim();
+ const hasDispatchContext = rawContext !== "";
+ if (!(isWorkflowCall || isRepositoryDispatch || (eventName === "workflow_dispatch" && hasDispatchContext))) {
+ return false;
+ }
+ if (eventName === "workflow_dispatch" && hasDispatchContext) {
+ try {
+ const awContext = JSON.parse(rawContext);
+ const isLabelCommand = typeof awContext.trigger_label === "string" && awContext.trigger_label.trim() !== "";
+ const isSlashCommand = SLASH_COMMAND_EVENT_TYPES.includes(awContext.event_type);
+ if (isLabelCommand || isSlashCommand) {
+ return false;
+ }
+ } catch {
+ // Malformed aw_context: fall through and skip as before.
+ }
+ }
+ return true;
}
/**
@@ -78,7 +101,7 @@ function matchesGuardrailArtifactName(artifactName) {
}
/**
- * @param {import("@actions/artifact").DefaultArtifactClient} artifactClient
+ * @param {{ listArtifacts: Function, downloadArtifact: Function }} artifactClient
* @param {number} runId
* @param {string} token
* @param {string} owner
@@ -285,16 +308,15 @@ async function appendDailyAICSummary(workflowName, actorLogin, threshold, counte
*
* Error handling: all GitHub API interactions after the initial guard checks are wrapped
* in a top-level try-catch. Any unexpected error (network failure, permission error, etc.)
- * is logged as a warning and the function returns cleanly with `daily_effective_workflow_exceeded`
- * left at its default value of `"false"`. This design ensures the step never fails the
- * activation job — a guardrail error results in a safe bypass (agent allowed to run) rather
- * than a confusing workflow failure that blocks the agent entirely.
+ * is logged as a warning and the function returns cleanly with `daily_ai_credits_exceeded`
+ * left at its default value of `"false"` (safe bypass). When the guardrail is actually exceeded,
+ * the step marks the job as failed after setting outputs so downstream conclusion handling can
+ * still run and produce failure issues.
*/
async function main() {
- core.setOutput("daily_effective_workflow_exceeded", "false");
- core.setOutput("daily_effective_workflow_total_effective_tokens", "");
- core.setOutput("daily_effective_workflow_total_ai_credits", "");
- core.setOutput("daily_effective_workflow_threshold", "");
+ core.setOutput("daily_ai_credits_exceeded", "false");
+ core.setOutput("daily_ai_credits_total_effective_tokens", "");
+ core.setOutput("daily_ai_credits_threshold", "");
const threshold = parsePositiveCompactNumber(process.env.GH_AW_MAX_DAILY_AI_CREDITS);
if (threshold <= 0) {
return;
@@ -312,7 +334,7 @@ async function main() {
// Wrap all GitHub API interactions in a top-level try-catch so that transient API
// errors, permission failures, or unexpected exceptions never fail the activation
- // job step. A failure here would leave `daily_effective_workflow_exceeded` at its
+ // job step. A failure here would leave `daily_ai_credits_exceeded` at its
// default "false" value, which is the safe fallback: the agent is allowed to run
// and the guardrail is effectively bypassed for this invocation rather than causing
// a confusing workflow failure.
@@ -410,7 +432,7 @@ async function main() {
truncatedByRateLimit,
});
- const artifactClient = await getArtifactClient();
+ const artifactClient = await module.exports.getArtifactClient();
let totalAIC = 0;
/** @type {Array<{id:number, html_url:string, created_at:string, conclusion:string, aic:number}>} */
const countedRuns = [];
@@ -420,7 +442,7 @@ async function main() {
break;
}
try {
- const runAIC = await getRunAIC(artifactClient, run.id, token, owner, repo);
+ const runAIC = await module.exports.getRunAIC(artifactClient, run.id, token, owner, repo);
if (runAIC <= 0) {
logDailyGuardrail("Skipping run without AIC usage artifact data", {
runId: run.id,
@@ -449,9 +471,8 @@ async function main() {
}
}
- core.setOutput("daily_effective_workflow_total_effective_tokens", String(totalAIC));
- core.setOutput("daily_effective_workflow_total_ai_credits", String(totalAIC));
- core.setOutput("daily_effective_workflow_threshold", String(threshold));
+ core.setOutput("daily_ai_credits_total_effective_tokens", String(totalAIC));
+ core.setOutput("daily_ai_credits_threshold", String(threshold));
/** @type {{candidateRunsCount:number,inspectedRunsCount:number,truncatedByRateLimit:boolean}} */
const summaryMeta = {
@@ -490,19 +511,26 @@ async function main() {
return;
}
- core.setOutput("daily_effective_workflow_exceeded", "true");
- await appendDailyAICSummary(workflowName, actorLogin, threshold, countedRuns, rateLimit, summaryMeta);
+ core.setOutput("daily_ai_credits_exceeded", "true");
+ try {
+ await appendDailyAICSummary(workflowName, actorLogin, threshold, countedRuns, rateLimit, summaryMeta);
+ } catch (summaryError) {
+ core.warning(`Failed to write daily AIC summary: ${getErrorMessage(summaryError)}`);
+ }
core.warning(`Daily workflow AIC guardrail exceeded for ${workflowName}: ${totalAIC}/${threshold}.`);
+ core.setFailed(`Daily workflow AIC guardrail exceeded for ${workflowName}: ${totalAIC}/${threshold}.`);
} catch (error) {
- // Treat any unexpected error as a non-blocking skip so the step never fails the
- // activation job. The output stays at the default "false", allowing the agent to
- // run. The guardrail is effectively bypassed for this invocation.
+ // Treat unexpected guardrail execution errors as non-blocking skips so transient
+ // API/runtime issues do not fail activation. The output stays at the default "false",
+ // allowing the agent to run. Legitimate threshold exceedance still fails via setFailed.
core.warning(`Daily workflow AI Credits guardrail encountered an unexpected error and will be skipped: ${getErrorMessage(error)}`);
}
}
module.exports = {
main,
+ getArtifactClient,
+ getRunAIC,
shouldSkipDailyAICGuardrail,
matchesGuardrailArtifactName,
findJSONLFiles,
diff --git a/setup/js/generate_aw_info.cjs b/setup/js/generate_aw_info.cjs
index 49e9b4d..ba091d3 100644
--- a/setup/js/generate_aw_info.cjs
+++ b/setup/js/generate_aw_info.cjs
@@ -2,7 +2,6 @@
///
const fs = require("fs");
-const path = require("path");
const { TMP_GH_AW_PATH } = require("./constants.cjs");
const { generateWorkflowOverview } = require("./generate_workflow_overview.cjs");
const { logStagedPreviewInfo } = require("./staged_preview.cjs");
@@ -160,7 +159,6 @@ async function main(core, ctx) {
// Write to /tmp/gh-aw directory to avoid inclusion in PR
fs.mkdirSync(TMP_GH_AW_PATH, { recursive: true });
- writeMergedModelMultipliers(core, tokenWeights);
writeMergedModelsJSON(core);
const tmpPath = TMP_GH_AW_PATH + "/aw_info.json";
fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2));
@@ -192,49 +190,6 @@ async function main(core, ctx) {
}
}
- /**
- * Load and merge model multiplier data, then persist it to /tmp/gh-aw for downstream jobs.
- * Base data is read from the setup action directory; custom workflow overrides (if any)
- * are merged on top.
- * @param {typeof import('@actions/core')} core
- * @param {Record | null} tokenWeights
- */
- function writeMergedModelMultipliers(core, tokenWeights) {
- const builtInPath = path.join(__dirname, "model_multipliers.json");
- /** @type {Record} */
- let builtIn = {};
- if (fs.existsSync(builtInPath)) {
- try {
- const parsed = JSON.parse(fs.readFileSync(builtInPath, "utf8"));
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
- builtIn = parsed;
- } else {
- core.warning(`Built-in model_multipliers.json is not a JSON object: ${builtInPath}`);
- }
- } catch (error) {
- core.warning(`Failed to parse built-in model_multipliers.json: ${String(error)}`);
- }
- } else {
- core.warning(`Built-in model_multipliers.json not found at ${builtInPath}`);
- }
-
- const builtInTokenClassWeights = getPlainObjectOrEmpty(builtIn.token_class_weights);
- const builtInMultipliers = getPlainObjectOrEmpty(builtIn.multipliers);
-
- const customTokenClassWeights = getPlainObjectOrEmpty(tokenWeights?.token_class_weights);
- const customMultipliers = getPlainObjectOrEmpty(tokenWeights?.multipliers);
-
- const merged = {
- ...builtIn,
- token_class_weights: { ...builtInTokenClassWeights, ...customTokenClassWeights },
- multipliers: { ...builtInMultipliers, ...customMultipliers },
- };
-
- const mergedPath = `${TMP_GH_AW_PATH}/model_multipliers.json`;
- fs.writeFileSync(mergedPath, JSON.stringify(merged, null, 2));
- core.info(`Generated merged model multipliers at: ${mergedPath}`);
- }
-
core.info("Generated aw_info.json at: " + tmpPath);
core.info(JSON.stringify(awInfo, null, 2));
@@ -247,22 +202,3 @@ async function main(core, ctx) {
}
module.exports = { main };
-
-/**
- * @param {unknown} value
- * @returns {Record}
- */
-function getPlainObjectOrEmpty(value) {
- if (isPlainObject(value)) {
- return value;
- }
- return {};
-}
-
-/**
- * @param {unknown} value
- * @returns {value is Record}
- */
-function isPlainObject(value) {
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
-}
diff --git a/setup/js/handle_agent_failure.cjs b/setup/js/handle_agent_failure.cjs
index 47e5038..e9eb3dd 100644
--- a/setup/js/handle_agent_failure.cjs
+++ b/setup/js/handle_agent_failure.cjs
@@ -200,7 +200,7 @@ function buildFailureMatchCategories(options) {
if (options.hasAppTokenMintingFailed) categories.push("app_token_minting_failed");
if (options.hasLockdownCheckFailed) categories.push("lockdown_check_failed");
if (options.hasStaleLockFileFailed) categories.push("stale_lock_file_failed");
- if (options.hasDailyAICExceeded) categories.push("daily_effective_workflow_exceeded");
+ if (options.hasDailyAICExceeded) categories.push("daily_ai_credits_exceeded");
if (options.agentConclusion === "failure" && !options.isTimedOut) {
categories.push("agent_failure");
@@ -231,7 +231,7 @@ function buildFailureMatchCategories(options) {
*/
function buildFailureIssueTitle(options) {
const { workflowName } = options;
- if (options.hasDailyAICExceeded) return `[aw] ${workflowName} exceeded daily effective workflow budget`;
+ if (options.hasDailyAICExceeded) return `[aw] ${workflowName} exceeded daily AI credits budget`;
if (options.maxAICreditsExceeded) return `[aw] ${workflowName} exceeded max AI credits`;
if (options.aiCreditsRateLimitError) return `[aw] ${workflowName} hit AI credits rate limit`;
if (options.hasAppTokenMintingFailed) return `[aw] ${workflowName} failed to mint GitHub App token`;
@@ -1869,6 +1869,33 @@ function buildAssignCopilotFailureContext(hasAssignCopilotFailures, assignCopilo
return "\n" + renderTemplateFromFile(templatePath, { issues: issueList });
}
+/**
+ * Build the secret verification failure context for the agent failure issue/comment.
+ * For the Copilot engine, adds a suggestion to use `permissions.copilot-requests: write`
+ * to enable Copilot inference through the org without a personal access token.
+ * @param {string} secretVerificationResult - The secret verification result ("failed" or other)
+ * @param {string} engineId - The engine ID (e.g. "copilot")
+ * @returns {string} Formatted context string, or empty string if verification did not fail
+ */
+function buildSecretVerificationContext(secretVerificationResult, engineId) {
+ if (secretVerificationResult !== "failed") {
+ return "";
+ }
+
+ let context =
+ buildWarningAlertLine("Secret Verification Failed", "The workflow's secret validation step failed. Please check that the required secrets are configured in your repository settings.") +
+ "\nFor more information on configuring tokens, see: https://github.github.com/gh-aw/reference/engines/\n";
+
+ if ((engineId || "").toLowerCase() === "copilot") {
+ context +=
+ "\n**Alternative**: If your organization has a Copilot subscription, you can avoid the need for a personal access token by adding a top-level `permissions` block to your workflow file. This enables Copilot inference through the org using the built-in GitHub Actions token.\n" +
+ "\n```yaml\npermissions:\n copilot-requests: write\n```\n" +
+ "\nSee: https://github.github.com/gh-aw/reference/engines/#github-copilot-default\n";
+ }
+
+ return context;
+}
+
/**
* Check whether agent-stdio.log contains a terminal_reason: "completed" result entry,
* indicating the agent finished its task successfully despite a non-zero job exit code.
@@ -2376,9 +2403,9 @@ async function main() {
// stored in the compiled .lock.yml no longer matches the source .md file.
// The agent is skipped in this case; the conclusion job runs to surface remediation guidance.
const hasStaleLockFileFailed = process.env.GH_AW_STALE_LOCK_FILE_FAILED === "true";
- const hasDailyAICExceeded = process.env.GH_AW_DAILY_EFFECTIVE_WORKFLOW_EXCEEDED === "true";
- const dailyAICTotal = process.env.GH_AW_DAILY_EFFECTIVE_WORKFLOW_TOTAL_EFFECTIVE_TOKENS || "";
- const dailyAICThreshold = process.env.GH_AW_DAILY_EFFECTIVE_WORKFLOW_THRESHOLD || "";
+ const hasDailyAICExceeded = process.env.GH_AW_DAILY_AI_CREDITS_EXCEEDED === "true";
+ const dailyAICTotal = process.env.GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS || "";
+ const dailyAICThreshold = process.env.GH_AW_DAILY_AI_CREDITS_THRESHOLD || "";
// Cache-memory availability flag — set when cache-memory is configured for the workflow.
// Used to detect cache-miss misconfigurations reported by the agent.
const cacheMemoryEnabled = process.env.GH_AW_CACHE_MEMORY_ENABLED === "true";
@@ -2870,11 +2897,7 @@ async function main() {
workflow_source: workflowSource,
workflow_source_url: workflowSourceURL,
secret_verification_failed: String(secretVerificationResult === "failed"),
- secret_verification_context:
- secretVerificationResult === "failed"
- ? buildWarningAlertLine("Secret Verification Failed", "The workflow's secret validation step failed. Please check that the required secrets are configured in your repository settings.") +
- "\nFor more information on configuring tokens, see: https://github.github.com/gh-aw/reference/engines/\n"
- : "",
+ secret_verification_context: buildSecretVerificationContext(secretVerificationResult, engineId),
credential_auth_error_context: credentialAuthErrorContext,
assignment_errors_context: assignmentErrorsContext,
assign_copilot_failure_context: assignCopilotFailureContext,
@@ -2899,7 +2922,7 @@ async function main() {
app_token_minting_failed_context: appTokenMintingFailedContext,
lockdown_check_failed_context: lockdownCheckFailedContext,
stale_lock_file_failed_context: staleLockFileFailedContext,
- daily_effective_workflow_exceeded_context: dailyAICExceededContext,
+ daily_ai_credits_exceeded_context: dailyAICExceededContext,
};
// Render the comment template
@@ -3099,11 +3122,7 @@ async function main() {
branch: currentBranch,
pull_request_info: pullRequest ? ` \n**Pull Request:** [#${pullRequest.number}](${pullRequest.html_url})` : "",
secret_verification_failed: String(secretVerificationResult === "failed"),
- secret_verification_context:
- secretVerificationResult === "failed"
- ? buildWarningAlertLine("Secret Verification Failed", "The workflow's secret validation step failed. Please check that the required secrets are configured in your repository settings.") +
- "\nFor more information on configuring tokens, see: https://github.github.com/gh-aw/reference/engines/\n"
- : "",
+ secret_verification_context: buildSecretVerificationContext(secretVerificationResult, engineId),
credential_auth_error_context: credentialAuthErrorContext,
assignment_errors_context: assignmentErrorsContext,
assign_copilot_failure_context: assignCopilotFailureContext,
@@ -3128,7 +3147,7 @@ async function main() {
app_token_minting_failed_context: appTokenMintingFailedContext,
lockdown_check_failed_context: lockdownCheckFailedContext,
stale_lock_file_failed_context: staleLockFileFailedContext,
- daily_effective_workflow_exceeded_context: dailyAICExceededContext,
+ daily_ai_credits_exceeded_context: dailyAICExceededContext,
};
// Render the issue template
@@ -3240,6 +3259,7 @@ module.exports = {
hasAgentTerminalReasonCompleted,
detectAndHandleFailureCascade,
findRecentFailureIssues,
+ buildSecretVerificationContext,
CASCADE_WINDOW_MINUTES,
CASCADE_WINDOW_MS,
CASCADE_THRESHOLD,
diff --git a/setup/js/merge_awf_model_multipliers.cjs b/setup/js/merge_awf_model_multipliers.cjs
deleted file mode 100644
index d7c471e..0000000
--- a/setup/js/merge_awf_model_multipliers.cjs
+++ /dev/null
@@ -1,212 +0,0 @@
-// @ts-check
-///
-
-const fs = require("fs");
-const path = require("path");
-const { TMP_GH_AW_PATH } = require("./constants.cjs");
-
-const DEFAULT_MODEL_MULTIPLIERS_PATH = `${TMP_GH_AW_PATH}/model_multipliers.json`;
-
-/**
- * @param {unknown} value
- * @returns {value is Record}
- */
-function isPlainObject(value) {
- return value !== null && typeof value === "object" && !Array.isArray(value);
-}
-
-/**
- * @param {Record} rawMultipliers
- * @returns {Record}
- */
-function normalizeMultipliers(rawMultipliers) {
- /** @type {Record} */
- const normalized = {};
- for (const [key, value] of Object.entries(rawMultipliers)) {
- if (typeof value === "number" && Number.isFinite(value) && value > 0) {
- normalized[key] = value;
- }
- }
- return normalized;
-}
-
-/**
- * @param {unknown} rawModels
- * @returns {Array<{alias: string, targets: string[]}>}
- */
-function normalizeAliasRows(rawModels) {
- if (!isPlainObject(rawModels)) {
- return [];
- }
-
- /** @type {Array<{alias: string, targets: string[]}>} */
- const rows = [];
- for (const [alias, rawTargets] of Object.entries(rawModels)) {
- if (!Array.isArray(rawTargets)) {
- continue;
- }
- const trimmedTargets = rawTargets.map(target => (typeof target === "string" ? target.trim() : ""));
- const targets = trimmedTargets.filter(target => target !== "");
- if (targets.length === 0) {
- continue;
- }
- rows.push({ alias, targets });
- }
-
- rows.sort((a, b) => {
- // Sort the default alias ("") last for readability in step-summary tables.
- // We substitute it with U+ffff only for comparison; it is a noncharacter and
- // reliably sorts after typical printable alias strings in localeCompare.
- const left = a.alias || "\uffff";
- const right = b.alias || "\uffff";
- return left.localeCompare(right);
- });
- return rows;
-}
-
-/**
- * @param {string} value
- * @returns {string}
- */
-function escapeTableValue(value) {
- return value.replace(/\|/g, "\\|");
-}
-
-/**
- * @param {Array<{alias: string, targets: string[]}>} aliasRows
- * @returns {string}
- */
-function renderModelAliasSummary(aliasRows) {
- const lines = [];
- lines.push("");
- lines.push(`AWF model aliases (${aliasRows.length})
`);
- lines.push("");
- lines.push("| Alias | Resolution order |");
- lines.push("|-------|------------------|");
- for (const row of aliasRows) {
- const aliasLabel = row.alias === "" ? "(default)" : `\`${escapeTableValue(row.alias)}\``;
- const resolutionOrder = row.targets.map(target => `\`${escapeTableValue(target)}\``).join(" → ");
- lines.push(`| ${aliasLabel} | ${resolutionOrder} |`);
- }
- lines.push("");
- lines.push(" ");
- lines.push("");
- return lines.join("\n");
-}
-
-/**
- * @param {Array<{alias: string, targets: string[]}>} aliasRows
- * @param {(message: string) => void} warn
- */
-function writeAliasSummary(aliasRows, warn) {
- if (aliasRows.length === 0) {
- return;
- }
-
- const summaryPath = process.env.GITHUB_STEP_SUMMARY;
- if (!summaryPath) {
- return;
- }
-
- try {
- fs.appendFileSync(summaryPath, renderModelAliasSummary(aliasRows), "utf8");
- } catch (error) {
- warn(`warning: failed to write AWF model alias summary: ${String(error)}`);
- }
-}
-
-/**
- * @param {object} options
- * @param {string} options.configPath
- * @param {string} options.multipliersPath
- * @param {(message: string) => void} options.warn
- */
-function mergeModelMultipliers({ configPath, multipliersPath, warn }) {
- if (!fs.existsSync(configPath) || !fs.existsSync(multipliersPath)) {
- return;
- }
-
- /** @type {Record | null} */
- let multipliersDoc = null;
- try {
- const parsed = JSON.parse(fs.readFileSync(multipliersPath, "utf8"));
- if (isPlainObject(parsed)) {
- multipliersDoc = parsed;
- }
- } catch (error) {
- warn(`warning: failed to parse model multipliers file: ${String(error)}`);
- return;
- }
- if (!multipliersDoc || !isPlainObject(multipliersDoc.multipliers)) {
- return;
- }
-
- const normalized = normalizeMultipliers(multipliersDoc.multipliers);
-
- /** @type {Record | null} */
- let configDoc = null;
- try {
- const parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
- if (isPlainObject(parsed)) {
- configDoc = parsed;
- }
- } catch (error) {
- warn(`warning: failed to parse awf-config.json before model multiplier merge: ${String(error)}`);
- return;
- }
- if (!configDoc) {
- return;
- }
-
- const apiProxy = isPlainObject(configDoc.apiProxy) ? configDoc.apiProxy : {};
- const aliasRows = normalizeAliasRows(apiProxy.models);
- if (Object.keys(normalized).length > 0) {
- apiProxy.modelMultipliers = normalized;
- } else {
- delete apiProxy.modelMultipliers;
- }
- configDoc.apiProxy = apiProxy;
-
- fs.writeFileSync(configPath, JSON.stringify(configDoc), "utf8");
- writeAliasSummary(aliasRows, warn);
-}
-
-/**
- * @param {object} [options]
- * @param {string} [options.runnerTemp]
- * @param {string} [options.configPath]
- * @param {string} [options.multipliersPath]
- * @param {(message: string) => void} [options.warn]
- */
-function main(options = {}) {
- const runnerTemp = options.runnerTemp ?? process.env.RUNNER_TEMP;
- if (!runnerTemp) {
- throw new Error("RUNNER_TEMP is required");
- }
-
- const configPath = options.configPath ?? path.join(runnerTemp, "gh-aw", "awf-config.json");
- const multipliersPath = options.multipliersPath ?? process.env.GH_AW_MODEL_MULTIPLIERS_PATH ?? DEFAULT_MODEL_MULTIPLIERS_PATH;
- const warn = options.warn ?? (message => process.stderr.write(`${message}\n`));
-
- mergeModelMultipliers({ configPath, multipliersPath, warn });
-}
-
-if (require.main === module) {
- try {
- main();
- } catch (error) {
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
- process.exit(1);
- }
-}
-
-module.exports = {
- DEFAULT_MODEL_MULTIPLIERS_PATH,
- isPlainObject,
- normalizeMultipliers,
- normalizeAliasRows,
- renderModelAliasSummary,
- writeAliasSummary,
- mergeModelMultipliers,
- main,
-};
diff --git a/setup/js/messages_footer.cjs b/setup/js/messages_footer.cjs
index 7267768..b5c78cf 100644
--- a/setup/js/messages_footer.cjs
+++ b/setup/js/messages_footer.cjs
@@ -181,6 +181,7 @@ function getFooterMessage(ctx) {
aiCreditsFormatted = explicitContextAIC ? formatAIC(explicitContextAIC) : undefined;
aiCreditsSuffix = aiCreditsFormatted ? ` · ${aiCreditsFormatted} AIC` : "";
}
+ const aiCreditsSuffixForTemplate = `${aiCreditsSuffix}${envAmbientContextSuffix}`;
// Create context with both camelCase and snake_case keys, including computed history_link and agentic_workflow_url
const templateContext = toSnakeCase({
@@ -189,7 +190,7 @@ function getFooterMessage(ctx) {
historyLink,
agenticWorkflowUrl,
aiCreditsFormatted,
- aiCreditsSuffix,
+ aiCreditsSuffix: aiCreditsSuffixForTemplate,
ambientContext,
ambientContextFormatted: envAmbientContextFormatted,
ambientContextSuffix: envAmbientContextSuffix,
@@ -354,6 +355,7 @@ function getFooterAgentFailureIssueMessage(ctx) {
const aiCredits = hasExplicitContextAIC ? explicitContextAIC : envAIC;
const aiCreditsFormatted = hasExplicitContextAIC ? (explicitContextAIC ? formatAIC(explicitContextAIC) : undefined) : envAICFormatted;
const aiCreditsSuffix = hasExplicitContextAIC ? (aiCreditsFormatted ? ` · ${aiCreditsFormatted} AIC` : "") : envAICSuffix;
+ const aiCreditsSuffixForTemplate = `${aiCreditsSuffix}${ambientContextSuffix}`;
// Create context with both camelCase and snake_case keys, including computed history_link and agentic_workflow_url
const templateContext = toSnakeCase({
@@ -362,7 +364,7 @@ function getFooterAgentFailureIssueMessage(ctx) {
agenticWorkflowUrl,
aiCredits,
aiCreditsFormatted,
- aiCreditsSuffix,
+ aiCreditsSuffix: aiCreditsSuffixForTemplate,
agentAiCredits,
agentAiCreditsFormatted,
agentAiCreditsSuffix,
@@ -382,10 +384,10 @@ function getFooterAgentFailureIssueMessage(ctx) {
// Default footer template with link to workflow run
let defaultFooter = "> Generated from [{workflow_name}]({run_url})";
if (aiCredits) {
- defaultFooter += "{ai_credits_suffix}";
+ defaultFooter += aiCreditsSuffix;
}
if (ambientContext) {
- defaultFooter += "{ambient_context_suffix}";
+ defaultFooter += ambientContextSuffix;
}
// Append history link when available
if (ctx.historyUrl) {
@@ -422,11 +424,13 @@ function getFooterAgentFailureCommentMessage(ctx) {
threatDetectionAiCreditsFormatted,
threatDetectionAiCreditsSuffix,
} = getAICFromEnv();
+ const { ambientContext, ambientContextFormatted, ambientContextSuffix } = getAmbientContextFromEnv();
const hasExplicitContextAIC = ctx.aiCredits !== undefined && ctx.aiCredits !== null;
const explicitContextAIC = parseExplicitContextAIC(ctx.aiCredits);
const aiCredits = hasExplicitContextAIC ? explicitContextAIC : envAIC;
const aiCreditsFormatted = hasExplicitContextAIC ? (explicitContextAIC ? formatAIC(explicitContextAIC) : undefined) : envAICFormatted;
const aiCreditsSuffix = hasExplicitContextAIC ? (aiCreditsFormatted ? ` · ${aiCreditsFormatted} AIC` : "") : envAICSuffix;
+ const aiCreditsSuffixForTemplate = `${aiCreditsSuffix}${ambientContextSuffix}`;
// Create context with both camelCase and snake_case keys, including computed history_link and agentic_workflow_url
const templateContext = toSnakeCase({
@@ -435,13 +439,16 @@ function getFooterAgentFailureCommentMessage(ctx) {
agenticWorkflowUrl,
aiCredits,
aiCreditsFormatted,
- aiCreditsSuffix,
+ aiCreditsSuffix: aiCreditsSuffixForTemplate,
agentAiCredits,
agentAiCreditsFormatted,
agentAiCreditsSuffix,
threatDetectionAiCredits,
threatDetectionAiCreditsFormatted,
threatDetectionAiCreditsSuffix,
+ ambientContext,
+ ambientContextFormatted,
+ ambientContextSuffix,
});
// Use custom agent failure comment footer if configured, otherwise use default footer
@@ -452,7 +459,10 @@ function getFooterAgentFailureCommentMessage(ctx) {
// Default footer template with link to workflow run
let defaultFooter = "> Generated from [{workflow_name}]({run_url})";
if (aiCredits) {
- defaultFooter += "{ai_credits_suffix}";
+ defaultFooter += aiCreditsSuffix;
+ }
+ if (ambientContext) {
+ defaultFooter += ambientContextSuffix;
}
// Append history link when available
if (ctx.historyUrl) {
diff --git a/setup/js/model_multipliers.json b/setup/js/model_multipliers.json
deleted file mode 100644
index 7842ba1..0000000
--- a/setup/js/model_multipliers.json
+++ /dev/null
@@ -1,161 +0,0 @@
-{
- "version": "1",
- "description": "Effective Tokens (ET) computation data per the gh-aw Effective Tokens Specification v0.2.0. Token class weights are applied first to normalize across token classes, then the per-model multiplier scales the result relative to the reference model. The registry keeps complete model history; entries are removed only by explicit manual deletion.",
- "reference_model": "claude-sonnet-4.5",
- "token_class_weights": {
- "input": 1.0,
- "cached_input": 0.1,
- "output": 4.0,
- "reasoning": 4.0,
- "cache_write": 1.0
- },
- "multipliers": {
- "claude-haiku-4-5": 0.33,
- "claude-haiku-4.5": 0.33,
- "claude-haiku-4-5-20251001": 0.33,
- "claude-3-5-haiku": 0.1,
- "claude-3-haiku": 0.1,
- "claude-sonnet-4": 1.0,
- "claude-sonnet-4-20250514": 1.0,
- "claude-sonnet-4-5": 6.0,
- "claude-sonnet-4.5": 6.0,
- "claude-sonnet-4-5-20250929": 6.0,
- "claude-sonnet-4-6": 9.0,
- "claude-sonnet-4.6": 9.0,
- "claude-3-5-sonnet": 1.0,
- "claude-3-7-sonnet": 1.0,
- "claude-3-sonnet": 1.0,
- "claude-opus-4": 5.0,
- "claude-opus-4-20250514": 5.0,
- "claude-opus-4-1": 5.0,
- "claude-opus-4-1-20250805": 5.0,
- "claude-opus-4-5": 15.0,
- "claude-opus-4-5-20251101": 15.0,
- "claude-opus-4-6": 27.0,
- "claude-opus-4-7": 27.0,
- "claude-opus-4-8": 27.0,
- "claude-opus-4.5": 15.0,
- "claude-opus-4.6": 27.0,
- "claude-opus-4.6-fast": 27.0,
- "claude-opus-4.7": 27.0,
- "claude-opus-4.8": 27.0,
- "claude-3-5-opus": 5.0,
- "claude-3-opus": 5.0,
- "gpt-4o-2024-05-13": 0.33,
- "gpt-4o-2024-08-06": 0.33,
- "gpt-4o-2024-11-20": 0.33,
- "gpt-4o-mini-2024-07-18": 0.33,
- "gpt-4.1-2025-04-14": 1.0,
- "gpt-41-copilot": 1.0,
- "gpt-4.1-mini": 1.0,
- "gpt-4.1-nano": 1.0,
- "gpt-4-turbo": 1.0,
- "gpt-4": 1.0,
- "gpt-4-0613": 1.0,
- "gpt-4-o-preview": 1.0,
- "gpt-3.5-turbo": 0.0,
- "gpt-3.5-turbo-0613": 0.0,
- "gpt-5": 1.0,
- "gpt-5-2025-08-07": 1.0,
- "gpt-5-search-api": 1.0,
- "gpt-5-search-api-2025-10-14": 1.0,
- "gpt-5-chat-latest": 1.0,
- "gpt-5-mini": 0.33,
- "gpt-5-mini-2025-08-07": 0.33,
- "gpt-5-nano": 0.05,
- "gpt-5-nano-2025-08-07": 0.05,
- "gpt-5-pro": 2.0,
- "gpt-5-pro-2025-10-06": 2.0,
- "gpt-5.1": 3.0,
- "gpt-5.1-2025-11-13": 3.0,
- "gpt-5.1-chat-latest": 3.0,
- "gpt-5-codex": 1.0,
- "gpt-5.1-codex": 3.0,
- "gpt-5.1-codex-mini": 0.33,
- "gpt-5.1-codex-max": 3.0,
- "gpt-5.1-codex-max-customsummarizer": 3.0,
- "gpt-5.2": 3.0,
- "gpt-5.2-2025-12-11": 3.0,
- "gpt-5.2-chat-latest": 3.0,
- "gpt-5.2-codex": 3.0,
- "gpt-5.2-pro": 3.0,
- "gpt-5.2-pro-2025-12-11": 3.0,
- "gpt-5.3-chat-latest": 3.0,
- "gpt-5.3-codex": 6.0,
- "gpt-5.3-codex-api-preview": 6.0,
- "gpt-5.3-codex-api-preview-preambles": 6.0,
- "gpt-5.4": 6.0,
- "gpt-5.4-2026-03-05": 6.0,
- "gpt-5.4-mini": 6.0,
- "gpt-5.4-mini-2026-03-17": 6.0,
- "gpt-5.4-nano-2026-03-17": 6.0,
- "gpt-5.4-pro": 6.0,
- "gpt-5.4-pro-2026-03-05": 6.0,
- "gpt-5.5": 57.0,
- "gpt-5.5-2026-04-23": 57.0,
- "gpt-5.5-pro": 2.0,
- "gpt-5.5-pro-2026-04-23": 2.0,
- "o1": 3.0,
- "o1-2024-12-17": 3.0,
- "o1-mini": 0.5,
- "o1-pro": 10.0,
- "o1-pro-2025-03-19": 10.0,
- "o3": 3.0,
- "o3-2025-04-16": 3.0,
- "o3-mini": 0.5,
- "o3-mini-2025-01-31": 0.5,
- "o3-pro": 10.0,
- "o3-pro-2025-06-10": 10.0,
- "o3-deep-research": 3.0,
- "o3-deep-research-2025-06-26": 3.0,
- "o4-mini": 0.5,
- "o4-mini-2025-04-16": 0.5,
- "o4-mini-deep-research": 0.5,
- "o4-mini-deep-research-2025-06-26": 0.5,
- "gemini-2.5-pro": 1.0,
- "gemini-2.5-pro-preview-tts": 1.0,
- "gemini-2.5-flash": 0.2,
- "gemini-2.5-flash-native-audio-latest": 0.2,
- "gemini-2.5-flash-native-audio-preview-09-2025": 0.2,
- "gemini-2.5-flash-native-audio-preview-12-2025": 0.2,
- "gemini-2.5-flash-preview-tts": 0.2,
- "gemini-2.5-flash-image": 0.2,
- "gemini-2.5-flash-lite": 0.1,
- "gemini-2.0-flash": 0.1,
- "gemini-2.0-flash-001": 0.1,
- "gemini-2.0-flash-lite": 0.1,
- "gemini-2.0-flash-lite-001": 0.1,
- "gemini-1.5-pro": 1.0,
- "gemini-1.5-flash": 0.1,
- "gemini-flash-latest": 0.2,
- "gemini-flash-lite-latest": 0.1,
- "gemini-pro-latest": 1.0,
- "gemini-3-flash-preview": 0.33,
- "gemini-3-pro-preview": 6.0,
- "gemini-3-pro-image": 6.0,
- "gemini-3-pro-image-preview": 6.0,
- "gemini-3.1-pro-preview": 6.0,
- "gemini-3.1-pro-preview-customtools": 6.0,
- "gemini-3.1-flash-live-preview": 0.1,
- "gemini-3.1-flash-lite": 0.1,
- "gemini-3.1-flash-lite-preview": 0.1,
- "gemini-3.1-flash-image": 0.33,
- "gemini-3.1-flash-image-preview": 0.33,
- "gemini-3.1-flash-tts-preview": 0.1,
- "gemini-3.5-flash": 14.0,
- "antigravity-preview-05-2026": 1.0,
- "nano-banana-pro-preview": 0.2,
- "gemini-2.5-computer-use-preview": 0.2,
- "gemini-2.5-computer-use-preview-10-2025": 0.2,
- "gemini-robotics-er-1.5-preview": 0.2,
- "gemini-robotics-er-1.6-preview": 0.2,
- "deep-research-max-preview-04-2026": 1.0,
- "deep-research-preview-04-2026": 1.0,
- "deep-research-pro-preview-12-2025": 1.0,
- "MAI-Code-1-Flash": 0.33,
- "gemma-4-26b-a4b-it": 0.1,
- "gemma-4-31b-it": 0.2,
- "grok-code-fast-1": 0.33,
- "raptor-mini": 0.33
- }
-}
diff --git a/setup/js/package.json b/setup/js/package.json
index 978b6c4..be9040c 100644
--- a/setup/js/package.json
+++ b/setup/js/package.json
@@ -1,6 +1,5 @@
{
"devDependencies": {
- "@actions/artifact": "^6.0.0",
"@actions/core": "^3.0.1",
"@actions/exec": "^3.0.0",
"@actions/github": "^9.1.1",
diff --git a/setup/js/upload_artifact.cjs b/setup/js/upload_artifact.cjs
index 56b01f3..d521024 100644
--- a/setup/js/upload_artifact.cjs
+++ b/setup/js/upload_artifact.cjs
@@ -5,7 +5,7 @@
* upload_artifact handler
*
* Validates artifact upload requests emitted by the model via the upload_artifact safe output
- * tool, then uploads the approved files directly via the @actions/artifact REST API client.
+ * tool, then uploads the approved files directly via the internal artifact client.
*
* Files can be pre-staged in /tmp/gh-aw/safeoutputs/upload-artifacts/ or referenced by their
* original path. When a requested path is not found in the staging directory the handler
@@ -34,6 +34,7 @@
const fs = require("fs");
const path = require("path");
+const { DefaultArtifactClient } = require("./artifact_client.cjs");
const { getErrorMessage } = require("./error_helpers.cjs");
const { globPatternToRegex } = require("./glob_pattern_helpers.cjs");
const { ERR_VALIDATION } = require("./error_codes.cjs");
@@ -421,16 +422,14 @@ function deriveArtifactName(request, slotIndex) {
}
/**
- * Create or return the @actions/artifact DefaultArtifactClient.
+ * Create or return the internal DefaultArtifactClient.
* global.__createArtifactClient can be set in tests to inject a mock client factory.
- * Uses dynamic import() because @actions/artifact v2+ is an ES module.
* @returns {Promise<{ uploadArtifact: (name: string, files: string[], rootDir: string, opts: object) => Promise<{id?: number, size?: number}> }>}
*/
async function getArtifactClient() {
if (typeof global.__createArtifactClient === "function") {
return global.__createArtifactClient();
}
- const { DefaultArtifactClient } = await import("@actions/artifact");
return new DefaultArtifactClient();
}
@@ -532,7 +531,7 @@ async function main(config = {}) {
let artifactUrl = "";
if (!isStaged) {
- // Upload files directly via @actions/artifact REST API.
+ // Upload files directly via the internal artifact client.
const absoluteFiles = files.map(f => path.join(STAGING_DIR, f));
const client = await getArtifactClient();
try {
diff --git a/setup/md/agent_failure_comment.md b/setup/md/agent_failure_comment.md
index 38ed013..46ba4fb 100644
--- a/setup/md/agent_failure_comment.md
+++ b/setup/md/agent_failure_comment.md
@@ -1,3 +1,3 @@
Agent job [{run_id}]({run_url}) failed.
-{secret_verification_context}{credential_auth_error_context}{inference_access_error_context}{mcp_policy_error_context}{model_not_supported_error_context}{ai_credits_rate_limit_error_context}{unknown_model_ai_credits_context}{app_token_minting_failed_context}{lockdown_check_failed_context}{stale_lock_file_failed_context}{daily_effective_workflow_exceeded_context}{assignment_errors_context}{assign_copilot_failure_context}{create_discussion_errors_context}{code_push_failure_context}{repo_memory_validation_context}{push_repo_memory_failure_context}{missing_data_context}{missing_tool_context}{permission_denied_context}{tool_denials_exceeded_context}{report_incomplete_context}{missing_safe_outputs_context}{engine_failure_context}{timeout_context}{fork_context}
+{secret_verification_context}{credential_auth_error_context}{inference_access_error_context}{mcp_policy_error_context}{model_not_supported_error_context}{ai_credits_rate_limit_error_context}{unknown_model_ai_credits_context}{app_token_minting_failed_context}{lockdown_check_failed_context}{stale_lock_file_failed_context}{daily_ai_credits_exceeded_context}{assignment_errors_context}{assign_copilot_failure_context}{create_discussion_errors_context}{code_push_failure_context}{repo_memory_validation_context}{push_repo_memory_failure_context}{missing_data_context}{missing_tool_context}{permission_denied_context}{tool_denials_exceeded_context}{report_incomplete_context}{missing_safe_outputs_context}{engine_failure_context}{timeout_context}{fork_context}
diff --git a/setup/md/agent_failure_issue.md b/setup/md/agent_failure_issue.md
index 345c742..a2162cf 100644
--- a/setup/md/agent_failure_issue.md
+++ b/setup/md/agent_failure_issue.md
@@ -4,7 +4,7 @@
**Branch:** {branch}
**Run:** {run_url}{pull_request_info}
-{secret_verification_context}{credential_auth_error_context}{inference_access_error_context}{mcp_policy_error_context}{model_not_supported_error_context}{ai_credits_rate_limit_error_context}{unknown_model_ai_credits_context}{app_token_minting_failed_context}{lockdown_check_failed_context}{stale_lock_file_failed_context}{daily_effective_workflow_exceeded_context}{assignment_errors_context}{assign_copilot_failure_context}{create_discussion_errors_context}{code_push_failure_context}{repo_memory_validation_context}{push_repo_memory_failure_context}{missing_data_context}{missing_tool_context}{permission_denied_context}{tool_denials_exceeded_context}{report_incomplete_context}{missing_safe_outputs_context}{engine_failure_context}{timeout_context}{fork_context}
+{secret_verification_context}{credential_auth_error_context}{inference_access_error_context}{mcp_policy_error_context}{model_not_supported_error_context}{ai_credits_rate_limit_error_context}{unknown_model_ai_credits_context}{app_token_minting_failed_context}{lockdown_check_failed_context}{stale_lock_file_failed_context}{daily_ai_credits_exceeded_context}{assignment_errors_context}{assign_copilot_failure_context}{create_discussion_errors_context}{code_push_failure_context}{repo_memory_validation_context}{push_repo_memory_failure_context}{missing_data_context}{missing_tool_context}{permission_denied_context}{tool_denials_exceeded_context}{report_incomplete_context}{missing_safe_outputs_context}{engine_failure_context}{timeout_context}{fork_context}
### Action Required
diff --git a/setup/md/daily_workflow_aic_exceeded.md b/setup/md/daily_workflow_aic_exceeded.md
index 325ccb9..00f5972 100644
--- a/setup/md/daily_workflow_aic_exceeded.md
+++ b/setup/md/daily_workflow_aic_exceeded.md
@@ -40,7 +40,7 @@ Commit and push the updated `.lock.yml` file.
The `max-daily-ai-credits` frontmatter option sets a per-workflow spending cap measured in *AI Credits* across the 24-hour window before the current run. The cap is scoped to the repository and workflow — it aggregates usage across all runs of this workflow regardless of who triggered them.
-When the aggregated AI Credits usage across all completed runs of this workflow in the last 24 hours exceeds the threshold, the activation job sets the `daily_effective_workflow_exceeded` output to `true` and the agent job is skipped for that run. The conclusion job still runs and creates this report.
+When the aggregated AI Credits usage across all completed runs of this workflow in the last 24 hours exceeds the threshold, the activation job sets the `daily_ai_credits_exceeded` output to `true` and the agent job is skipped for that run. The conclusion job still runs and creates this report.
The guardrail is evaluated at activation time, not retrospectively, so a single very large run that pushes usage over the threshold only blocks *subsequent* runs in the same window — it does not cancel a run that is already in progress.
diff --git a/setup/setup.sh b/setup/setup.sh
index 8e16b27..e13caa6 100755
--- a/setup/setup.sh
+++ b/setup/setup.sh
@@ -84,9 +84,6 @@ DESTINATION="${INPUT_DESTINATION:-${GH_AW_ROOT}/actions}"
# Get safe-output-custom-tokens flag from input (default: false)
SAFE_OUTPUT_CUSTOM_TOKENS_ENABLED="${INPUT_SAFE_OUTPUT_CUSTOM_TOKENS:-false}"
-# Get safe-output-artifact-client flag from input (default: false)
-SAFE_OUTPUT_ARTIFACT_CLIENT_ENABLED="${INPUT_SAFE_OUTPUT_ARTIFACT_CLIENT:-false}"
-
debug_log "Copying activation files to ${DESTINATION}"
debug_log "Safe-output custom tokens support: ${SAFE_OUTPUT_CUSTOM_TOKENS_ENABLED}"
@@ -396,38 +393,6 @@ fi
echo "Successfully copied ${SAFE_OUTPUTS_COUNT} safe-outputs files to ${SAFE_OUTPUTS_DEST}"
-# Install @actions/artifact package if upload-artifact safe output is configured.
-# upload_artifact.cjs uses DefaultArtifactClient to upload via Actions REST API directly.
-if [ "${SAFE_OUTPUT_ARTIFACT_CLIENT_ENABLED}" = "true" ]; then
- echo "Artifact client enabled - installing @actions/artifact package in ${DESTINATION}..."
- cd "${DESTINATION}"
-
- # Check if npm is available
- if ! command -v npm &> /dev/null; then
- echo "::error::npm is not available. Cannot install @actions/artifact package."
- exit 1
- fi
-
- # Create a minimal package.json if it doesn't exist
- if [ ! -f "package.json" ]; then
- echo '{"private": true}' > package.json
- fi
-
- # Install @actions/artifact package
- npm install --ignore-scripts --no-save --loglevel=error @actions/artifact@^6.0.0 2>&1 | grep -v "npm WARN" || true
- if [ -d "node_modules/@actions/artifact" ]; then
- echo "✓ Successfully installed @actions/artifact package"
- else
- echo "::error::Failed to install @actions/artifact package"
- exit 1
- fi
-
- # Return to original directory
- cd - > /dev/null
-else
- debug_log "Artifact client not enabled - skipping @actions/artifact installation"
-fi
-
# Send OTLP job setup span when configured (non-fatal).
# Delegates to action_setup_otlp.cjs (same file used by actions/setup/index.js)
# to keep dev/release and script mode behavior in sync.