diff --git a/.github/workflows/daily-team-evolution-insights.lock.yml b/.github/workflows/daily-team-evolution-insights.lock.yml index eec68dcb541..4edc6082a3e 100644 --- a/.github/workflows/daily-team-evolution-insights.lock.yml +++ b/.github/workflows/daily-team-evolution-insights.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"2d0f779aaf8f7164d73363bd00b226c0ffb9138298fa71e22f4d4f95107f0bf0","body_hash":"1572e54b8a5340cd1eca0454a579e520b856fac10d8826f3eadd3cc63d1fb376","strict":true,"agent_id":"goose","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"goose":"1.45.0"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"2d0f779aaf8f7164d73363bd00b226c0ffb9138298fa71e22f4d4f95107f0bf0","body_hash":"1572e54b8a5340cd1eca0454a579e520b856fac10d8826f3eadd3cc63d1fb376","agent_id":"goose","agent_model":"copilot/claude-sonnet-4.5","engine_versions":{"goose":"1.45.0"}} # gh-aw-manifest: {"version":1,"secrets":["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":"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"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1","digest":"sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1@sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1","digest":"sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1@sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1","digest":"sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1@sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.9","digest":"sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.9@sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.9.0","digest":"sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e","pinned_image":"ghcr.io/github/github-mcp-server:v1.9.0@sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e"}]} # 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 # @@ -153,7 +153,7 @@ jobs: GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_INFO_AGENT_RUNTIME: "" GH_AW_INFO_FRONTMATTER_EMOJI: "📊" - GH_AW_COMPILED_STRICT: "true" + GH_AW_COMPILED_STRICT: "false" GH_AW_INFO_FEATURES: '{"gh-aw-detection":true}' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: @@ -162,6 +162,11 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); + - name: Enforce strict mode policy + if: ${{ vars.GH_AW_POLICY_STRICT == 'true' }} + run: | + echo "::error::GH_AW_POLICY_STRICT=true but this workflow was not compiled in strict mode. Recompile with --strict or strict: true." + exit 1 - name: Restore daily AIC usage cache id: restore-daily-aic-cache if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} diff --git a/.github/workflows/mcp-inspector.lock.yml b/.github/workflows/mcp-inspector.lock.yml index ad799555f5c..9557d8df63d 100644 --- a/.github/workflows/mcp-inspector.lock.yml +++ b/.github/workflows/mcp-inspector.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"059f9e679f1a5a2bd3e7ad1308a9c25e36ad9c93f4db2a618ea6aca1280af00a","body_hash":"42daae928c8295891472e702ddec1ad8479ca526a291e5a9db7f7f89037a8b94","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80","copilot-sdk":"1.0.11"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"059f9e679f1a5a2bd3e7ad1308a9c25e36ad9c93f4db2a618ea6aca1280af00a","body_hash":"42daae928c8295891472e702ddec1ad8479ca526a291e5a9db7f7f89037a8b94","agent_id":"copilot","engine_versions":{"copilot":"1.0.80","copilot-sdk":"1.0.11"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","DD_API_KEY","DD_APPLICATION_KEY","DD_APP_KEY","DD_SITE","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","SENTRY_ACCESS_TOKEN","SENTRY_OPENAI_API_KEY","SLACK_BOT_TOKEN","TAVILY_API_KEY"],"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-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"},{"repo":"docker/build-push-action","sha":"53b7df96c91f9c12dcc8a07bcb9ccacbed38856a","version":"v7.3.0"},{"repo":"docker/setup-buildx-action","sha":"bb05f3f5519dd87d3ba754cc423b652a5edd6d2c","version":"v4.2.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1","digest":"sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1@sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1","digest":"sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1@sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1","digest":"sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1@sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.9","digest":"sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.9@sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.9.0","digest":"sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e","pinned_image":"ghcr.io/github/github-mcp-server:v1.9.0@sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e"},{"image":"ghcr.io/oraios/serena:latest","digest":"sha256:0944b2ffe66dbcddeed531694b6819d7f9efd8125b442b282a1cc863f570a03e","pinned_image":"ghcr.io/oraios/serena:latest@sha256:0944b2ffe66dbcddeed531694b6819d7f9efd8125b442b282a1cc863f570a03e"},{"image":"node:lts-alpine","digest":"sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43","pinned_image":"node:lts-alpine@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43"}]} # 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 # @@ -178,7 +178,7 @@ jobs: GH_AW_INFO_AGENT_RUNTIME: "cloud-hypervisor" GH_AW_INFO_CACHE_MEMORY: "true" GH_AW_INFO_FRONTMATTER_EMOJI: "🔍" - GH_AW_COMPILED_STRICT: "true" + GH_AW_COMPILED_STRICT: "false" GH_AW_INFO_FEATURES: '{"gh-aw-detection":true}' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: @@ -187,6 +187,11 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); + - name: Enforce strict mode policy + if: ${{ vars.GH_AW_POLICY_STRICT == 'true' }} + run: | + echo "::error::GH_AW_POLICY_STRICT=true but this workflow was not compiled in strict mode. Recompile with --strict or strict: true." + exit 1 - name: Restore daily AIC usage cache id: restore-daily-aic-cache if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} diff --git a/.github/workflows/smoke-copilot-aoai-apikey.lock.yml b/.github/workflows/smoke-copilot-aoai-apikey.lock.yml index cff9818de4f..593c3f9624b 100644 --- a/.github/workflows/smoke-copilot-aoai-apikey.lock.yml +++ b/.github/workflows/smoke-copilot-aoai-apikey.lock.yml @@ -856,6 +856,11 @@ jobs: "ref": { "description": "The git ref (branch, tag, or SHA) to dispatch the workflow on. Must match one of the configured allowed ref patterns: refs/heads/${{ github.event.repository.default_branch }}. If omitted, the dispatching workflow's ref is used.", "type": "string" + }, + "temporary_id": { + "description": "Optional temporary ID to associate with the dispatched workflow run. Use this ID as the run_id in a later approve_workflow_run call.", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$", + "type": "string" } }, "required": [ @@ -1112,6 +1117,10 @@ jobs: "pattern": "^[^\\x00-\\x20\\x7f~^:?*\\[\\\\]+$", "patternError": "must be a valid git ref" }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" + }, "workflow_name": { "required": true, "type": "string", diff --git a/.github/workflows/smoke-copilot-aoai-entra.lock.yml b/.github/workflows/smoke-copilot-aoai-entra.lock.yml index b60d9470c68..8c18fe17ea6 100644 --- a/.github/workflows/smoke-copilot-aoai-entra.lock.yml +++ b/.github/workflows/smoke-copilot-aoai-entra.lock.yml @@ -872,6 +872,11 @@ jobs: "ref": { "description": "The git ref (branch, tag, or SHA) to dispatch the workflow on. Must match one of the configured allowed ref patterns: refs/heads/${{ github.event.repository.default_branch }}. If omitted, the dispatching workflow's ref is used.", "type": "string" + }, + "temporary_id": { + "description": "Optional temporary ID to associate with the dispatched workflow run. Use this ID as the run_id in a later approve_workflow_run call.", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$", + "type": "string" } }, "required": [ @@ -1128,6 +1133,10 @@ jobs: "pattern": "^[^\\x00-\\x20\\x7f~^:?*\\[\\\\]+$", "patternError": "must be a valid git ref" }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" + }, "workflow_name": { "required": true, "type": "string", diff --git a/.github/workflows/smoke-copilot-arm.lock.yml b/.github/workflows/smoke-copilot-arm.lock.yml index 94c53c8314a..b827e7565d7 100644 --- a/.github/workflows/smoke-copilot-arm.lock.yml +++ b/.github/workflows/smoke-copilot-arm.lock.yml @@ -776,6 +776,11 @@ jobs: "ref": { "description": "The git ref (branch, tag, or SHA) to dispatch the workflow on. Must match one of the configured allowed ref patterns: refs/heads/${{ github.event.repository.default_branch }}. If omitted, the dispatching workflow's ref is used.", "type": "string" + }, + "temporary_id": { + "description": "Optional temporary ID to associate with the dispatched workflow run. Use this ID as the run_id in a later approve_workflow_run call.", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$", + "type": "string" } }, "required": [ @@ -960,6 +965,10 @@ jobs: "pattern": "^[^\\x00-\\x20\\x7f~^:?*\\[\\\\]+$", "patternError": "must be a valid git ref" }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" + }, "workflow_name": { "required": true, "type": "string", diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index fe5b20d43c0..5424e2b710f 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -876,6 +876,11 @@ jobs: "ref": { "description": "The git ref (branch, tag, or SHA) to dispatch the workflow on. Must match one of the configured allowed ref patterns: refs/heads/${{ github.event.repository.default_branch }}. If omitted, the dispatching workflow's ref is used.", "type": "string" + }, + "temporary_id": { + "description": "Optional temporary ID to associate with the dispatched workflow run. Use this ID as the run_id in a later approve_workflow_run call.", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$", + "type": "string" } }, "required": [ @@ -1132,6 +1137,10 @@ jobs: "pattern": "^[^\\x00-\\x20\\x7f~^:?*\\[\\\\]+$", "patternError": "must be a valid git ref" }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" + }, "workflow_name": { "required": true, "type": "string", diff --git a/.github/workflows/squad-implement-worker.lock.yml b/.github/workflows/squad-implement-worker.lock.yml index efa5ec107cc..71001b44593 100644 --- a/.github/workflows/squad-implement-worker.lock.yml +++ b/.github/workflows/squad-implement-worker.lock.yml @@ -638,6 +638,11 @@ jobs: "ref": { "description": "The git ref (branch, tag, or SHA) to dispatch the workflow on. Must match one of the configured allowed ref patterns: refs/heads/${{ github.event.repository.default_branch }}. If omitted, the dispatching workflow's ref is used.", "type": "string" + }, + "temporary_id": { + "description": "Optional temporary ID to associate with the dispatched workflow run. Use this ID as the run_id in a later approve_workflow_run call.", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$", + "type": "string" } }, "type": "object" @@ -761,6 +766,10 @@ jobs: "pattern": "^[^\\x00-\\x20\\x7f~^:?*\\[\\\\]+$", "patternError": "must be a valid git ref" }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" + }, "workflow_name": { "required": true, "type": "string", diff --git a/.github/workflows/squad.lock.yml b/.github/workflows/squad.lock.yml index 4f0ec1c75f3..4a42526c9a4 100644 --- a/.github/workflows/squad.lock.yml +++ b/.github/workflows/squad.lock.yml @@ -679,6 +679,11 @@ jobs: "ref": { "description": "The git ref (branch, tag, or SHA) to dispatch the workflow on. Must match one of the configured allowed ref patterns: refs/heads/${{ github.event.repository.default_branch }}. If omitted, the dispatching workflow's ref is used.", "type": "string" + }, + "temporary_id": { + "description": "Optional temporary ID to associate with the dispatched workflow run. Use this ID as the run_id in a later approve_workflow_run call.", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$", + "type": "string" } }, "required": [ @@ -1291,6 +1296,10 @@ jobs: "pattern": "^[^\\x00-\\x20\\x7f~^:?*\\[\\\\]+$", "patternError": "must be a valid git ref" }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" + }, "workflow_name": { "required": true, "type": "string", diff --git a/actions/setup/js/approve_workflow_run.cjs b/actions/setup/js/approve_workflow_run.cjs index 028c4b3ca27..782d1af3e0a 100644 --- a/actions/setup/js/approve_workflow_run.cjs +++ b/actions/setup/js/approve_workflow_run.cjs @@ -12,6 +12,7 @@ const path = require("node:path"); const { isStagedMode } = require("./safe_output_helpers.cjs"); const { logStagedPreviewInfo } = require("./staged_preview.cjs"); const { checkFileProtectionPostApply } = require("./manifest_file_helpers.cjs"); +const { loadTemporaryIdMapFromResolved, resolveIssueNumber } = require("./temporary_id.cjs"); /** @type {string} Safe output type handled by this module */ const HANDLER_TYPE = "approve_workflow_run"; @@ -129,10 +130,17 @@ async function main(config = {}) { const githubClient = isStaged ? null : await createAuthenticatedGitHubClient(config); - return async function handleApproveWorkflowRun(message) { - const runId = parsePositiveInt(message.run_id); + return async function handleApproveWorkflowRun(message, resolvedTemporaryIds = {}) { + const resolvedRunId = resolveIssueNumber(message.run_id, loadTemporaryIdMapFromResolved(resolvedTemporaryIds)); + const runId = resolvedRunId.wasTemporaryId ? (resolvedRunId.resolved?.number ?? undefined) : parsePositiveInt(message.run_id); if (!runId) { - const error = "run_id must be a positive integer"; + const error = (resolvedRunId.wasTemporaryId ? resolvedRunId.errorMessage : null) || "run_id must be a positive integer or resolved temporary ID"; + core.warning(error); + return { success: false, error }; + } + const resolvedRepo = resolvedRunId.resolved?.repo; + if (resolvedRunId.wasTemporaryId && resolvedRepo !== `${context.repo.owner}/${context.repo.repo}`) { + const error = `Temporary workflow run ID '${message.run_id}' belongs to ${resolvedRepo}, not ${context.repo.owner}/${context.repo.repo}`; core.warning(error); return { success: false, error }; } diff --git a/actions/setup/js/approve_workflow_run.test.cjs b/actions/setup/js/approve_workflow_run.test.cjs index b2d34ede322..1bd2fda53cf 100644 --- a/actions/setup/js/approve_workflow_run.test.cjs +++ b/actions/setup/js/approve_workflow_run.test.cjs @@ -84,6 +84,27 @@ describe("approve_workflow_run", () => { expect(mockGetWorkflowRun).toHaveBeenCalledWith(expect.objectContaining({ run_id: 123 })); }); + it("resolves a temporary workflow run ID", async () => { + const { main } = require("./approve_workflow_run.cjs"); + const handler = await main(externalTokenConfig); + + const result = await handler({ run_id: "aw_run123" }, { aw_run123: { repo: "test-owner/test-repo", number: 123 } }); + + expect(result.success).toBe(true); + expect(mockGetWorkflowRun).toHaveBeenCalledWith(expect.objectContaining({ run_id: 123 })); + }); + + it("rejects a temporary workflow run ID from another repository", async () => { + const { main } = require("./approve_workflow_run.cjs"); + const handler = await main(externalTokenConfig); + + const result = await handler({ run_id: "aw_run123" }, { aw_run123: { repo: "other-owner/other-repo", number: 123 } }); + + expect(result.success).toBe(false); + expect(result.error).toContain("other-owner/other-repo"); + expect(mockGetWorkflowRun).not.toHaveBeenCalled(); + }); + it.each([undefined, "", 0, -1, 1.5, "abc", "12abc"])("rejects invalid run ID %j", async runId => { const { main } = require("./approve_workflow_run.cjs"); const handler = await main(externalTokenConfig); diff --git a/actions/setup/js/dispatch_workflow.cjs b/actions/setup/js/dispatch_workflow.cjs index 3c049eb04a6..3e81a7cd8a2 100644 --- a/actions/setup/js/dispatch_workflow.cjs +++ b/actions/setup/js/dispatch_workflow.cjs @@ -325,6 +325,9 @@ async function main(config = {}) { core.info(`✓ Successfully dispatched workflow: ${workflowFile} (run ID: ${runId})`); } else { core.info(`✓ Successfully dispatched workflow: ${workflowFile}`); + if (typeof message.temporary_id === "string") { + core.warning(`Unable to register temporary ID '${message.temporary_id}' because the GitHub API did not return a workflow run ID.`); + } } // Record the time of this dispatch for rate limiting @@ -335,6 +338,7 @@ async function main(config = {}) { workflow_name: workflowName, inputs: inputs, run_id: runId, + ...(typeof message.temporary_id === "string" && runId ? { temporaryId: message.temporary_id, repo: resolvedRepoSlug, number: runId } : {}), }; } catch (error) { const errorMessage = getErrorMessage(error); diff --git a/actions/setup/js/dispatch_workflow.test.cjs b/actions/setup/js/dispatch_workflow.test.cjs index a48be3d1422..ed2316fd00a 100644 --- a/actions/setup/js/dispatch_workflow.test.cjs +++ b/actions/setup/js/dispatch_workflow.test.cjs @@ -839,6 +839,26 @@ describe("dispatch_workflow handler factory", () => { expect(core.info).toHaveBeenCalledWith(expect.stringContaining("run ID: 987654")); }); + it("should return temporary ID mapping for a dispatched workflow run", async () => { + github.rest.actions.createWorkflowDispatch.mockResolvedValueOnce({ + data: { workflow_run_id: 987654 }, + }); + const handler = await main({ + workflows: ["test-workflow"], + workflow_files: { "test-workflow": ".lock.yml" }, + }); + + const result = await handler({ type: "dispatch_workflow", workflow_name: "test-workflow", temporary_id: "aw_run123", inputs: {} }, {}); + + expect(result).toMatchObject({ + success: true, + run_id: 987654, + temporaryId: "aw_run123", + repo: "test-owner/test-repo", + number: 987654, + }); + }); + it("should succeed without run_id when API returns no workflow_run_id", async () => { github.rest.actions.createWorkflowDispatch.mockResolvedValueOnce({ data: {} }); diff --git a/actions/setup/js/safe_outputs_tools.json b/actions/setup/js/safe_outputs_tools.json index 553cefd158c..4d30dc11ec2 100644 --- a/actions/setup/js/safe_outputs_tools.json +++ b/actions/setup/js/safe_outputs_tools.json @@ -1916,14 +1916,14 @@ }, { "name": "approve_workflow_run", - "description": "Approve a GitHub Actions workflow run awaiting required approval. Supply the positive run ID from the workflow run URL only when the run belongs to the triggering pull request or an explicitly allowed pull request. The handler approves only pull request runs with status waiting; it rejects other runs.", + "description": "Approve a GitHub Actions workflow run awaiting required approval. Supply the positive run ID from the workflow run URL or a temporary ID returned by dispatch_workflow, only when the run belongs to the triggering pull request or an explicitly allowed pull request. The handler approves only pull request runs with status waiting; it rejects other runs.", "inputSchema": { "type": "object", "required": ["run_id"], "properties": { "run_id": { "type": ["number", "string"], - "description": "Positive integer workflow run ID to approve (for example, 123456789 from /actions/runs/123456789).", + "description": "Positive integer workflow run ID to approve, or the temporary ID returned by a dispatch_workflow call (for example, 123456789 from /actions/runs/123456789 or aw_workflow_run).", "x-synonyms": ["runId", "workflow_run_id"] }, "secrecy": { diff --git a/actions/setup/js/safe_outputs_tools_loader.cjs b/actions/setup/js/safe_outputs_tools_loader.cjs index 06990ff15dc..3170f0d9f20 100644 --- a/actions/setup/js/safe_outputs_tools_loader.cjs +++ b/actions/setup/js/safe_outputs_tools_loader.cjs @@ -167,10 +167,11 @@ function attachHandlers(tools, handlers, logger) { // Create a custom handler that wraps args in inputs and adds workflow_name const workflowName = tool._workflow_name.trim(); tool.handler = args => { - const { ref, ...inputs } = args ?? {}; + const { ref, temporary_id, ...inputs } = args ?? {}; // Wrap workflow inputs in inputs and pass dispatch ref as top-level field return handlers.defaultHandler("dispatch_workflow")({ ...(ref && { ref }), + ...(temporary_id && { temporary_id }), ...(args !== undefined && { inputs }), workflow_name: workflowName, }); diff --git a/actions/setup/js/safe_outputs_tools_loader.test.cjs b/actions/setup/js/safe_outputs_tools_loader.test.cjs index 10c6a134bbb..21c08ac8c25 100644 --- a/actions/setup/js/safe_outputs_tools_loader.test.cjs +++ b/actions/setup/js/safe_outputs_tools_loader.test.cjs @@ -251,6 +251,21 @@ describe("safe_outputs_tools_loader", () => { }); }); + it("should pass temporary_id separately from dispatch workflow inputs", () => { + const tools = [{ name: "ci_workflow", description: "CI workflow", _workflow_name: "ci" }]; + const mockHandlerFunction = vi.fn(); + const handlers = { defaultHandler: vi.fn(() => mockHandlerFunction) }; + + const result = attachHandlers(tools, handlers); + result[0].handler({ environment: "staging", temporary_id: "aw_run123" }); + + expect(mockHandlerFunction).toHaveBeenCalledWith({ + workflow_name: "ci", + temporary_id: "aw_run123", + inputs: { environment: "staging" }, + }); + }); + it("should pass ref as top-level field for dispatch_workflow handler", () => { const tools = [{ name: "ci_workflow", description: "CI workflow", _workflow_name: "ci" }]; const mockHandlerFunction = vi.fn(); diff --git a/pkg/workflow/js/safe_outputs_tools.json b/pkg/workflow/js/safe_outputs_tools.json index 553cefd158c..4d30dc11ec2 100644 --- a/pkg/workflow/js/safe_outputs_tools.json +++ b/pkg/workflow/js/safe_outputs_tools.json @@ -1916,14 +1916,14 @@ }, { "name": "approve_workflow_run", - "description": "Approve a GitHub Actions workflow run awaiting required approval. Supply the positive run ID from the workflow run URL only when the run belongs to the triggering pull request or an explicitly allowed pull request. The handler approves only pull request runs with status waiting; it rejects other runs.", + "description": "Approve a GitHub Actions workflow run awaiting required approval. Supply the positive run ID from the workflow run URL or a temporary ID returned by dispatch_workflow, only when the run belongs to the triggering pull request or an explicitly allowed pull request. The handler approves only pull request runs with status waiting; it rejects other runs.", "inputSchema": { "type": "object", "required": ["run_id"], "properties": { "run_id": { "type": ["number", "string"], - "description": "Positive integer workflow run ID to approve (for example, 123456789 from /actions/runs/123456789).", + "description": "Positive integer workflow run ID to approve, or the temporary ID returned by a dispatch_workflow call (for example, 123456789 from /actions/runs/123456789 or aw_workflow_run).", "x-synonyms": ["runId", "workflow_run_id"] }, "secrecy": { diff --git a/pkg/workflow/safe_output_validation_config_test.go b/pkg/workflow/safe_output_validation_config_test.go index 9c1450b3afa..84d306cdde8 100644 --- a/pkg/workflow/safe_output_validation_config_test.go +++ b/pkg/workflow/safe_output_validation_config_test.go @@ -80,8 +80,8 @@ func TestApproveWorkflowRunValidationConfig(t *testing.T) { if config.DefaultMax != 1 { t.Errorf("approve_workflow_run DefaultMax = %d, want 1", config.DefaultMax) } - if runID := config.Fields["run_id"]; !runID.Required || !runID.PositiveInteger { - t.Errorf("approve_workflow_run run_id = %+v, want required positive integer", runID) + if runID := config.Fields["run_id"]; !runID.Required || !runID.IssueNumberOrTemporaryID { + t.Errorf("approve_workflow_run run_id = %+v, want required positive integer or temporary ID", runID) } jsonStr, err := GetValidationConfigJSONWithDataSchema([]string{"approve_workflow_run"}, nil, false, nil) @@ -96,8 +96,8 @@ func TestApproveWorkflowRunValidationConfig(t *testing.T) { if len(parsed) != 1 || !ok || parsedConfig.DefaultMax != 1 { t.Errorf("approve_workflow_run validation config = %#v, want defaultMax 1", parsedConfig) } - if runID := parsedConfig.Fields["run_id"]; !runID.Required || !runID.PositiveInteger { - t.Errorf("approve_workflow_run generated run_id = %+v, want required positive integer", runID) + if runID := parsedConfig.Fields["run_id"]; !runID.Required || !runID.IssueNumberOrTemporaryID { + t.Errorf("approve_workflow_run generated run_id = %+v, want required positive integer or temporary ID", runID) } } diff --git a/pkg/workflow/safe_outputs_dispatch.go b/pkg/workflow/safe_outputs_dispatch.go index 3bbf3177847..79e305c77c0 100644 --- a/pkg/workflow/safe_outputs_dispatch.go +++ b/pkg/workflow/safe_outputs_dispatch.go @@ -131,6 +131,11 @@ func generateDispatchWorkflowTool(workflowName string, workflowInputs map[string inputSchema, _ := tool["inputSchema"].(map[string]any) properties, _ := inputSchema["properties"].(map[string]any) + properties["temporary_id"] = map[string]any{ + "type": "string", + "description": "Optional temporary ID to associate with the dispatched workflow run. Use this ID as the run_id in a later approve_workflow_run call.", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$", + } requiredCount := 0 if required, ok := inputSchema["required"].([]string); ok { requiredCount = len(required) diff --git a/pkg/workflow/safe_outputs_tools_generation_test.go b/pkg/workflow/safe_outputs_tools_generation_test.go index 3856f9e5514..37a061fe49f 100644 --- a/pkg/workflow/safe_outputs_tools_generation_test.go +++ b/pkg/workflow/safe_outputs_tools_generation_test.go @@ -274,6 +274,11 @@ func TestGenerateDispatchWorkflowToolBasic(t *testing.T) { require.True(t, ok, "environment property should exist") assert.Equal(t, "string", envProp["type"], "choice maps to string") assert.Equal(t, []any{"staging", "production"}, envProp["enum"], "enum values should match") + + temporaryIDProp, ok := properties["temporary_id"].(map[string]any) + require.True(t, ok, "temporary_id property should exist") + assert.Equal(t, "string", temporaryIDProp["type"]) + assert.Equal(t, "^#?aw_[A-Za-z0-9_]{3,12}$", temporaryIDProp["pattern"]) } // TestGenerateDispatchWorkflowToolEmptyInputs tests dispatch workflow tool with no inputs. @@ -284,7 +289,8 @@ func TestGenerateDispatchWorkflowToolEmptyInputs(t *testing.T) { inputSchema := tool["inputSchema"].(map[string]any) properties := inputSchema["properties"].(map[string]any) - assert.Empty(t, properties, "Properties should be empty for workflow with no inputs") + assert.Len(t, properties, 1, "Only the temporary_id property should exist for workflow with no inputs") + assert.Contains(t, properties, "temporary_id") _, hasRequired := inputSchema["required"] assert.False(t, hasRequired, "required field should not be present when no required inputs") diff --git a/pkg/workflow/safe_outputs_validation_config.go b/pkg/workflow/safe_outputs_validation_config.go index 9d5fb7ee2dd..41806591496 100644 --- a/pkg/workflow/safe_outputs_validation_config.go +++ b/pkg/workflow/safe_outputs_validation_config.go @@ -81,7 +81,7 @@ var ValidationConfig = map[string]TypeValidationConfig{ "approve_workflow_run": { DefaultMax: 1, Fields: map[string]FieldValidation{ - "run_id": {Required: true, PositiveInteger: true}, + "run_id": {Required: true, IssueNumberOrTemporaryID: true}, }, }, "add_comment": { @@ -332,6 +332,7 @@ var ValidationConfig = map[string]TypeValidationConfig{ "workflow_name": {Required: true, Type: "string", Sanitize: true, MinLength: 1, MaxLength: 256, Pattern: ".*\\S.*", PatternError: "must not be empty"}, "inputs": {Type: "object"}, "ref": {Type: "string", MinLength: 1, MaxLength: 256, Pattern: "^[^\\x00-\\x20\\x7f~^:?*\\[\\\\]+$", PatternError: "must be a valid git ref"}, + "temporary_id": {Type: "string", Pattern: "^#?aw_[A-Za-z0-9_]{3,12}$"}, }, }, "missing_tool": { diff --git a/schemas/agent-output.json b/schemas/agent-output.json index f4647184521..7521a8a390d 100644 --- a/schemas/agent-output.json +++ b/schemas/agent-output.json @@ -606,7 +606,7 @@ }, "run_id": { "oneOf": [{ "type": "number" }, { "type": "string" }], - "description": "Positive integer workflow run ID to approve" + "description": "Positive integer workflow run ID to approve, or a temporary ID returned by dispatch_workflow" } }, "required": ["type", "run_id"], @@ -868,6 +868,11 @@ "type": "string", "description": "Optional git ref or branch name to dispatch against (highest priority when provided). Requires safe-outputs.dispatch-workflow.allowed-refs configuration.", "minLength": 1 + }, + "temporary_id": { + "type": "string", + "description": "Optional temporary ID used to reference the dispatched workflow run in later safe output calls", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" } }, "required": ["type", "workflow_name"],