diff --git a/.github/workflows/avenger.lock.yml b/.github/workflows/avenger.lock.yml index 092aada6f01..3d6eb6ec67f 100644 --- a/.github/workflows/avenger.lock.yml +++ b/.github/workflows/avenger.lock.yml @@ -1,5 +1,5 @@ # gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"6807590fb97b42c0858f3b0e068f5f2b823e6d3a5a873b1a1652f5658361eb44","body_hash":"e5fd04fe008ba327d939d9aee395ffb802836e30fd1f3772ca36984bdd1478f4","strict":true,"agent_id":"claude","agent_model":"claude-haiku-4.5","engine_versions":{"claude":"2.1.210"}} -# gh-aw-manifest: {"version":1,"secrets":["ANTHROPIC_API_KEY","COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","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":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"b7ad1dad31e06c5925ef5d2fc7ad053ef454303e","version":"v7.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.35","digest":"sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.35@sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} +# gh-aw-manifest: {"version":1,"secrets":["ANTHROPIC_API_KEY","COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","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":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"b7ad1dad31e06c5925ef5d2fc7ad053ef454303e","version":"v7.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.35","digest":"sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.35@sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ @@ -44,7 +44,6 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1146,7 +1145,7 @@ jobs: GH_HOST="${GH_HOST#http://}" echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Check last CI workflow run status on main branch diff --git a/.github/workflows/hourly-ci-cleaner.lock.yml b/.github/workflows/hourly-ci-cleaner.lock.yml index 7fbc3953765..4c96d7c4a51 100644 --- a/.github/workflows/hourly-ci-cleaner.lock.yml +++ b/.github/workflows/hourly-ci-cleaner.lock.yml @@ -1,5 +1,5 @@ # gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"8b86cc12904088cf1b7630e0faa28299f64081df184a028b6293cb4f8bfdd07d","body_hash":"8eb6f8fd1e8e3d63cfd268226d09333129d49634435ae9a3c88debb099521ed5","strict":true,"agent_id":"claude","engine_versions":{"claude":"2.1.210"}} -# gh-aw-manifest: {"version":1,"secrets":["ANTHROPIC_API_KEY","COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","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":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"b7ad1dad31e06c5925ef5d2fc7ad053ef454303e","version":"v7.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.35","digest":"sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.35@sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} +# gh-aw-manifest: {"version":1,"secrets":["ANTHROPIC_API_KEY","COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","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":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"b7ad1dad31e06c5925ef5d2fc7ad053ef454303e","version":"v7.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.35","digest":"sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.35@sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ @@ -45,7 +45,6 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1153,7 +1152,7 @@ jobs: GH_HOST="${GH_HOST#http://}" echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Check last CI workflow run status on main branch diff --git a/.github/workflows/release.lock.yml b/.github/workflows/release.lock.yml index 279d407502e..b670dfbb32e 100644 --- a/.github/workflows/release.lock.yml +++ b/.github/workflows/release.lock.yml @@ -1,5 +1,5 @@ # gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"2d6e82aec6fea9061e1ef1cbb085324e6ed6c079230d70bbbb96611bb18cf7cf","body_hash":"646353d7bb4e5523bc85349c2cce38188190095a303f83cc95961ff145a47043","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.70"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","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":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"b7ad1dad31e06c5925ef5d2fc7ad053ef454303e","version":"b7ad1dad31e06c5925ef5d2fc7ad053ef454303e"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"anchore/sbom-action","sha":"e22c389904149dbc22b58101806040fa8d37a610","version":"v0.24.0"},{"repo":"docker/build-push-action","sha":"53b7df96c91f9c12dcc8a07bcb9ccacbed38856a","version":"v7.3.0"},{"repo":"docker/login-action","sha":"af1e73f918a031802d376d3c8bbc3fe56130a9b0","version":"v4.4.0"},{"repo":"docker/metadata-action","sha":"dc802804100637a589fabce1cb79ff13a1411302","version":"v6.2.0"},{"repo":"docker/setup-buildx-action","sha":"bb05f3f5519dd87d3ba754cc423b652a5edd6d2c","version":"v4.2.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","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":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"b7ad1dad31e06c5925ef5d2fc7ad053ef454303e","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"anchore/sbom-action","sha":"e22c389904149dbc22b58101806040fa8d37a610","version":"v0.24.0"},{"repo":"docker/build-push-action","sha":"53b7df96c91f9c12dcc8a07bcb9ccacbed38856a","version":"v7.3.0"},{"repo":"docker/login-action","sha":"af1e73f918a031802d376d3c8bbc3fe56130a9b0","version":"v4.4.0"},{"repo":"docker/metadata-action","sha":"dc802804100637a589fabce1cb79ff13a1411302","version":"v6.2.0"},{"repo":"docker/setup-buildx-action","sha":"bb05f3f5519dd87d3ba754cc423b652a5edd6d2c","version":"v4.2.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ @@ -47,7 +47,7 @@ # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) -# - actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # b7ad1dad31e06c5925ef5d2fc7ad053ef454303e +# - actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 # - anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 # - docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 @@ -1949,7 +1949,7 @@ jobs: env: RELEASE_TAG: ${{ needs.config.outputs.release_tag }} - name: Setup Go - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # b7ad1dad31e06c5925ef5d2fc7ad053ef454303e + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: cache: false go-version-file: go.mod diff --git a/.github/workflows/skillet.lock.yml b/.github/workflows/skillet.lock.yml index 8d7320c75be..81a6ce1825b 100644 --- a/.github/workflows/skillet.lock.yml +++ b/.github/workflows/skillet.lock.yml @@ -1,5 +1,5 @@ # gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"2b7791e73899163b45a0881db45f5747495a6a7dbd2dc4e17f3f31af4ecf6898","body_hash":"ff6f6b1fbf4788ce998b5ddca9ed8f907259189ec1db98a5cd9768e4e0ed2f50","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.70"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","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":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"3a2844b7e9c422d3c10d287c895573f7108da1b3"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.35","digest":"sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.35@sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","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":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"3a2844b7e9c422d3c10d287c895573f7108da1b3"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.35","digest":"sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.35@sha256:fe83cd274636efa9de3f456e2b078fae137328b9bb6ee4986ae510acaef0cec5"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ @@ -45,7 +45,6 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 @@ -1728,7 +1727,7 @@ jobs: GH_AW_INFO_AWF_VERSION: "v0.27.35" GH_AW_INFO_ENGINE_ID: "copilot" - name: Checkout skills directory - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false sparse-checkout: | diff --git a/pkg/cli/logs_orchestrator.go b/pkg/cli/logs_orchestrator.go index 45af4d86e4e..27204ef8713 100644 --- a/pkg/cli/logs_orchestrator.go +++ b/pkg/cli/logs_orchestrator.go @@ -14,14 +14,9 @@ import ( "context" "errors" "fmt" - "os" - "path/filepath" - "strings" "time" - "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/envutil" - "github.com/github/gh-aw/pkg/fileutil" "github.com/github/gh-aw/pkg/logger" ) @@ -147,459 +142,42 @@ func buildContinuationIfNeeded( // DownloadWorkflowLogs downloads and analyzes workflow logs with metrics func DownloadWorkflowLogs(ctx context.Context, opts LogsDownloadOptions) error { - workflowName := opts.WorkflowName - count := opts.Count - startDate := opts.StartDate - endDate := opts.EndDate - outputDir := opts.OutputDir - engine := opts.Engine - ref := opts.Ref - beforeRunID := opts.BeforeRunID - afterRunID := opts.AfterRunID - repoOverride := opts.RepoOverride - verbose := opts.Verbose - toolGraph := opts.ToolGraph - noStaged := opts.NoStaged - firewallOnly := opts.FirewallOnly - noFirewall := opts.NoFirewall - parse := opts.Parse - jsonOutput := opts.JSONOutput - timeoutMinutes := opts.TimeoutMinutes - summaryFile := opts.SummaryFile - safeOutputType := opts.SafeOutputType - filteredIntegrity := opts.FilteredIntegrity - evalsOnly := opts.EvalsOnly - train := opts.Train - format := opts.Format - artifactSets := opts.ArtifactSets - after := opts.After - - logsOrchestratorLog.Printf("Starting workflow log download: workflow=%s, count=%d, startDate=%s, endDate=%s, outputDir=%s, summaryFile=%s, safeOutputType=%s, filteredIntegrity=%v, evalsOnly=%v, train=%v, format=%s, artifactSets=%v, after=%s", workflowName, count, startDate, endDate, outputDir, summaryFile, safeOutputType, filteredIntegrity, evalsOnly, train, format, artifactSets, after) - - // Validate and resolve artifact sets into a concrete filter (list of artifact base names). - if err := ValidateArtifactSets(artifactSets); err != nil { + runtime, err := prepareLogsDownload(ctx, opts) + if err != nil { return err } - artifactFilter := ResolveArtifactFilter(artifactSets) - if len(artifactFilter) > 0 { - logsOrchestratorLog.Printf("Artifact filter active: %v", artifactFilter) - if verbose { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Artifact filter: downloading only "+strings.Join(artifactFilter, ", "))) - } - } - - // Ensure .github/aw/logs/.gitignore exists on every invocation - if err := ensureLogsGitignore(); err != nil { - // Log but don't fail - this is not critical for downloading logs - logsOrchestratorLog.Printf("Failed to ensure logs .gitignore: %v", err) - if verbose { - fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to ensure .github/aw/logs/.gitignore: %v", err))) - } - } - - // Check context cancellation at the start - select { - case <-ctx.Done(): - fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Operation cancelled")) - return ctx.Err() - default: - } - - // Clean up cached run folders older than the --after cutoff, if specified. - // Runs after the context check so a cancelled context never triggers disk scanning. - if after != "" { - cutoff, parseErr := parseCleanupCutoff(after) - if parseErr != nil { - return parseErr - } - logsOrchestratorLog.Printf("Cleaning up run folders older than %s (cutoff: %s)", after, cutoff.Format(time.RFC3339)) - removed, cleanErr := cleanupOldRunFolders(outputDir, cutoff, verbose) - if cleanErr != nil { - // Non-fatal: log but continue with download - logsOrchestratorLog.Printf("Failed to clean up old run folders: %v", cleanErr) - if !jsonOutput { - fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to clean up old run folders: %v", cleanErr))) - } - } else if removed > 0 { - if !jsonOutput { - fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Removed %d cached run folder(s) older than %s", removed, after))) - } - } else if verbose && !jsonOutput { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("No cached run folders older than %s found", after))) - } - } - - if verbose { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Fetching workflow runs from GitHub Actions...")) - } - - // activeCtx is ctx extended with a deadline when timeoutMinutes > 0. - // Using a named variable avoids reassigning the ctx parameter and makes it - // explicit that a derived context governs all downstream downloads. - activeCtx := ctx - var startTime time.Time - var timeoutReached bool - if timeoutMinutes > 0 { - startTime = time.Now() - var timeoutCancel context.CancelFunc - activeCtx, timeoutCancel = context.WithTimeout(ctx, time.Duration(timeoutMinutes)*time.Minute) - defer timeoutCancel() - if verbose { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Timeout set to %d minutes", timeoutMinutes))) - } - } - - var processedRuns []ProcessedRun - var beforeDate string - iteration := 0 - - // Determine if we should fetch all runs in the date window or limit iteratively by count. - // In fetchAllInRange mode (when date filters are specified), count acts as a page size: - // the loop stops once count runs are collected and a continuation cursor is emitted so - // callers can page through the full date window. - // Without date filters, we fetch up to count runs with artifacts and stop (old behavior - // for backward compatibility). - fetchAllInRange := startDate != "" || endDate != "" - - // countLimitReached is set when the loop exits because len(processedRuns) >= count in - // fetchAllInRange mode. It signals that more runs may be available in the date window - // and drives continuation-data generation so callers can page through the full range. - var countLimitReached bool - - filters := runFilterOpts{ - engine: engine, - noStaged: noStaged, - firewallOnly: firewallOnly, - noFirewall: noFirewall, - safeOutputType: safeOutputType, - filteredIntegrity: filteredIntegrity, - evalsOnly: evalsOnly, - } + defer cancelLogsDownload(runtime.timeoutCancel) - // Iterative algorithm: keep fetching runs until we have enough or exhaust available runs -outerLoop: - for iteration < MaxIterations { - // Check context cancellation or timeout deadline - select { - case <-activeCtx.Done(): - if isDeadlineExceeded(activeCtx) { - // Our own timeout context expired — treat this as a graceful stop, - // not a hard error. break outerLoop falls through to renderLogsOutput - // which outputs whatever processedRuns were collected before the deadline. - timeoutReached = true - if verbose { - fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Timeout reached, stopping download")) - } - } else { - fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Operation cancelled")) - return activeCtx.Err() - } - break outerLoop - default: - } - - // Check timeout if specified - if timeoutMinutes > 0 { - elapsed := time.Since(startTime).Seconds() - if elapsed >= float64(timeoutMinutes)*60 { - timeoutReached = true - if verbose { - fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Timeout reached after %.1f seconds, stopping download", elapsed))) - } - break - } - } - - // Stop if we've collected enough processed runs. - // In fetchAllInRange mode, record that we hit the count limit so the caller - // can paginate to retrieve runs that fall outside this batch. - if len(processedRuns) >= count { - if fetchAllInRange { - countLimitReached = true - } - break - } - - // Query the GitHub API rate limit before each iteration (except the first) - // and wait as needed. This replaces the static cooldown sleep: the helper - // always sleeps at least APICallCooldown but will also block until the - // reset window when the remaining budget is nearly exhausted. - if iteration > 0 { - if rlErr := checkAndWaitForRateLimit(activeCtx, verbose); rlErr != nil { - if errors.Is(rlErr, context.Canceled) || errors.Is(rlErr, context.DeadlineExceeded) { - // Context was cancelled or timed out during the rate-limit wait. - // Use continue (not break) so the top-of-loop activeCtx.Done() path - // preserves Canceled vs DeadlineExceeded behavior. - continue - } - logsOrchestratorLog.Printf("Rate limit check failed (using static cooldown): %v", rlErr) - } - } - - iteration++ - - if verbose && iteration > 1 { - if fetchAllInRange { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Iteration %d: Fetching more runs in date range...", iteration))) - } else { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Iteration %d: Need %d more runs with artifacts, fetching more...", iteration, count-len(processedRuns)))) - } - } - - // Fetch a batch of runs - batchSize := BatchSize - if workflowName == "" { - // When searching for all agentic workflows, use a larger batch size - // since there may be many CI runs interspersed with agentic runs - batchSize = BatchSizeForAllWorkflows - } - - // When not fetching all in range, optimize batch size based on how many we still need - if !fetchAllInRange && count-len(processedRuns) < batchSize { - // If we need fewer runs than the batch size, request exactly what we need - // but add some buffer since many runs might not have artifacts - needed := count - len(processedRuns) - batchSize = needed * 3 // Request 3x what we need to account for runs without artifacts - if workflowName == "" && batchSize < BatchSizeForAllWorkflows { - // For all-workflows search, maintain a minimum batch size - batchSize = BatchSizeForAllWorkflows - } - if batchSize > BatchSizeForAllWorkflows { - batchSize = BatchSizeForAllWorkflows - } - } - - var oldestFetchedCreatedAt time.Time - runs, totalFetched, err := listWorkflowRunsWithPagination(ListWorkflowRunsOptions{ - WorkflowName: workflowName, - Limit: batchSize, - StartDate: startDate, - EndDate: endDate, - BeforeDate: beforeDate, - Ref: ref, - BeforeRunID: beforeRunID, - AfterRunID: afterRunID, - RepoOverride: repoOverride, - OldestFetchedCreatedAt: &oldestFetchedCreatedAt, - ProcessedCount: len(processedRuns), - TargetCount: count, - Verbose: verbose, - }) - if err != nil { - return err - } - - if len(runs) == 0 { - if shouldStopPagination(totalFetched, batchSize) { - if verbose { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("No more workflow runs found, stopping iteration")) - } - break - } - - cursor, ok := selectPaginationCursorDate(nil, oldestFetchedCreatedAt) - if !ok { - if verbose { - fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Workflow batch filtered to zero runs but no pagination cursor was found, stopping iteration")) - } - break - } - - beforeDate = cursor - if verbose { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Batch filtered to zero runs; advancing pagination cursor and continuing")) - } - continue - } - - if verbose { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Found %d workflow runs in batch %d", len(runs), iteration))) - } - - // Process runs in chunks so cache hits can satisfy the count without - // forcing us to scan the entire batch. - batchProcessed := 0 - runsRemaining := runs - innerLoop: - for len(runsRemaining) > 0 && len(processedRuns) < count { - remainingNeeded := count - len(processedRuns) - if remainingNeeded <= 0 { - break - } - - // Check context/timeout before starting each new chunk so we stop - // promptly when the deadline fires between individual chunk downloads. - select { - case <-activeCtx.Done(): - if isDeadlineExceeded(activeCtx) { - timeoutReached = true - } - break innerLoop - default: - } - - // Process slightly more than we need to account for skips due to filters. - chunkSize := min(max(remainingNeeded*3, remainingNeeded), len(runsRemaining)) - - chunk := runsRemaining[:chunkSize] - runsRemaining = runsRemaining[chunkSize:] - - downloadResults := downloadRunArtifactsConcurrent(activeCtx, chunk, runArtifactsConcurrentOptions{outputDir: outputDir, verbose: verbose, maxRuns: remainingNeeded, repoOverride: repoOverride, artifactFilter: artifactFilter, evalsOnly: evalsOnly, artifactSets: artifactSets}) - - for _, result := range downloadResults { - if result.Skipped { - if verbose { - if result.Error != nil { - fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Skipping run %d: %v", result.Run.DatabaseID, result.Error))) - } - } - continue - } - - if result.Error != nil { - fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to download artifacts for run %d: %v", result.Run.DatabaseID, result.Error))) - continue - } - - if applyRunFilters(activeCtx, result, filters, verbose) { - continue - } - - processedRun := buildProcessedRun(result, verbose, true) - - // If --parse flag is set, parse the agent log and write to log.md - if parse { - // Get the engine from aw_info.json - awInfoPath := filepath.Join(result.LogsPath, "aw_info.json") - detectedEngine := extractEngineFromAwInfo(awInfoPath, verbose) - - if err := parseAgentLog(result.LogsPath, detectedEngine, verbose); err != nil { - fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to parse log for run %d: %v", processedRun.Run.DatabaseID, err))) - } else { - // Always show success message for parsing, not just in verbose mode - logMdPath := filepath.Join(result.LogsPath, "log.md") - if fileutil.FileExists(logMdPath) { - fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("✓ Parsed log for run %d → %s", processedRun.Run.DatabaseID, logMdPath))) - } - } - - // Also parse firewall logs if they exist - if err := parseFirewallLogs(result.LogsPath, verbose); err != nil { - fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to parse firewall logs for run %d: %v", processedRun.Run.DatabaseID, err))) - } else { - // Show success message if firewall.md was created - firewallMdPath := filepath.Join(result.LogsPath, "firewall.md") - if fileutil.FileExists(firewallMdPath) { - fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("✓ Parsed firewall logs for run %d → %s", processedRun.Run.DatabaseID, firewallMdPath))) - } - } - } - - processedRuns = append(processedRuns, processedRun) - batchProcessed++ - - // Stop processing this batch once we've collected enough runs. - if len(processedRuns) >= count { - break - } - } - } - - if verbose { - if fetchAllInRange { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Processed %d runs with artifacts in batch %d (total: %d)", batchProcessed, iteration, len(processedRuns)))) - } else { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Processed %d runs with artifacts in batch %d (total: %d/%d)", batchProcessed, iteration, len(processedRuns), count))) - } - } - - // Prepare for next iteration: set beforeDate to the oldest run from the raw API batch. - // This guarantees pagination moves forward even when filtered runs are sparse. - if len(runsRemaining) == 0 { - if cursor, ok := selectPaginationCursorDate(runs, oldestFetchedCreatedAt); ok { - beforeDate = cursor - } - } - - // If we got fewer runs than requested in this batch, we've likely hit the end - // IMPORTANT: Use totalFetched (API response size before filtering) not len(runs) (after filtering) - // to detect end. When workflowName is empty, runs are filtered to only agentic workflows, - // so len(runs) may be much smaller than totalFetched even when more data is available from GitHub. - // Example: API returns 250 total runs, but only 5 are agentic workflows after filtering. - // Old buggy logic: len(runs)=5 < batchSize=250, stop iteration (WRONG - misses more agentic workflows!) - // Fixed logic: totalFetched=250 < batchSize=250 is false, continue iteration (CORRECT) - if shouldStopPagination(totalFetched, batchSize) { - if verbose { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Received fewer runs than requested, likely reached end of available runs")) - } - break - } - } - - // Check if we hit the maximum iterations limit - if iteration >= MaxIterations { - if fetchAllInRange { - fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Reached maximum iterations (%d), collected %d runs with artifacts", MaxIterations, len(processedRuns)))) - } else if len(processedRuns) < count { - fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Reached maximum iterations (%d), collected %d runs with artifacts out of %d requested", MaxIterations, len(processedRuns), count))) - } - } - - // Report if timeout was reached - if timeoutReached && len(processedRuns) > 0 { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Timeout reached, returning %d processed runs", len(processedRuns)))) - } - - if len(processedRuns) == 0 { - // When JSON output is requested, output JSON first to stdout before any stderr messages - // This prevents stderr messages from corrupting JSON when both streams are redirected together - if jsonOutput { - logsData := buildLogsData([]ProcessedRun{}, outputDir, nil) - logsData.Message = noRunsMessage(startDate, timeoutReached) - if err := renderLogsJSON(logsData, verbose); err != nil { - return fmt.Errorf("failed to render JSON output: %w", err) - } - } - // Now print warning messages to stderr after JSON output (if any) is complete - if timeoutReached { - fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Timeout reached before any runs could be downloaded")) - } else { - fmt.Fprintln(os.Stderr, console.FormatWarningMessage("No workflow runs with artifacts found matching the specified criteria")) - } - return nil + processedRuns, timeoutReached, countLimitReached, err := collectProcessedWorkflowRuns(runtime, opts) + if err != nil { + return err } - - // Apply count limit to final results (truncate to count if we fetched more) - if len(processedRuns) > count { - if verbose { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Limiting output to %d most recent runs (fetched %d total)", count, len(processedRuns)))) - } - processedRuns = processedRuns[:count] + if handled, err := handleEmptyProcessedRuns(processedRuns, opts, timeoutReached); handled || err != nil { + return err } - // Build continuation data if timeout was reached and there are processed runs, - // OR if a date-range fetch hit the count limit (more runs may exist in the window). + processedRuns = limitProcessedRuns(processedRuns, opts.Count, opts.Verbose) continuation := buildContinuationIfNeeded(processedRuns, timeoutReached, countLimitReached, continuationOptions{ - workflowName: workflowName, - startDate: startDate, - endDate: endDate, - engine: engine, - branch: ref, - afterRunID: afterRunID, - count: count, - timeoutMinutes: timeoutMinutes, + workflowName: opts.WorkflowName, + startDate: opts.StartDate, + endDate: opts.EndDate, + engine: opts.Engine, + branch: opts.Ref, + afterRunID: opts.AfterRunID, + count: opts.Count, + timeoutMinutes: opts.TimeoutMinutes, }) return renderLogsOutput(processedRuns, renderLogsOutputOptions{ - outputDir: outputDir, - summaryFile: summaryFile, - format: format, + outputDir: opts.OutputDir, + summaryFile: opts.SummaryFile, + format: opts.Format, reportFile: opts.ReportFile, - jsonOutput: jsonOutput, - toolGraph: toolGraph, - train: train, + jsonOutput: opts.JSONOutput, + toolGraph: opts.ToolGraph, + train: opts.Train, continuation: continuation, - verbose: verbose, - artifactFilter: artifactFilter, + verbose: opts.Verbose, + artifactFilter: runtime.artifactFilter, }) } diff --git a/pkg/cli/logs_orchestrator_download.go b/pkg/cli/logs_orchestrator_download.go new file mode 100644 index 00000000000..a221780fee4 --- /dev/null +++ b/pkg/cli/logs_orchestrator_download.go @@ -0,0 +1,520 @@ +package cli + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/github/gh-aw/pkg/console" + "github.com/github/gh-aw/pkg/fileutil" +) + +type logsDownloadRuntime struct { + activeCtx context.Context + startTime time.Time + timeoutCancel context.CancelFunc + artifactFilter []string + fetchAllInRange bool + filters runFilterOpts +} + +type workflowRunBatch struct { + runs []WorkflowRun + totalFetched int + batchSize int + oldestFetchedCreatedAt time.Time +} + +type processWorkflowRunBatchOptions struct { + count int + outputDir string + verbose bool + repoOverride string + artifactFilter []string + evalsOnly bool + artifactSets []string + parse bool + filters runFilterOpts +} + +func prepareLogsDownload(ctx context.Context, opts LogsDownloadOptions) (logsDownloadRuntime, error) { + logLogsDownloadStart(opts) + artifactFilter, err := resolveLogsArtifactFilter(opts.ArtifactSets, opts.Verbose) + if err != nil { + return logsDownloadRuntime{}, err + } + if err := prepareLogsDownloadOutput(ctx, opts); err != nil { + return logsDownloadRuntime{}, err + } + activeCtx, timeoutCancel, startTime := buildLogsDownloadContext(ctx, opts.TimeoutMinutes, opts.Verbose) + return logsDownloadRuntime{ + activeCtx: activeCtx, + startTime: startTime, + timeoutCancel: timeoutCancel, + artifactFilter: artifactFilter, + fetchAllInRange: opts.StartDate != "" || opts.EndDate != "", + filters: runFilterOpts{ + engine: opts.Engine, + noStaged: opts.NoStaged, + firewallOnly: opts.FirewallOnly, + noFirewall: opts.NoFirewall, + safeOutputType: opts.SafeOutputType, + filteredIntegrity: opts.FilteredIntegrity, + evalsOnly: opts.EvalsOnly, + }, + }, nil +} + +func logLogsDownloadStart(opts LogsDownloadOptions) { + logsOrchestratorLog.Printf("Starting workflow log download: workflow=%s, count=%d, startDate=%s, endDate=%s, outputDir=%s, summaryFile=%s, safeOutputType=%s, filteredIntegrity=%v, evalsOnly=%v, train=%v, format=%s, artifactSets=%v, after=%s", opts.WorkflowName, opts.Count, opts.StartDate, opts.EndDate, opts.OutputDir, opts.SummaryFile, opts.SafeOutputType, opts.FilteredIntegrity, opts.EvalsOnly, opts.Train, opts.Format, opts.ArtifactSets, opts.After) +} + +func resolveLogsArtifactFilter(artifactSets []string, verbose bool) ([]string, error) { + if err := ValidateArtifactSets(artifactSets); err != nil { + return nil, err + } + artifactFilter := ResolveArtifactFilter(artifactSets) + if len(artifactFilter) > 0 { + logsOrchestratorLog.Printf("Artifact filter active: %v", artifactFilter) + if verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Artifact filter: downloading only "+strings.Join(artifactFilter, ", "))) + } + } + return artifactFilter, nil +} + +func prepareLogsDownloadOutput(ctx context.Context, opts LogsDownloadOptions) error { + if err := ensureLogsGitignoreWithWarning(opts.Verbose); err != nil { + return err + } + if err := checkLogsDownloadContext(ctx); err != nil { + return err + } + if err := cleanupLogsOutputDir(opts); err != nil { + return err + } + if opts.Verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Fetching workflow runs from GitHub Actions...")) + } + return nil +} + +func ensureLogsGitignoreWithWarning(verbose bool) error { + if err := ensureLogsGitignore(); err != nil { + logsOrchestratorLog.Printf("Failed to ensure logs .gitignore: %v", err) + if verbose { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to ensure .github/aw/logs/.gitignore: %v", err))) + } + } + return nil +} + +func checkLogsDownloadContext(ctx context.Context) error { + select { + case <-ctx.Done(): + fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Operation cancelled")) + return ctx.Err() + default: + return nil + } +} + +func cleanupLogsOutputDir(opts LogsDownloadOptions) error { + if opts.After == "" { + return nil + } + cutoff, err := parseCleanupCutoff(opts.After) + if err != nil { + return err + } + logsOrchestratorLog.Printf("Cleaning up run folders older than %s (cutoff: %s)", opts.After, cutoff.Format(time.RFC3339)) + removed, cleanErr := cleanupOldRunFolders(opts.OutputDir, cutoff, opts.Verbose) + if cleanErr != nil { + logsOrchestratorLog.Printf("Failed to clean up old run folders: %v", cleanErr) + if !opts.JSONOutput { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to clean up old run folders: %v", cleanErr))) + } + return nil + } + if removed > 0 && !opts.JSONOutput { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Removed %d cached run folder(s) older than %s", removed, opts.After))) + } else if removed == 0 && opts.Verbose && !opts.JSONOutput { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("No cached run folders older than %s found", opts.After))) + } + return nil +} + +func buildLogsDownloadContext(ctx context.Context, timeoutMinutes int, verbose bool) (context.Context, context.CancelFunc, time.Time) { + if timeoutMinutes <= 0 { + return ctx, nil, time.Time{} + } + startTime := time.Now() + activeCtx, timeoutCancel := context.WithTimeout(ctx, time.Duration(timeoutMinutes)*time.Minute) + if verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Timeout set to %d minutes", timeoutMinutes))) + } + return activeCtx, timeoutCancel, startTime +} + +func cancelLogsDownload(cancel context.CancelFunc) { + if cancel != nil { + cancel() + } +} + +func collectProcessedWorkflowRuns(runtime logsDownloadRuntime, opts LogsDownloadOptions) ([]ProcessedRun, bool, bool, error) { + var processedRuns []ProcessedRun + var beforeDate string + var iteration int + var timeoutReached, countLimitReached bool + for iteration < MaxIterations { + stop, timedOut, err := shouldStopLogsIteration(runtime, opts) + if err != nil { + return processedRuns, timeoutReached || timedOut, countLimitReached, err + } + if stop { + timeoutReached = timeoutReached || timedOut + break + } + if len(processedRuns) >= opts.Count { + countLimitReached = runtime.fetchAllInRange + break + } + if err := waitForLogsRateLimit(runtime.activeCtx, opts.Verbose, iteration); err != nil { + continue + } + iteration++ + logLogsIterationFetch(opts, runtime.fetchAllInRange, iteration, len(processedRuns)) + batch, err := fetchWorkflowRunBatch(opts, beforeDate, len(processedRuns), runtime.fetchAllInRange) + if err != nil { + return nil, false, false, err + } + if len(batch.runs) == 0 { + cursor, shouldContinue, shouldStop := handleEmptyWorkflowRunBatch(batch, opts.Verbose) + if shouldStop { + break + } + if shouldContinue { + beforeDate = cursor + continue + } + } + logWorkflowRunBatchFound(batch, iteration, opts.Verbose) + processedRuns, batchProcessed, allRunsConsumed, timedOut := processWorkflowRunBatch(runtime.activeCtx, batch, processedRuns, processWorkflowRunBatchOptions{ + count: opts.Count, + outputDir: opts.OutputDir, + verbose: opts.Verbose, + repoOverride: opts.RepoOverride, + artifactFilter: runtime.artifactFilter, + evalsOnly: opts.EvalsOnly, + artifactSets: opts.ArtifactSets, + parse: opts.Parse, + filters: runtime.filters, + }) + timeoutReached = timeoutReached || timedOut + logProcessedWorkflowRunBatch(opts, runtime.fetchAllInRange, iteration, batchProcessed, len(processedRuns), opts.Verbose) + if allRunsConsumed { + if cursor, ok := selectPaginationCursorDate(batch.runs, batch.oldestFetchedCreatedAt); ok { + beforeDate = cursor + } + } + if shouldStopAfterWorkflowRunBatch(batch, opts.Verbose) { + break + } + } + logLogsIterationLimit(runtime.fetchAllInRange, iteration, len(processedRuns), opts.Count) + logLogsTimeoutResult(timeoutReached, len(processedRuns)) + return processedRuns, timeoutReached, countLimitReached, nil +} + +func shouldStopLogsIteration(runtime logsDownloadRuntime, opts LogsDownloadOptions) (bool, bool, error) { + select { + case <-runtime.activeCtx.Done(): + if isDeadlineExceeded(runtime.activeCtx) { + if opts.Verbose { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Timeout reached, stopping download")) + } + return true, true, nil + } + fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Operation cancelled")) + return true, false, runtime.activeCtx.Err() + default: + } + if opts.TimeoutMinutes > 0 && time.Since(runtime.startTime).Seconds() >= float64(opts.TimeoutMinutes)*60 { + if opts.Verbose { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Timeout reached after %.1f seconds, stopping download", time.Since(runtime.startTime).Seconds()))) + } + return true, true, nil + } + return false, false, nil +} + +func waitForLogsRateLimit(ctx context.Context, verbose bool, iteration int) error { + if iteration == 0 { + return nil + } + if err := checkAndWaitForRateLimit(ctx, verbose); err != nil { + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return err + } + logsOrchestratorLog.Printf("Rate limit check failed (using static cooldown): %v", err) + } + return nil +} + +func logLogsIterationFetch(opts LogsDownloadOptions, fetchAllInRange bool, iteration, processedCount int) { + if !opts.Verbose || iteration <= 1 { + return + } + if fetchAllInRange { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Iteration %d: Fetching more runs in date range...", iteration))) + return + } + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Iteration %d: Need %d more runs with artifacts, fetching more...", iteration, opts.Count-processedCount))) +} + +func fetchWorkflowRunBatch(opts LogsDownloadOptions, beforeDate string, processedCount int, fetchAllInRange bool) (workflowRunBatch, error) { + batchSize := computeLogsBatchSize(opts.WorkflowName, opts.Count, processedCount, fetchAllInRange) + var oldestFetchedCreatedAt time.Time + runs, totalFetched, err := listWorkflowRunsWithPagination(ListWorkflowRunsOptions{ + WorkflowName: opts.WorkflowName, + Limit: batchSize, + StartDate: opts.StartDate, + EndDate: opts.EndDate, + BeforeDate: beforeDate, + Ref: opts.Ref, + BeforeRunID: opts.BeforeRunID, + AfterRunID: opts.AfterRunID, + RepoOverride: opts.RepoOverride, + OldestFetchedCreatedAt: &oldestFetchedCreatedAt, + ProcessedCount: processedCount, + TargetCount: opts.Count, + Verbose: opts.Verbose, + }) + return workflowRunBatch{runs: runs, totalFetched: totalFetched, batchSize: batchSize, oldestFetchedCreatedAt: oldestFetchedCreatedAt}, err +} + +func computeLogsBatchSize(workflowName string, count, processedCount int, fetchAllInRange bool) int { + batchSize := BatchSize + if workflowName == "" { + batchSize = BatchSizeForAllWorkflows + } + if fetchAllInRange || count-processedCount >= batchSize { + return batchSize + } + needed := count - processedCount + batchSize = needed * 3 + if workflowName == "" && batchSize < BatchSizeForAllWorkflows { + batchSize = BatchSizeForAllWorkflows + } + if batchSize > BatchSizeForAllWorkflows { + batchSize = BatchSizeForAllWorkflows + } + return batchSize +} + +func handleEmptyWorkflowRunBatch(batch workflowRunBatch, verbose bool) (string, bool, bool) { + if len(batch.runs) > 0 { + return "", false, false + } + if shouldStopPagination(batch.totalFetched, batch.batchSize) { + if verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("No more workflow runs found, stopping iteration")) + } + return "", false, true + } + cursor, ok := selectPaginationCursorDate(nil, batch.oldestFetchedCreatedAt) + if !ok { + if verbose { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Workflow batch filtered to zero runs but no pagination cursor was found, stopping iteration")) + } + return "", false, true + } + if verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Batch filtered to zero runs; advancing pagination cursor and continuing")) + } + return cursor, true, false +} + +func logWorkflowRunBatchFound(batch workflowRunBatch, iteration int, verbose bool) { + if verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Found %d workflow runs in batch %d", len(batch.runs), iteration))) + } +} + +func processWorkflowRunBatch( + activeCtx context.Context, + batch workflowRunBatch, + processedRuns []ProcessedRun, + opts processWorkflowRunBatchOptions, +) ([]ProcessedRun, int, bool, bool) { + runsRemaining := batch.runs + batchProcessed := 0 + for len(runsRemaining) > 0 && len(processedRuns) < opts.count { + remainingNeeded := opts.count - len(processedRuns) + if remainingNeeded <= 0 { + break + } + if stop, timedOut := batchContextDone(activeCtx); stop { + return processedRuns, batchProcessed, false, timedOut + } + chunk := nextWorkflowRunChunk(&runsRemaining, remainingNeeded) + processedRuns, batchProcessed = appendProcessedWorkflowRuns(activeCtx, processedRuns, chunk, batchProcessed, opts) + if len(processedRuns) >= opts.count { + break + } + } + return processedRuns, batchProcessed, len(runsRemaining) == 0, false +} + +func batchContextDone(ctx context.Context) (bool, bool) { + select { + case <-ctx.Done(): + return true, isDeadlineExceeded(ctx) + default: + return false, false + } +} + +func nextWorkflowRunChunk(runsRemaining *[]WorkflowRun, remainingNeeded int) []WorkflowRun { + chunkSize := min(max(remainingNeeded*3, remainingNeeded), len(*runsRemaining)) + chunk := (*runsRemaining)[:chunkSize] + *runsRemaining = (*runsRemaining)[chunkSize:] + return chunk +} + +func appendProcessedWorkflowRuns( + activeCtx context.Context, + processedRuns []ProcessedRun, + chunk []WorkflowRun, + batchProcessed int, + opts processWorkflowRunBatchOptions, +) ([]ProcessedRun, int) { + downloadResults := downloadRunArtifactsConcurrent(activeCtx, chunk, runArtifactsConcurrentOptions{ + outputDir: opts.outputDir, + verbose: opts.verbose, + maxRuns: opts.count - len(processedRuns), + repoOverride: opts.repoOverride, + artifactFilter: opts.artifactFilter, + evalsOnly: opts.evalsOnly, + artifactSets: opts.artifactSets, + }) + for _, result := range downloadResults { + if shouldSkipProcessedWorkflowRun(result, opts.verbose) || applyRunFilters(activeCtx, result, opts.filters, opts.verbose) { + continue + } + processedRun := buildProcessedRun(result, opts.verbose, true) + parseWorkflowRunArtifacts(result, processedRun, opts.parse, opts.verbose) + processedRuns = append(processedRuns, processedRun) + batchProcessed++ + if len(processedRuns) >= opts.count { + break + } + } + return processedRuns, batchProcessed +} + +func shouldSkipProcessedWorkflowRun(result DownloadResult, verbose bool) bool { + if result.Skipped { + if verbose && result.Error != nil { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Skipping run %d: %v", result.Run.DatabaseID, result.Error))) + } + return true + } + if result.Error != nil { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to download artifacts for run %d: %v", result.Run.DatabaseID, result.Error))) + return true + } + return false +} + +func parseWorkflowRunArtifacts(result DownloadResult, processedRun ProcessedRun, parse, verbose bool) { + if !parse { + return + } + awInfoPath := filepath.Join(result.LogsPath, "aw_info.json") + detectedEngine := extractEngineFromAwInfo(awInfoPath, verbose) + if err := parseAgentLog(result.LogsPath, detectedEngine, verbose); err != nil { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to parse log for run %d: %v", processedRun.Run.DatabaseID, err))) + } else if logMdPath := filepath.Join(result.LogsPath, "log.md"); fileutil.FileExists(logMdPath) { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("✓ Parsed log for run %d → %s", processedRun.Run.DatabaseID, logMdPath))) + } + if err := parseFirewallLogs(result.LogsPath, verbose); err != nil { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to parse firewall logs for run %d: %v", processedRun.Run.DatabaseID, err))) + } else if firewallMdPath := filepath.Join(result.LogsPath, "firewall.md"); fileutil.FileExists(firewallMdPath) { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("✓ Parsed firewall logs for run %d → %s", processedRun.Run.DatabaseID, firewallMdPath))) + } +} + +func logProcessedWorkflowRunBatch(opts LogsDownloadOptions, fetchAllInRange bool, iteration, batchProcessed, processedCount int, verbose bool) { + if !verbose { + return + } + if fetchAllInRange { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Processed %d runs with artifacts in batch %d (total: %d)", batchProcessed, iteration, processedCount))) + return + } + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Processed %d runs with artifacts in batch %d (total: %d/%d)", batchProcessed, iteration, processedCount, opts.Count))) +} + +func shouldStopAfterWorkflowRunBatch(batch workflowRunBatch, verbose bool) bool { + if !shouldStopPagination(batch.totalFetched, batch.batchSize) { + return false + } + if verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Received fewer runs than requested, likely reached end of available runs")) + } + return true +} + +func logLogsIterationLimit(fetchAllInRange bool, iteration, processedCount, count int) { + if iteration < MaxIterations { + return + } + if fetchAllInRange { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Reached maximum iterations (%d), collected %d runs with artifacts", MaxIterations, processedCount))) + return + } + if processedCount < count { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Reached maximum iterations (%d), collected %d runs with artifacts out of %d requested", MaxIterations, processedCount, count))) + } +} + +func logLogsTimeoutResult(timeoutReached bool, processedCount int) { + if timeoutReached && processedCount > 0 { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Timeout reached, returning %d processed runs", processedCount))) + } +} + +func handleEmptyProcessedRuns(processedRuns []ProcessedRun, opts LogsDownloadOptions, timeoutReached bool) (bool, error) { + if len(processedRuns) > 0 { + return false, nil + } + if opts.JSONOutput { + logsData := buildLogsData([]ProcessedRun{}, opts.OutputDir, nil) + logsData.Message = noRunsMessage(opts.StartDate, timeoutReached) + if err := renderLogsJSON(logsData, opts.Verbose); err != nil { + return true, fmt.Errorf("failed to render JSON output: %w", err) + } + } + if timeoutReached { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Timeout reached before any runs could be downloaded")) + } else { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage("No workflow runs with artifacts found matching the specified criteria")) + } + return true, nil +} + +func limitProcessedRuns(processedRuns []ProcessedRun, count int, verbose bool) []ProcessedRun { + if len(processedRuns) <= count { + return processedRuns + } + if verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Limiting output to %d most recent runs (fetched %d total)", count, len(processedRuns)))) + } + return processedRuns[:count] +} diff --git a/pkg/cli/logs_orchestrator_unit_test.go b/pkg/cli/logs_orchestrator_unit_test.go index 61d4134fe12..f7dc315ba35 100644 --- a/pkg/cli/logs_orchestrator_unit_test.go +++ b/pkg/cli/logs_orchestrator_unit_test.go @@ -206,3 +206,79 @@ func TestBuildContinuationIfNeeded(t *testing.T) { assert.Nil(t, c, "expected nil when no runs were processed") }) } + +func TestComputeLogsBatchSize(t *testing.T) { + tests := []struct { + name string + workflowName string + count int + processedCount int + fetchAllInRange bool + want int + }{ + { + name: "default batch size for named workflow", + workflowName: "logs.yml", + count: 100, + processedCount: 0, + want: BatchSize, + }, + { + name: "larger default for all workflows", + count: 100, + processedCount: 0, + want: BatchSizeForAllWorkflows, + }, + { + name: "small remaining count uses buffered batch size", + workflowName: "logs.yml", + count: 10, + processedCount: 8, + want: 6, + }, + { + name: "date range keeps default batch size", + workflowName: "logs.yml", + count: 10, + processedCount: 8, + fetchAllInRange: true, + want: BatchSize, + }, + { + name: "all workflows keep minimum scan size", + count: 10, + processedCount: 8, + want: BatchSizeForAllWorkflows, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, computeLogsBatchSize(tt.workflowName, tt.count, tt.processedCount, tt.fetchAllInRange)) + }) + } +} + +func TestHandleEmptyWorkflowRunBatch(t *testing.T) { + t.Run("stop when pagination exhausted", func(t *testing.T) { + cursor, shouldContinue, shouldStop := handleEmptyWorkflowRunBatch(workflowRunBatch{ + totalFetched: 5, + batchSize: 10, + }, false) + assert.Empty(t, cursor) + assert.False(t, shouldContinue) + assert.True(t, shouldStop) + }) + + t.Run("advance cursor when more pages may exist", func(t *testing.T) { + cursorTime := time.Date(2026, time.July, 1, 12, 0, 0, 0, time.UTC) + cursor, shouldContinue, shouldStop := handleEmptyWorkflowRunBatch(workflowRunBatch{ + totalFetched: BatchSize, + batchSize: BatchSize, + oldestFetchedCreatedAt: cursorTime, + }, false) + assert.Equal(t, cursorTime.Format(time.RFC3339), cursor) + assert.True(t, shouldContinue) + assert.False(t, shouldStop) + }) +}