mirror of
https://github.com/dotnet/skills.git
synced 2026-09-20 09:49:54 +08:00
fix: validate health publisher payloads
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+100
-29
@@ -1,4 +1,4 @@
|
||||
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"3cdc7e2ddd6f2b8f743783b151988ab8a27337b6d9e56e11e09cd9b2eb566386","body_hash":"2064bcfa4c63e1bef3639078cdffa0dd9e5a0c03aa422f697b8184a7a215413a","compiler_version":"v0.88.7","strict":true,"agent_id":"copilot","agent_model":"${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'gpt-5.6-sol' }}","engine_versions":{"copilot":"1.0.80"}}
|
||||
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"66081a747e97afeb0438f18707cfea0fac9ae595cba233b0c05d2ad4e58906c0","body_hash":"2064bcfa4c63e1bef3639078cdffa0dd9e5a0c03aa422f697b8184a7a215413a","compiler_version":"v0.88.7","strict":true,"agent_id":"copilot","agent_model":"${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'gpt-5.6-sol' }}","engine_versions":{"copilot":"1.0.80"}}
|
||||
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_PAT_0","COPILOT_PAT_1","COPILOT_PAT_2","COPILOT_PAT_3","COPILOT_PAT_4","COPILOT_PAT_5","COPILOT_PAT_6","COPILOT_PAT_7","COPILOT_PAT_8","COPILOT_PAT_9","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"5e508589e03a7757a7e05b26e834292f5445bfb6","version":"v0.88.7"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.14","digest":"sha256:f7df036c86575527b61f3f7df91c4412349a12b2a74988d929eafa2999230c98","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.14@sha256:f7df036c86575527b61f3f7df91c4412349a12b2a74988d929eafa2999230c98"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.14","digest":"sha256:6f95e2234dd9bd6333a8ff28ccea7ecf0204acd4a09108723844dbd2bf6268c5","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.14@sha256:6f95e2234dd9bd6333a8ff28ccea7ecf0204acd4a09108723844dbd2bf6268c5"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.14","digest":"sha256:2ce8df3abf3e9b76e9c0cf5863da41f1ab3f89b20ad14b988806ab89e7bf2cd5","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.14@sha256:2ce8df3abf3e9b76e9c0cf5863da41f1ab3f89b20ad14b988806ab89e7bf2cd5"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.18","digest":"sha256:85b940556a8faa4e1fdbef124bfd75f2c4ebd855a10b88a1c3b6f3e97f6f1a53","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.18@sha256:85b940556a8faa4e1fdbef124bfd75f2c4ebd855a10b88a1c3b6f3e97f6f1a53"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:33e1ec1d967ac1f28c2cedc24ce103dea3226840626de345d3fe579e96cf5c7d","pinned_image":"ghcr.io/github/gh-aw-node@sha256:33e1ec1d967ac1f28c2cedc24ce103dea3226840626de345d3fe579e96cf5c7d"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["actions_get","actions_list","get_commit","get_file_contents","get_job_logs","get_latest_release","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_releases","list_starred_repositories","list_tags","search_code","search_issues","search_repositories"]},{"name":"safeoutputs","tools":["missing_data","missing_tool","noop","publish_groomed_dashboard"]}]}
|
||||
# This file was automatically generated by gh-aw (v0.88.7). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
|
||||
#
|
||||
@@ -1850,6 +1850,7 @@ jobs:
|
||||
/(^|\n)## 🔍 Investigation Results\n[\s\S]*?(?=\n## |\n<!-- devops-health-state:v1|$)/;
|
||||
const parseRows = value => {
|
||||
const rows = new Map();
|
||||
const correlations = new Set();
|
||||
for (const line of value.split("\n")) {
|
||||
const match = line.match(
|
||||
/^\| `([^`]+)` \| ([^|]*) \| ([^|]*) \| (⏳ Pending|🔄 Dispatched|✅ Done) \| ([^|]*) \| (.*) \|$/
|
||||
@@ -1860,12 +1861,25 @@ jobs:
|
||||
if (rows.has(match[1])) {
|
||||
throw new Error(`Duplicate Investigation Results row for ${match[1]}`);
|
||||
}
|
||||
const correlationMatches = [
|
||||
...match[6].matchAll(
|
||||
/<!-- correlation:(hc-[1-9][0-9]*-[1-9][0-9]*) -->/g
|
||||
),
|
||||
];
|
||||
if (
|
||||
correlationMatches.length !== 1 ||
|
||||
correlations.has(correlationMatches[0][1])
|
||||
) {
|
||||
throw new Error(`Invalid row correlation for ${match[1]}`);
|
||||
}
|
||||
correlations.add(correlationMatches[0][1]);
|
||||
rows.set(match[1], {
|
||||
title: match[2].trim(),
|
||||
severity: match[3].trim(),
|
||||
status: match[4],
|
||||
first_seen: match[5].trim(),
|
||||
result: match[6],
|
||||
correlation: match[6].match(
|
||||
/<!-- correlation:(hc-[1-9][0-9]*-[1-9][0-9]*) -->/
|
||||
)?.[1],
|
||||
correlation: correlationMatches[0][1],
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
@@ -1875,32 +1889,95 @@ jobs:
|
||||
/<!-- devops-health-state:v1\s*\n([\s\S]*?)\n-->/g
|
||||
),
|
||||
];
|
||||
let activeIds = null;
|
||||
if (stateMatches.length > 1) {
|
||||
throw new Error("Dashboard state marker is duplicated");
|
||||
if (stateMatches.length !== 1) {
|
||||
throw new Error("Dashboard state marker is missing or duplicated");
|
||||
}
|
||||
if (stateMatches.length === 1) {
|
||||
let state;
|
||||
try {
|
||||
state = JSON.parse(stateMatches[0][1]);
|
||||
} catch (error) {
|
||||
throw new Error(`Dashboard state JSON is invalid: ${error.message}`);
|
||||
}
|
||||
if (!Array.isArray(state.active_findings)) {
|
||||
throw new Error("Dashboard active findings are invalid");
|
||||
}
|
||||
activeIds = new Set(
|
||||
state.active_findings.map(finding => finding?.fingerprint)
|
||||
);
|
||||
if (activeIds.has(undefined) || activeIds.size !== state.active_findings.length) {
|
||||
throw new Error("Dashboard active finding IDs are invalid");
|
||||
let state;
|
||||
try {
|
||||
state = JSON.parse(stateMatches[0][1]);
|
||||
} catch (error) {
|
||||
throw new Error(`Dashboard state JSON is invalid: ${error.message}`);
|
||||
}
|
||||
if (!Array.isArray(state.active_findings)) {
|
||||
throw new Error("Dashboard active findings are invalid");
|
||||
}
|
||||
const stateFindings = new Map();
|
||||
for (const finding of state.active_findings) {
|
||||
if (
|
||||
!finding ||
|
||||
typeof finding.fingerprint !== "string" ||
|
||||
typeof finding.title !== "string" ||
|
||||
!["critical", "warning", "info"].includes(finding.severity) ||
|
||||
typeof finding.first_seen !== "string" ||
|
||||
stateFindings.has(finding.fingerprint)
|
||||
) {
|
||||
throw new Error("Dashboard active finding is invalid");
|
||||
}
|
||||
stateFindings.set(finding.fingerprint, finding);
|
||||
}
|
||||
const newRows = parseRows(section);
|
||||
const severityLabels = {
|
||||
critical: "🔴 Critical",
|
||||
warning: "🟡 Warning",
|
||||
info: "🔵 Info",
|
||||
};
|
||||
const doneRows = [];
|
||||
for (const [findingId, row] of newRows) {
|
||||
const finding = stateFindings.get(findingId);
|
||||
if (
|
||||
!finding ||
|
||||
row.title !== finding.title ||
|
||||
row.severity !== severityLabels[finding.severity] ||
|
||||
row.first_seen !== finding.first_seen
|
||||
) {
|
||||
throw new Error(
|
||||
`Investigation Results row does not match active state for ${findingId}`
|
||||
);
|
||||
}
|
||||
if (row.status === "✅ Done") {
|
||||
const doneResult = row.result.match(
|
||||
new RegExp(
|
||||
"^\\[[^\\]\\r\\n|]{1,512}\\]\\(" +
|
||||
`https://github\\.com/${context.repo.owner}/${context.repo.repo}` +
|
||||
"/issues/695#issuecomment-([1-9][0-9]*)\\) " +
|
||||
`<!-- correlation:${row.correlation} -->$`
|
||||
)
|
||||
);
|
||||
if (!doneResult) {
|
||||
throw new Error(`Done row result is invalid for ${findingId}`);
|
||||
}
|
||||
doneRows.push({
|
||||
finding_id: findingId,
|
||||
correlation_id: row.correlation,
|
||||
comment_id: Number(doneResult[1]),
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const doneRow of doneRows) {
|
||||
const { data: comment } = await github.rest.issues.getComment({
|
||||
...context.repo,
|
||||
comment_id: doneRow.comment_id,
|
||||
});
|
||||
if (
|
||||
comment.user?.login !== "github-actions[bot]" ||
|
||||
comment.issue_url !==
|
||||
`https://api.github.com/repos/${context.repo.owner}/${context.repo.repo}/issues/695` ||
|
||||
!comment.body?.includes(
|
||||
`**Finding ID:** \`${doneRow.finding_id}\``
|
||||
) ||
|
||||
!comment.body?.includes(
|
||||
`**Correlation:** ${doneRow.correlation_id}`
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`Done row comment verification failed for ${doneRow.finding_id}`
|
||||
);
|
||||
}
|
||||
}
|
||||
const priorIsland = (issue.body || "").match(islandPattern)?.[0] || "";
|
||||
const priorRows = parseRows(priorIsland);
|
||||
for (const [findingId, priorRow] of priorRows) {
|
||||
const mustPreserve = activeIds === null || activeIds.has(findingId);
|
||||
const mustPreserve = stateFindings.has(findingId);
|
||||
if (!mustPreserve) {
|
||||
continue;
|
||||
}
|
||||
@@ -1924,12 +2001,6 @@ jobs:
|
||||
);
|
||||
}
|
||||
}
|
||||
if (
|
||||
activeIds !== null &&
|
||||
[...newRows.keys()].some(findingId => !activeIds.has(findingId))
|
||||
) {
|
||||
throw new Error("Investigation Results contains a non-active finding");
|
||||
}
|
||||
let nextBody;
|
||||
if (islandPattern.test(issue.body || "")) {
|
||||
nextBody = (issue.body || "").replace(
|
||||
|
||||
@@ -141,6 +141,7 @@ safe-outputs:
|
||||
/(^|\n)## 🔍 Investigation Results\n[\s\S]*?(?=\n## |\n<!-- devops-health-state:v1|$)/;
|
||||
const parseRows = value => {
|
||||
const rows = new Map();
|
||||
const correlations = new Set();
|
||||
for (const line of value.split("\n")) {
|
||||
const match = line.match(
|
||||
/^\| `([^`]+)` \| ([^|]*) \| ([^|]*) \| (⏳ Pending|🔄 Dispatched|✅ Done) \| ([^|]*) \| (.*) \|$/
|
||||
@@ -151,12 +152,25 @@ safe-outputs:
|
||||
if (rows.has(match[1])) {
|
||||
throw new Error(`Duplicate Investigation Results row for ${match[1]}`);
|
||||
}
|
||||
const correlationMatches = [
|
||||
...match[6].matchAll(
|
||||
/<!-- correlation:(hc-[1-9][0-9]*-[1-9][0-9]*) -->/g
|
||||
),
|
||||
];
|
||||
if (
|
||||
correlationMatches.length !== 1 ||
|
||||
correlations.has(correlationMatches[0][1])
|
||||
) {
|
||||
throw new Error(`Invalid row correlation for ${match[1]}`);
|
||||
}
|
||||
correlations.add(correlationMatches[0][1]);
|
||||
rows.set(match[1], {
|
||||
title: match[2].trim(),
|
||||
severity: match[3].trim(),
|
||||
status: match[4],
|
||||
first_seen: match[5].trim(),
|
||||
result: match[6],
|
||||
correlation: match[6].match(
|
||||
/<!-- correlation:(hc-[1-9][0-9]*-[1-9][0-9]*) -->/
|
||||
)?.[1],
|
||||
correlation: correlationMatches[0][1],
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
@@ -166,32 +180,95 @@ safe-outputs:
|
||||
/<!-- devops-health-state:v1\s*\n([\s\S]*?)\n-->/g
|
||||
),
|
||||
];
|
||||
let activeIds = null;
|
||||
if (stateMatches.length > 1) {
|
||||
throw new Error("Dashboard state marker is duplicated");
|
||||
if (stateMatches.length !== 1) {
|
||||
throw new Error("Dashboard state marker is missing or duplicated");
|
||||
}
|
||||
if (stateMatches.length === 1) {
|
||||
let state;
|
||||
try {
|
||||
state = JSON.parse(stateMatches[0][1]);
|
||||
} catch (error) {
|
||||
throw new Error(`Dashboard state JSON is invalid: ${error.message}`);
|
||||
}
|
||||
if (!Array.isArray(state.active_findings)) {
|
||||
throw new Error("Dashboard active findings are invalid");
|
||||
}
|
||||
activeIds = new Set(
|
||||
state.active_findings.map(finding => finding?.fingerprint)
|
||||
);
|
||||
if (activeIds.has(undefined) || activeIds.size !== state.active_findings.length) {
|
||||
throw new Error("Dashboard active finding IDs are invalid");
|
||||
let state;
|
||||
try {
|
||||
state = JSON.parse(stateMatches[0][1]);
|
||||
} catch (error) {
|
||||
throw new Error(`Dashboard state JSON is invalid: ${error.message}`);
|
||||
}
|
||||
if (!Array.isArray(state.active_findings)) {
|
||||
throw new Error("Dashboard active findings are invalid");
|
||||
}
|
||||
const stateFindings = new Map();
|
||||
for (const finding of state.active_findings) {
|
||||
if (
|
||||
!finding ||
|
||||
typeof finding.fingerprint !== "string" ||
|
||||
typeof finding.title !== "string" ||
|
||||
!["critical", "warning", "info"].includes(finding.severity) ||
|
||||
typeof finding.first_seen !== "string" ||
|
||||
stateFindings.has(finding.fingerprint)
|
||||
) {
|
||||
throw new Error("Dashboard active finding is invalid");
|
||||
}
|
||||
stateFindings.set(finding.fingerprint, finding);
|
||||
}
|
||||
const newRows = parseRows(section);
|
||||
const severityLabels = {
|
||||
critical: "🔴 Critical",
|
||||
warning: "🟡 Warning",
|
||||
info: "🔵 Info",
|
||||
};
|
||||
const doneRows = [];
|
||||
for (const [findingId, row] of newRows) {
|
||||
const finding = stateFindings.get(findingId);
|
||||
if (
|
||||
!finding ||
|
||||
row.title !== finding.title ||
|
||||
row.severity !== severityLabels[finding.severity] ||
|
||||
row.first_seen !== finding.first_seen
|
||||
) {
|
||||
throw new Error(
|
||||
`Investigation Results row does not match active state for ${findingId}`
|
||||
);
|
||||
}
|
||||
if (row.status === "✅ Done") {
|
||||
const doneResult = row.result.match(
|
||||
new RegExp(
|
||||
"^\\[[^\\]\\r\\n|]{1,512}\\]\\(" +
|
||||
`https://github\\.com/${context.repo.owner}/${context.repo.repo}` +
|
||||
"/issues/695#issuecomment-([1-9][0-9]*)\\) " +
|
||||
`<!-- correlation:${row.correlation} -->$`
|
||||
)
|
||||
);
|
||||
if (!doneResult) {
|
||||
throw new Error(`Done row result is invalid for ${findingId}`);
|
||||
}
|
||||
doneRows.push({
|
||||
finding_id: findingId,
|
||||
correlation_id: row.correlation,
|
||||
comment_id: Number(doneResult[1]),
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const doneRow of doneRows) {
|
||||
const { data: comment } = await github.rest.issues.getComment({
|
||||
...context.repo,
|
||||
comment_id: doneRow.comment_id,
|
||||
});
|
||||
if (
|
||||
comment.user?.login !== "github-actions[bot]" ||
|
||||
comment.issue_url !==
|
||||
`https://api.github.com/repos/${context.repo.owner}/${context.repo.repo}/issues/695` ||
|
||||
!comment.body?.includes(
|
||||
`**Finding ID:** \`${doneRow.finding_id}\``
|
||||
) ||
|
||||
!comment.body?.includes(
|
||||
`**Correlation:** ${doneRow.correlation_id}`
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`Done row comment verification failed for ${doneRow.finding_id}`
|
||||
);
|
||||
}
|
||||
}
|
||||
const priorIsland = (issue.body || "").match(islandPattern)?.[0] || "";
|
||||
const priorRows = parseRows(priorIsland);
|
||||
for (const [findingId, priorRow] of priorRows) {
|
||||
const mustPreserve = activeIds === null || activeIds.has(findingId);
|
||||
const mustPreserve = stateFindings.has(findingId);
|
||||
if (!mustPreserve) {
|
||||
continue;
|
||||
}
|
||||
@@ -215,12 +292,6 @@ safe-outputs:
|
||||
);
|
||||
}
|
||||
}
|
||||
if (
|
||||
activeIds !== null &&
|
||||
[...newRows.keys()].some(findingId => !activeIds.has(findingId))
|
||||
) {
|
||||
throw new Error("Investigation Results contains a non-active finding");
|
||||
}
|
||||
let nextBody;
|
||||
if (islandPattern.test(issue.body || "")) {
|
||||
nextBody = (issue.body || "").replace(
|
||||
|
||||
+71
-4
@@ -1,4 +1,4 @@
|
||||
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"40db5b11956b0097c6b6b8289af92a03afe31e4a74338166f6d09ddd9e2be4e3","body_hash":"421e044a12c72b5e7a1777b99685be999509819aa808b413719e85a28b2a16ea","compiler_version":"v0.88.7","strict":true,"agent_id":"copilot","agent_model":"${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'gpt-5.6-sol' }}","engine_versions":{"copilot":"1.0.80"}}
|
||||
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"981b313ae20b412ea49ecca4304000a7a0ac1793c4ddaa982927324721983d6d","body_hash":"9c6b1f5a55f7328496bfea9d9e1068450c69087ee06c00ee468aed247f87837a","compiler_version":"v0.88.7","strict":true,"agent_id":"copilot","agent_model":"${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'gpt-5.6-sol' }}","engine_versions":{"copilot":"1.0.80"}}
|
||||
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_PAT_0","COPILOT_PAT_1","COPILOT_PAT_2","COPILOT_PAT_3","COPILOT_PAT_4","COPILOT_PAT_5","COPILOT_PAT_6","COPILOT_PAT_7","COPILOT_PAT_8","COPILOT_PAT_9","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"5e508589e03a7757a7e05b26e834292f5445bfb6","version":"v0.88.7"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.14","digest":"sha256:f7df036c86575527b61f3f7df91c4412349a12b2a74988d929eafa2999230c98","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.14@sha256:f7df036c86575527b61f3f7df91c4412349a12b2a74988d929eafa2999230c98"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.14","digest":"sha256:6f95e2234dd9bd6333a8ff28ccea7ecf0204acd4a09108723844dbd2bf6268c5","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.14@sha256:6f95e2234dd9bd6333a8ff28ccea7ecf0204acd4a09108723844dbd2bf6268c5"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.14","digest":"sha256:2ce8df3abf3e9b76e9c0cf5863da41f1ab3f89b20ad14b988806ab89e7bf2cd5","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.14@sha256:2ce8df3abf3e9b76e9c0cf5863da41f1ab3f89b20ad14b988806ab89e7bf2cd5"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.18","digest":"sha256:85b940556a8faa4e1fdbef124bfd75f2c4ebd855a10b88a1c3b6f3e97f6f1a53","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.18@sha256:85b940556a8faa4e1fdbef124bfd75f2c4ebd855a10b88a1c3b6f3e97f6f1a53"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:33e1ec1d967ac1f28c2cedc24ce103dea3226840626de345d3fe579e96cf5c7d","pinned_image":"ghcr.io/github/gh-aw-node@sha256:33e1ec1d967ac1f28c2cedc24ce103dea3226840626de345d3fe579e96cf5c7d"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["actions_get","actions_list","get_commit","get_file_contents","get_job_logs","get_latest_release","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["missing_data","missing_tool","noop","publish_investigation_report"]}]}
|
||||
# This file was automatically generated by gh-aw (v0.88.7). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
|
||||
#
|
||||
@@ -1807,6 +1807,7 @@ jobs:
|
||||
env:
|
||||
EXPECTED_CORRELATION_ID: ${{ inputs.correlation_id }}
|
||||
EXPECTED_FINDING_ID: ${{ inputs.finding_id }}
|
||||
EXPECTED_SEVERITY: ${{ inputs.finding_severity }}
|
||||
GH_AW_AGENT_OUTPUT: ${{ runner.temp }}/gh-aw/safe-jobs/agent_output.json
|
||||
with:
|
||||
script: |
|
||||
@@ -1832,12 +1833,14 @@ jobs:
|
||||
const reportBody = items[0].report_body;
|
||||
const findingId = process.env.EXPECTED_FINDING_ID;
|
||||
const correlationId = process.env.EXPECTED_CORRELATION_ID;
|
||||
const expectedSeverity = process.env.EXPECTED_SEVERITY;
|
||||
if (
|
||||
typeof reportBody !== "string" ||
|
||||
reportBody.length === 0 ||
|
||||
reportBody.length > 65000 ||
|
||||
typeof findingId !== "string" ||
|
||||
typeof correlationId !== "string"
|
||||
typeof correlationId !== "string" ||
|
||||
typeof expectedSeverity !== "string"
|
||||
) {
|
||||
throw new Error("Investigation report inputs are invalid");
|
||||
}
|
||||
@@ -1861,6 +1864,46 @@ jobs:
|
||||
) {
|
||||
throw new Error("Investigation report identity does not match workflow inputs");
|
||||
}
|
||||
const requiredHeadings = [
|
||||
"### Root Cause",
|
||||
"### Blast Radius",
|
||||
"### Suggested Fix",
|
||||
"### Remediation Status",
|
||||
"### Evidence",
|
||||
"### Related",
|
||||
];
|
||||
if (
|
||||
!reportBody.match(
|
||||
new RegExp(
|
||||
`^\\*\\*Severity:\\*\\* ${expectedSeverity}\\s*$`,
|
||||
"m"
|
||||
)
|
||||
) ||
|
||||
!reportBody.match(
|
||||
/^\*\*Executive Summary:\*\* [^\r\n]{1,512}$/m
|
||||
) ||
|
||||
!reportBody.match(
|
||||
/^\*\*Confidence:\*\* (?:High|Medium|Low) — [^\r\n]+$/m
|
||||
) ||
|
||||
!reportBody.match(/^\*\*Validation:\*\* [^\r\n]+$/m) ||
|
||||
!reportBody.match(/^\*\*Owner:\*\* [^\r\n]+$/m) ||
|
||||
!reportBody.match(/^### Suggested Fix\s*\n1\. \S/m) ||
|
||||
!reportBody.match(/^### Remediation Status\s*\nReport-only\. \S/m) ||
|
||||
requiredHeadings.some(
|
||||
heading =>
|
||||
(reportBody.match(
|
||||
new RegExp(
|
||||
`^${heading.replace(
|
||||
/[.*+?^${}()|[\]\\]/g,
|
||||
"\\$&"
|
||||
)}\\s*$`,
|
||||
"gm"
|
||||
)
|
||||
) || []).length !== 1
|
||||
)
|
||||
) {
|
||||
throw new Error("Investigation report template is incomplete");
|
||||
}
|
||||
const correlation = correlationId.match(
|
||||
/^hc-([1-9][0-9]*)-([1-9][0-9]*)$/
|
||||
);
|
||||
@@ -1883,11 +1926,35 @@ jobs:
|
||||
throw new Error("Correlation does not reference a valid health-check run");
|
||||
}
|
||||
|
||||
for (const match of reportBody.matchAll(/https?:\/\/[^\s)<>"']+/g)) {
|
||||
const link = new URL(match[0].replace(/[.,;:!?]+$/, ""));
|
||||
const validateLinkDestination = destination => {
|
||||
if (destination.startsWith("#")) {
|
||||
return;
|
||||
}
|
||||
if (destination.startsWith("//")) {
|
||||
throw new Error(`Protocol-relative links are not allowed: ${destination}`);
|
||||
}
|
||||
const link = new URL(destination);
|
||||
if (link.protocol !== "https:" || link.hostname !== "github.com") {
|
||||
throw new Error(`Only github.com links are allowed: ${link.href}`);
|
||||
}
|
||||
};
|
||||
for (const match of reportBody.matchAll(/https?:\/\/[^\s)<>"']+/g)) {
|
||||
validateLinkDestination(
|
||||
match[0].replace(/[.,;:!?]+$/, "")
|
||||
);
|
||||
}
|
||||
if (/(^|[^:])\/\/[A-Za-z0-9]/m.test(reportBody)) {
|
||||
throw new Error("Protocol-relative links are not allowed");
|
||||
}
|
||||
for (const match of reportBody.matchAll(
|
||||
/!?\[[^\]\r\n]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g
|
||||
)) {
|
||||
validateLinkDestination(match[1]);
|
||||
}
|
||||
for (const match of reportBody.matchAll(
|
||||
/(?:href|src)\s*=\s*["']([^"']+)["']/gi
|
||||
)) {
|
||||
validateLinkDestination(match[1]);
|
||||
}
|
||||
const prose = reportBody
|
||||
.replace(/```[\s\S]*?```/g, "")
|
||||
|
||||
@@ -79,6 +79,7 @@ safe-outputs:
|
||||
env:
|
||||
EXPECTED_FINDING_ID: ${{ inputs.finding_id }}
|
||||
EXPECTED_CORRELATION_ID: ${{ inputs.correlation_id }}
|
||||
EXPECTED_SEVERITY: ${{ inputs.finding_severity }}
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
@@ -110,12 +111,14 @@ safe-outputs:
|
||||
const reportBody = items[0].report_body;
|
||||
const findingId = process.env.EXPECTED_FINDING_ID;
|
||||
const correlationId = process.env.EXPECTED_CORRELATION_ID;
|
||||
const expectedSeverity = process.env.EXPECTED_SEVERITY;
|
||||
if (
|
||||
typeof reportBody !== "string" ||
|
||||
reportBody.length === 0 ||
|
||||
reportBody.length > 65000 ||
|
||||
typeof findingId !== "string" ||
|
||||
typeof correlationId !== "string"
|
||||
typeof correlationId !== "string" ||
|
||||
typeof expectedSeverity !== "string"
|
||||
) {
|
||||
throw new Error("Investigation report inputs are invalid");
|
||||
}
|
||||
@@ -139,6 +142,46 @@ safe-outputs:
|
||||
) {
|
||||
throw new Error("Investigation report identity does not match workflow inputs");
|
||||
}
|
||||
const requiredHeadings = [
|
||||
"### Root Cause",
|
||||
"### Blast Radius",
|
||||
"### Suggested Fix",
|
||||
"### Remediation Status",
|
||||
"### Evidence",
|
||||
"### Related",
|
||||
];
|
||||
if (
|
||||
!reportBody.match(
|
||||
new RegExp(
|
||||
`^\\*\\*Severity:\\*\\* ${expectedSeverity}\\s*$`,
|
||||
"m"
|
||||
)
|
||||
) ||
|
||||
!reportBody.match(
|
||||
/^\*\*Executive Summary:\*\* [^\r\n]{1,512}$/m
|
||||
) ||
|
||||
!reportBody.match(
|
||||
/^\*\*Confidence:\*\* (?:High|Medium|Low) — [^\r\n]+$/m
|
||||
) ||
|
||||
!reportBody.match(/^\*\*Validation:\*\* [^\r\n]+$/m) ||
|
||||
!reportBody.match(/^\*\*Owner:\*\* [^\r\n]+$/m) ||
|
||||
!reportBody.match(/^### Suggested Fix\s*\n1\. \S/m) ||
|
||||
!reportBody.match(/^### Remediation Status\s*\nReport-only\. \S/m) ||
|
||||
requiredHeadings.some(
|
||||
heading =>
|
||||
(reportBody.match(
|
||||
new RegExp(
|
||||
`^${heading.replace(
|
||||
/[.*+?^${}()|[\]\\]/g,
|
||||
"\\$&"
|
||||
)}\\s*$`,
|
||||
"gm"
|
||||
)
|
||||
) || []).length !== 1
|
||||
)
|
||||
) {
|
||||
throw new Error("Investigation report template is incomplete");
|
||||
}
|
||||
const correlation = correlationId.match(
|
||||
/^hc-([1-9][0-9]*)-([1-9][0-9]*)$/
|
||||
);
|
||||
@@ -161,11 +204,35 @@ safe-outputs:
|
||||
throw new Error("Correlation does not reference a valid health-check run");
|
||||
}
|
||||
|
||||
for (const match of reportBody.matchAll(/https?:\/\/[^\s)<>"']+/g)) {
|
||||
const link = new URL(match[0].replace(/[.,;:!?]+$/, ""));
|
||||
const validateLinkDestination = destination => {
|
||||
if (destination.startsWith("#")) {
|
||||
return;
|
||||
}
|
||||
if (destination.startsWith("//")) {
|
||||
throw new Error(`Protocol-relative links are not allowed: ${destination}`);
|
||||
}
|
||||
const link = new URL(destination);
|
||||
if (link.protocol !== "https:" || link.hostname !== "github.com") {
|
||||
throw new Error(`Only github.com links are allowed: ${link.href}`);
|
||||
}
|
||||
};
|
||||
for (const match of reportBody.matchAll(/https?:\/\/[^\s)<>"']+/g)) {
|
||||
validateLinkDestination(
|
||||
match[0].replace(/[.,;:!?]+$/, "")
|
||||
);
|
||||
}
|
||||
if (/(^|[^:])\/\/[A-Za-z0-9]/m.test(reportBody)) {
|
||||
throw new Error("Protocol-relative links are not allowed");
|
||||
}
|
||||
for (const match of reportBody.matchAll(
|
||||
/!?\[[^\]\r\n]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g
|
||||
)) {
|
||||
validateLinkDestination(match[1]);
|
||||
}
|
||||
for (const match of reportBody.matchAll(
|
||||
/(?:href|src)\s*=\s*["']([^"']+)["']/gi
|
||||
)) {
|
||||
validateLinkDestination(match[1]);
|
||||
}
|
||||
const prose = reportBody
|
||||
.replace(/```[\s\S]*?```/g, "")
|
||||
@@ -458,6 +525,9 @@ publish-investigation-report:
|
||||
Report-only. {Trusted evidence, proposed change, validation plan, and owner,
|
||||
or why the available evidence cannot verify an exact fix.}
|
||||
|
||||
**Validation:** {targeted validation for a maintainer}
|
||||
**Owner:** {suggested owner}
|
||||
|
||||
### Evidence
|
||||
{key log excerpts, API responses, or code references}
|
||||
|
||||
|
||||
@@ -212,6 +212,7 @@ def run_investigation_publisher(
|
||||
test_case: unittest.TestCase,
|
||||
*,
|
||||
actor: str = "github-actions[bot]",
|
||||
report_body: str | None = None,
|
||||
) -> dict[str, object]:
|
||||
node = shutil.which("node")
|
||||
if not node:
|
||||
@@ -231,13 +232,29 @@ def run_investigation_publisher(
|
||||
{json.dumps({"active_findings": [{"fingerprint": finding_id, "category": "pipeline"}], "history": []}, separators=(",", ":"))}
|
||||
-->
|
||||
"""
|
||||
report_body = (
|
||||
"## 🔍 Investigation: Evaluation tests failed\n\n"
|
||||
f"**Finding ID:** `{finding_id}`\n"
|
||||
"**Severity:** critical\n"
|
||||
f"**Correlation:** {correlation_id}\n"
|
||||
"**Executive Summary:** Tests failed."
|
||||
)
|
||||
if report_body is None:
|
||||
report_body = (
|
||||
"## 🔍 Investigation: Evaluation tests failed\n\n"
|
||||
f"**Finding ID:** `{finding_id}`\n"
|
||||
"**Severity:** critical\n"
|
||||
f"**Correlation:** {correlation_id}\n"
|
||||
"**Executive Summary:** Tests failed.\n\n"
|
||||
"### Root Cause\n"
|
||||
"A deterministic test failure was confirmed.\n\n"
|
||||
"**Confidence:** High — the failing assertion identifies the cause.\n\n"
|
||||
"### Blast Radius\n"
|
||||
"The evaluation workflow is affected.\n\n"
|
||||
"### Suggested Fix\n"
|
||||
"1. Correct the failing test setup.\n\n"
|
||||
"### Remediation Status\n"
|
||||
"Report-only. A maintainer should apply the proposed change.\n\n"
|
||||
"**Validation:** Run the targeted evaluation test.\n"
|
||||
"**Owner:** Evaluation maintainers\n\n"
|
||||
"### Evidence\n"
|
||||
"The failed workflow run and repository files agree.\n\n"
|
||||
"### Related\n"
|
||||
"None found."
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
output_path = temp_path / "agent-output.json"
|
||||
@@ -314,6 +331,7 @@ const context = {{
|
||||
"GH_AW_AGENT_OUTPUT": str(output_path),
|
||||
"EXPECTED_FINDING_ID": finding_id,
|
||||
"EXPECTED_CORRELATION_ID": correlation_id,
|
||||
"EXPECTED_SEVERITY": "critical",
|
||||
}
|
||||
)
|
||||
completed = subprocess.run(
|
||||
@@ -786,6 +804,14 @@ class TokenFailoverTests(unittest.TestCase):
|
||||
"Active Investigation Results row was not preserved",
|
||||
groom_script,
|
||||
)
|
||||
self.assertIn(
|
||||
"Done row comment verification failed",
|
||||
groom_script,
|
||||
)
|
||||
self.assertIn(
|
||||
"Investigation Results row does not match active state",
|
||||
groom_script,
|
||||
)
|
||||
groom_manifest = json.loads(
|
||||
groom_lock_text.splitlines()[1].removeprefix("# gh-aw-manifest: ")
|
||||
)
|
||||
@@ -996,7 +1022,7 @@ class TokenFailoverTests(unittest.TestCase):
|
||||
{section}
|
||||
|
||||
<!-- devops-health-state:v1
|
||||
{json.dumps({"active_findings": [{"fingerprint": finding_id}], "history": []}, separators=(",", ":"))}
|
||||
{json.dumps({"active_findings": [{"fingerprint": finding_id, "title": "Evaluation tests failed", "severity": "critical", "first_seen": "2026-09-16"}], "history": []}, separators=(",", ":"))}
|
||||
-->
|
||||
"""
|
||||
empty_section = """## 🔍 Investigation Results
|
||||
@@ -1913,6 +1939,47 @@ class TokenFailoverTests(unittest.TestCase):
|
||||
self.assertIn("github-actions[bot] provenance", manual["error"])
|
||||
self.assertEqual(manual["calls"], [])
|
||||
|
||||
incomplete = run_investigation_publisher(
|
||||
self,
|
||||
report_body=(
|
||||
"## 🔍 Investigation: Evaluation tests failed\n\n"
|
||||
"**Finding ID:** `pipeline:evaluation:evaluate:test:failure`\n"
|
||||
"**Severity:** critical\n"
|
||||
"**Correlation:** hc-123-1\n"
|
||||
"**Executive Summary:** Tests failed."
|
||||
),
|
||||
)
|
||||
self.assertFalse(incomplete["ok"])
|
||||
self.assertIn("Investigation report template is incomplete", incomplete["error"])
|
||||
self.assertEqual(incomplete["calls"], [])
|
||||
|
||||
unsafe_report = (
|
||||
"## 🔍 Investigation: Evaluation tests failed\n\n"
|
||||
"**Finding ID:** `pipeline:evaluation:evaluate:test:failure`\n"
|
||||
"**Severity:** critical\n"
|
||||
"**Correlation:** hc-123-1\n"
|
||||
"**Executive Summary:** Tests failed.\n\n"
|
||||
"### Root Cause\nA deterministic failure was confirmed.\n\n"
|
||||
"**Confidence:** High — the assertion identifies the cause.\n\n"
|
||||
"### Blast Radius\nThe evaluation workflow is affected.\n\n"
|
||||
"### Suggested Fix\n1. Correct the test setup.\n\n"
|
||||
"### Remediation Status\nReport-only. A maintainer should fix it.\n\n"
|
||||
"**Validation:** Run the targeted test.\n"
|
||||
"**Owner:** Evaluation maintainers\n\n"
|
||||
"### Evidence\nThe workflow output confirms the failure.\n\n"
|
||||
"### Related\n[details](//attacker.example/path)"
|
||||
)
|
||||
unsafe = run_investigation_publisher(
|
||||
self,
|
||||
report_body=unsafe_report,
|
||||
)
|
||||
self.assertFalse(unsafe["ok"])
|
||||
self.assertIn("Protocol-relative links are not allowed", unsafe["error"])
|
||||
self.assertEqual(
|
||||
[call["type"] for call in unsafe["calls"]],
|
||||
["get-run"],
|
||||
)
|
||||
|
||||
def test_devops_health_investigator_has_no_mutating_tools(self) -> None:
|
||||
workflows = REPO_ROOT / ".github" / "workflows"
|
||||
investigate_source = workflows / "devops-health-investigate.md"
|
||||
|
||||
Reference in New Issue
Block a user