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
4 changes: 4 additions & 0 deletions setup/js/action_input_utils.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
* some runner versions preserve the original hyphen from the input name. Checking
* both forms ensures the value is resolved regardless of the runner version.
*
* The underscore form has precedence: if `INPUT_<NAME>` exists (even as whitespace),
* it is used. The hyphen form is only checked if the underscore form is absent or
* empty string.
*
* @param {string} name - Input name in UPPER_UNDERSCORE form (e.g. "JOB_NAME")
* @returns {string} Trimmed input value, or "" if not set.
*/
Expand Down
43 changes: 39 additions & 4 deletions setup/js/awf_reflect.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const AWF_API_PROXY_REFLECT_URL = "http://api-proxy:10000/reflect";
// co-located with other AWF firewall observability data so it is included in the agent artifact.
const AWF_REFLECT_OUTPUT_PATH = "/tmp/gh-aw/sandbox/firewall/awf-reflect.json";
// Milliseconds to wait for the /reflect endpoint before giving up.
const AWF_REFLECT_TIMEOUT_MS = 5000;
const AWF_REFLECT_TIMEOUT_MS = 60000;
// Milliseconds to wait for each models_url fallback fetch (shorter than the main reflect timeout).
const AWF_MODELS_URL_TIMEOUT_MS = 3000;
// Gemini model name prefix stripped from model IDs in the Gemini models API response.
Expand Down Expand Up @@ -165,7 +165,15 @@ async function enrichReflectModels(reflectData, timeoutMs, logger) {
* logger?: (msg: string) => void,
* writeFileSync?: (path: string, data: string, options: object) => void,
* }=} options
* @returns {Promise<void>}
* @returns {Promise<{
* ok: boolean,
* reflectUrl: string,
* outputPath: string,
* bytesWritten?: number,
* reason?: "unexpected_status"|"timeout"|"request_failed",
* status?: number,
* error?: string,
* }>}
*/
async function fetchAWFReflect(options) {
const reflectUrl = (options && options.reflectUrl) || AWF_API_PROXY_REFLECT_URL;
Expand All @@ -178,7 +186,9 @@ async function fetchAWFReflect(options) {
logger(`awf-reflect: fetching ${reflectUrl} (timeout=${timeoutMs}ms)`);

const ac = new AbortController();
let timedOut = false;
const timer = setTimeout(() => {
timedOut = true;
logger(`awf-reflect: request timed out after ${timeoutMs}ms`);
ac.abort();
}, timeoutMs);
Expand All @@ -187,7 +197,13 @@ async function fetchAWFReflect(options) {
const res = await fetch(reflectUrl, { signal: ac.signal });
if (!res.ok) {
logger(`awf-reflect: unexpected status ${res.status}, skipping`);
return;
return {
ok: false,
reflectUrl,
outputPath,
reason: "unexpected_status",
status: res.status,
};
}
const reflectData = await res.json();
// Attempt to fill in null models for configured providers by fetching directly
Expand All @@ -198,12 +214,31 @@ async function fetchAWFReflect(options) {
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
writeFile(outputPath, enrichedBody, { encoding: "utf8" });
logger(`awf-reflect: saved ${enrichedBody.length}B to ${outputPath}`);
return {
ok: true,
reflectUrl,
outputPath,
bytesWritten: enrichedBody.length,
};
} catch (err) {
const e = /** @type {Error} */ err;
if (e.name === "AbortError") {
return; // already logged above
return {
ok: false,
reflectUrl,
outputPath,
reason: "timeout",
error: timedOut ? `request timed out after ${timeoutMs}ms` : e.message,
};
}
logger(`awf-reflect: request failed: ${e.message}`);
return {
ok: false,
reflectUrl,
outputPath,
reason: "request_failed",
error: e.message,
};
} finally {
clearTimeout(timer);
}
Expand Down
173 changes: 171 additions & 2 deletions setup/js/awf_reflect_summary.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

const fs = require("fs");

const AWF_CONFIG_PATH = "/tmp/gh-aw/awf-config.json";
const AWF_REFLECT_PATH = "/tmp/gh-aw/sandbox/firewall/awf-reflect.json";
const AWF_MODELS_PATH = "/tmp/gh-aw/sandbox/firewall/models.json";

/**
* Read the AWF reflect payload that was persisted to disk by copilot_harness.cjs.
Expand All @@ -21,6 +23,40 @@ function readReflectData() {
}
}

/**
* Read the AWF config payload when available.
* Returns null when the file is absent or unparseable.
*
* @returns {object|null}
*/
function readAWFConfigData() {
if (!fs.existsSync(AWF_CONFIG_PATH)) {
return null;
}
try {
return JSON.parse(fs.readFileSync(AWF_CONFIG_PATH, "utf8"));
} catch {
return null;
}
}

/**
* Read the sandbox.firewall models.json payload when available.
* Returns null when the file is absent or unparseable.
*
* @returns {object|null}
*/
function readRuntimeModelsData() {
if (!fs.existsSync(AWF_MODELS_PATH)) {
return null;
}
try {
return JSON.parse(fs.readFileSync(AWF_MODELS_PATH, "utf8"));
} catch {
return null;
}
}

/**
* Format a list of model IDs into a compact comma-separated string, capping the output
* at `maxModels` entries and appending "… +N more" when the list is longer.
Expand All @@ -40,6 +76,104 @@ function formatModelList(models, maxModels) {
return `${shown.join(", ")} … +${remaining} more`;
}

/**
* Normalize runtime model entries to a common table-friendly shape.
*
* Supported payload shapes:
* - { endpoints: [...] }
* - { providers: { [providerName]: { ... } } }
* - { provider: "x", models: [...] }
*
* @param {any} runtimeModelsData
* @returns {Array<{provider: string, endpoint: string, models: string[]}>}
*/
function normalizeRuntimeModelRows(runtimeModelsData) {
if (!runtimeModelsData || typeof runtimeModelsData !== "object") {
return [];
}

/** @type {Array<{provider: string, endpoint: string, models: string[]}>} */
const rows = [];

/**
* @param {string} provider
* @param {any} entry
*/
function pushRow(provider, entry) {
const modelIds = extractRuntimeModelIds(entry?.models || entry?.available_models || entry?.detected_models || entry?.model_ids || entry?.availableModels);
rows.push({
provider: String(provider || entry?.provider || entry?.name || "unknown"),
endpoint: String(entry?.endpoint || entry?.base_url || entry?.baseUrl || entry?.url || entry?.models_url || entry?.modelsUrl || "—"),
models: modelIds,
});
}

if (Array.isArray(runtimeModelsData.endpoints)) {
for (const entry of runtimeModelsData.endpoints) {
pushRow(entry?.provider, entry);
}
}

if (runtimeModelsData.providers && typeof runtimeModelsData.providers === "object") {
for (const [provider, entry] of Object.entries(runtimeModelsData.providers)) {
pushRow(provider, entry);
}
}

if (typeof runtimeModelsData.provider === "string" && Array.isArray(runtimeModelsData.models)) {
pushRow(runtimeModelsData.provider, runtimeModelsData);
}

return rows.sort((a, b) => a.provider.localeCompare(b.provider) || a.endpoint.localeCompare(b.endpoint));
}

/**
* Extract model IDs from runtime models.json payload entries.
*
* @param {any} models
* @returns {string[]}
*/
function extractRuntimeModelIds(models) {
if (!Array.isArray(models)) {
return [];
}

return models
.map(model => {
if (typeof model === "string") return model;
if (!model || typeof model !== "object") return null;
return model.id || model.name || model.model || null;
})
.filter(Boolean)
.sort();
}

/**
* Normalize model aliases from awf-config.json into a table-friendly shape.
*
* @param {any} awfConfigData
* @returns {Array<{alias: string, label: string, targets: string[]}>}
*/
function normalizeModelAliasRows(awfConfigData) {
const aliasMap = awfConfigData?.apiProxy?.models;
if (!aliasMap || typeof aliasMap !== "object" || Array.isArray(aliasMap)) {
return [];
}

return Object.entries(aliasMap)
.filter(([, targets]) => Array.isArray(targets))
.map(([alias, targets]) => ({
alias,
label: alias === "" ? "(default)" : alias,
targets: targets.map(target => String(target)),
}))
.sort((a, b) => {
if (a.alias === "") return -1;
if (b.alias === "") return 1;
return a.alias.localeCompare(b.alias);
});
}

/**
* Build a markdown step summary from AWF /reflect response data.
*
Expand All @@ -51,13 +185,15 @@ function formatModelList(models, maxModels) {
* - Available models (first `maxModels` entries, with overflow indicator)
*
* @param {object} reflectData - Parsed /reflect JSON response
* @param {{ maxModels?: number }} options
* @param {{ maxModels?: number, runtimeModelsData?: object, awfConfigData?: object }} options
* @returns {string}
*/
function buildReflectSummary(reflectData, options) {
const maxModels = options && options.maxModels != null ? options.maxModels : 5;
const endpoints = Array.isArray(reflectData.endpoints) ? reflectData.endpoints : [];
const fetchComplete = reflectData.models_fetch_complete === true;
const runtimeModelRows = normalizeRuntimeModelRows(options && options.runtimeModelsData);
const modelAliasRows = normalizeModelAliasRows(options && options.awfConfigData);

const lines = [];
lines.push("<details>");
Expand All @@ -70,6 +206,8 @@ function buildReflectSummary(reflectData, options) {
lines.push("No endpoint information available.");
} else {
const fetchNote = fetchComplete ? "" : " *(model list may be incomplete — fetch in progress)*";
lines.push("Configured endpoints");
lines.push("");
lines.push(`| Provider | Port | Configured | Available models${fetchNote} |`);
lines.push("|----------|------|:----------:|-----------------|");

Expand All @@ -82,6 +220,28 @@ function buildReflectSummary(reflectData, options) {
}
}

if (runtimeModelRows.length > 0) {
lines.push("");
lines.push("Runtime models.json");
lines.push("");
lines.push("| Provider | Endpoint | Available models |");
lines.push("|----------|----------|------------------|");
for (const row of runtimeModelRows) {
lines.push(`| ${row.provider} | ${row.endpoint} | ${formatModelList(row.models, maxModels)} |`);
}
}

if (modelAliasRows.length > 0) {
lines.push("");
lines.push("Model aliases");
lines.push("");
lines.push("| Alias | Resolution order |");
lines.push("|-------|------------------|");
for (const row of modelAliasRows) {
lines.push(`| ${row.label} | ${formatModelList(row.targets, maxModels)} |`);
}
}

lines.push("");
lines.push("</details>");
lines.push("");
Expand All @@ -90,24 +250,33 @@ function buildReflectSummary(reflectData, options) {
}

async function main() {
const awfConfigData = readAWFConfigData();
const reflectData = readReflectData();
const runtimeModelsData = readRuntimeModelsData();

if (!reflectData) {
core.info("AWF reflect data not available (AWF not enabled or /reflect not reachable), skipping summary");
return;
}

const markdown = buildReflectSummary(reflectData, {});
const markdown = buildReflectSummary(reflectData, { awfConfigData, runtimeModelsData });
await core.summary.addRaw(markdown).write();
core.info("AWF reflect summary written to step summary");
}

if (typeof module !== "undefined" && module.exports) {
module.exports = {
AWF_CONFIG_PATH,
AWF_MODELS_PATH,
AWF_REFLECT_PATH,
buildReflectSummary,
extractRuntimeModelIds,
formatModelList,
main,
normalizeModelAliasRows,
readAWFConfigData,
readReflectData,
readRuntimeModelsData,
normalizeRuntimeModelRows,
};
}
Loading
Loading