From c74cbe4b395958c6128b3b545e03587c502ee36e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:27:13 +0000 Subject: [PATCH 1/7] Initial plan From 5817acfd8619dc6cc54e4d56ab6b5f9818019616 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:40:07 +0000 Subject: [PATCH 2/7] chore: outline plan for update_issue MCP handler fix Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/daily-file-diet.lock.yml | 3 ++- .github/workflows/daily-testify-uber-super-expert.lock.yml | 3 ++- .github/workflows/go-fan.lock.yml | 3 ++- .github/workflows/spec-librarian.lock.yml | 3 ++- actions/setup-cli/install.sh | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/daily-file-diet.lock.yml b/.github/workflows/daily-file-diet.lock.yml index b9a00bfac1d..70c52032698 100644 --- a/.github/workflows/daily-file-diet.lock.yml +++ b/.github/workflows/daily-file-diet.lock.yml @@ -825,6 +825,7 @@ jobs: # --allow-tool github # --allow-tool safeoutputs # --allow-tool serena + # --allow-tool shell(awk) # --allow-tool shell(cat pkg/**/*.go) # --allow-tool shell(cat) # --allow-tool shell(date) @@ -901,7 +902,7 @@ jobs: fi # shellcheck disable=SC1003 sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GH_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull --difc-proxy-host host.docker.internal:18443 --difc-proxy-ca-cert /tmp/gh-aw/difc-proxy-tls/ca.crt \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --agent developer.instructions --allow-tool github --allow-tool safeoutputs --allow-tool serena --allow-tool '\''shell(cat pkg/**/*.go)'\'' --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(find pkg -name "*.go" ! -name "*_test.go" -type f -exec wc -l {} \; | sort -rn)'\'' --allow-tool '\''shell(find pkg -name "*.go" ! -name "*_test.go" -type f)'\'' --allow-tool '\''shell(find pkg -type f -name "*.go" ! -name "*_test.go")'\'' --allow-tool '\''shell(find pkg/ -maxdepth 1 -ls)'\'' --allow-tool '\''shell(find pkg/workflow/ -maxdepth 1 -ls)'\'' --allow-tool '\''shell(gh:*)'\'' --allow-tool '\''shell(grep -r "func " pkg --include="*.go")'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head -n * pkg/**/*.go)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(printf)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(safeoutputs:*)'\'' --allow-tool '\''shell(serena:*)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc -l pkg/**/*.go)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --agent developer.instructions --allow-tool github --allow-tool safeoutputs --allow-tool serena --allow-tool '\''shell(awk)'\'' --allow-tool '\''shell(cat pkg/**/*.go)'\'' --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(find pkg -name "*.go" ! -name "*_test.go" -type f -exec wc -l {} \; | sort -rn)'\'' --allow-tool '\''shell(find pkg -name "*.go" ! -name "*_test.go" -type f)'\'' --allow-tool '\''shell(find pkg -type f -name "*.go" ! -name "*_test.go")'\'' --allow-tool '\''shell(find pkg/ -maxdepth 1 -ls)'\'' --allow-tool '\''shell(find pkg/workflow/ -maxdepth 1 -ls)'\'' --allow-tool '\''shell(gh:*)'\'' --allow-tool '\''shell(grep -r "func " pkg --include="*.go")'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head -n * pkg/**/*.go)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(printf)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(safeoutputs:*)'\'' --allow-tool '\''shell(serena:*)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc -l pkg/**/*.go)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE diff --git a/.github/workflows/daily-testify-uber-super-expert.lock.yml b/.github/workflows/daily-testify-uber-super-expert.lock.yml index 7563eb927a8..eed47f927bd 100644 --- a/.github/workflows/daily-testify-uber-super-expert.lock.yml +++ b/.github/workflows/daily-testify-uber-super-expert.lock.yml @@ -851,6 +851,7 @@ jobs: # --allow-tool github # --allow-tool safeoutputs # --allow-tool serena + # --allow-tool shell(awk) # --allow-tool shell(cat **/*_test.go) # --allow-tool shell(cat pkg/**/*.go) # --allow-tool shell(cat) @@ -940,7 +941,7 @@ jobs: COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} COPILOT_SDK_URI: http://127.0.0.1:3002 GH_AW_COPILOT_SDK_DRIVER: 1 - GH_AW_COPILOT_SDK_SERVER_ARGS: '["--headless","--no-auto-update","--port","3002","--add-dir","/tmp/gh-aw/","--log-level","all","--log-dir","/tmp/gh-aw/sandbox/agent/logs/","--disable-builtin-mcps","--no-ask-user","--allow-tool","github","--allow-tool","safeoutputs","--allow-tool","serena","--allow-tool","shell(cat **/*_test.go)","--allow-tool","shell(cat pkg/**/*.go)","--allow-tool","shell(cat)","--allow-tool","shell(date)","--allow-tool","shell(echo)","--allow-tool","shell(find . -name \"*_test.go\" -type f)","--allow-tool","shell(find pkg -name \"*.go\" ! -name \"*_test.go\" -type f)","--allow-tool","shell(find pkg -type f -name \"*.go\" ! -name \"*_test.go\")","--allow-tool","shell(find pkg/ -maxdepth 1 -ls)","--allow-tool","shell(find pkg/workflow/ -maxdepth 1 -ls)","--allow-tool","shell(gh:*)","--allow-tool","shell(go test -v ./...)","--allow-tool","shell(grep -r \"func \" pkg --include=\"*.go\")","--allow-tool","shell(grep -r \"func Test\" . --include=\"*_test.go\")","--allow-tool","shell(grep)","--allow-tool","shell(head -n * pkg/**/*.go)","--allow-tool","shell(head)","--allow-tool","shell(ls)","--allow-tool","shell(printf)","--allow-tool","shell(pwd)","--allow-tool","shell(safeoutputs:*)","--allow-tool","shell(serena:*)","--allow-tool","shell(sort)","--allow-tool","shell(tail)","--allow-tool","shell(uniq)","--allow-tool","shell(wc -l **/*_test.go)","--allow-tool","shell(wc -l pkg/**/*.go)","--allow-tool","shell(wc)","--allow-tool","shell(yq)","--allow-tool","write","--allow-all-paths"]' + GH_AW_COPILOT_SDK_SERVER_ARGS: '["--headless","--no-auto-update","--port","3002","--add-dir","/tmp/gh-aw/","--log-level","all","--log-dir","/tmp/gh-aw/sandbox/agent/logs/","--disable-builtin-mcps","--no-ask-user","--allow-tool","github","--allow-tool","safeoutputs","--allow-tool","serena","--allow-tool","shell(awk)","--allow-tool","shell(cat **/*_test.go)","--allow-tool","shell(cat pkg/**/*.go)","--allow-tool","shell(cat)","--allow-tool","shell(date)","--allow-tool","shell(echo)","--allow-tool","shell(find . -name \"*_test.go\" -type f)","--allow-tool","shell(find pkg -name \"*.go\" ! -name \"*_test.go\" -type f)","--allow-tool","shell(find pkg -type f -name \"*.go\" ! -name \"*_test.go\")","--allow-tool","shell(find pkg/ -maxdepth 1 -ls)","--allow-tool","shell(find pkg/workflow/ -maxdepth 1 -ls)","--allow-tool","shell(gh:*)","--allow-tool","shell(go test -v ./...)","--allow-tool","shell(grep -r \"func \" pkg --include=\"*.go\")","--allow-tool","shell(grep -r \"func Test\" . --include=\"*_test.go\")","--allow-tool","shell(grep)","--allow-tool","shell(head -n * pkg/**/*.go)","--allow-tool","shell(head)","--allow-tool","shell(ls)","--allow-tool","shell(printf)","--allow-tool","shell(pwd)","--allow-tool","shell(safeoutputs:*)","--allow-tool","shell(serena:*)","--allow-tool","shell(sort)","--allow-tool","shell(tail)","--allow-tool","shell(uniq)","--allow-tool","shell(wc -l **/*_test.go)","--allow-tool","shell(wc -l pkg/**/*.go)","--allow-tool","shell(wc)","--allow-tool","shell(yq)","--allow-tool","write","--allow-all-paths"]' GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TOOL_DENIALS: 5 GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} diff --git a/.github/workflows/go-fan.lock.yml b/.github/workflows/go-fan.lock.yml index 4a86548f579..9454bff69f8 100644 --- a/.github/workflows/go-fan.lock.yml +++ b/.github/workflows/go-fan.lock.yml @@ -844,6 +844,7 @@ jobs: - name: Execute Claude Code CLI id: agentic_execution # Allowed tools (sorted): + # - Bash(awk) # - Bash(cat /tmp/gh-aw/cache-memory/) # - Bash(cat > /tmp/gh-aw/cache-memory/) # - Bash(cat go.mod) @@ -1001,7 +1002,7 @@ jobs: fi # shellcheck disable=SC1003 sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --tty --env-all --exclude-env ANTHROPIC_API_KEY --exclude-env GH_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull --difc-proxy-host host.docker.internal:18443 --difc-proxy-ca-cert /tmp/gh-aw/difc-proxy-tls/ca.crt \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/claude_harness.cjs claude --print --no-chrome --allowed-tools '\''Bash(cat /tmp/gh-aw/cache-memory/),Bash(cat > /tmp/gh-aw/cache-memory/),Bash(cat go.mod),Bash(cat go.sum),Bash(cat pkg/**/*.go),Bash(cat scratchpad/mods/*),Bash(cat),Bash(date),Bash(echo),Bash(find pkg -name "*.go" ! -name "*_test.go" -type f),Bash(find pkg -name "*.go"),Bash(find pkg -type f -name "*.go" ! -name "*_test.go"),Bash(find pkg/ -maxdepth 1 -ls),Bash(find pkg/workflow/ -maxdepth 1 -ls),Bash(find scratchpad/mods/ -maxdepth 1 -ls),Bash(gh:*),Bash(go list -m all),Bash(grep -r "func " pkg --include="*.go"),Bash(grep -r "import" --include="*.go"),Bash(grep),Bash(head -n * pkg/**/*.go),Bash(head),Bash(ls),Bash(mkdir -p /tmp/gh-aw/cache-memory/),Bash(mv /tmp/gh-aw/cache-memory/),Bash(printf),Bash(pwd),Bash(safeoutputs:*),Bash(serena:*),Bash(sort),Bash(tail),Bash(uniq),Bash(wc -l pkg/**/*.go),Bash(wc),Bash(yq),BashOutput,Edit,Edit(/tmp/*),Edit(/tmp/gh-aw/agent/*),Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,MultiEdit(/tmp/*),MultiEdit(/tmp/gh-aw/agent/*),MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookEdit,NotebookRead,Read,Read(/tmp/*),Read(/tmp/gh-aw/agent/*),Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,Write,Write(/tmp/*),Write(/tmp/gh-aw/agent/*),Write(/tmp/gh-aw/cache-memory/*),mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__issue_read,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users,mcp__safeoutputs,mcp__serena'\'' --debug-file /tmp/gh-aw/agent-stdio.log --verbose --permission-mode acceptEdits --output-format stream-json --mcp-config "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt${GH_AW_MODEL_AGENT_CLAUDE:+ --model "$GH_AW_MODEL_AGENT_CLAUDE"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/claude_harness.cjs claude --print --no-chrome --allowed-tools '\''Bash(awk),Bash(cat /tmp/gh-aw/cache-memory/),Bash(cat > /tmp/gh-aw/cache-memory/),Bash(cat go.mod),Bash(cat go.sum),Bash(cat pkg/**/*.go),Bash(cat scratchpad/mods/*),Bash(cat),Bash(date),Bash(echo),Bash(find pkg -name "*.go" ! -name "*_test.go" -type f),Bash(find pkg -name "*.go"),Bash(find pkg -type f -name "*.go" ! -name "*_test.go"),Bash(find pkg/ -maxdepth 1 -ls),Bash(find pkg/workflow/ -maxdepth 1 -ls),Bash(find scratchpad/mods/ -maxdepth 1 -ls),Bash(gh:*),Bash(go list -m all),Bash(grep -r "func " pkg --include="*.go"),Bash(grep -r "import" --include="*.go"),Bash(grep),Bash(head -n * pkg/**/*.go),Bash(head),Bash(ls),Bash(mkdir -p /tmp/gh-aw/cache-memory/),Bash(mv /tmp/gh-aw/cache-memory/),Bash(printf),Bash(pwd),Bash(safeoutputs:*),Bash(serena:*),Bash(sort),Bash(tail),Bash(uniq),Bash(wc -l pkg/**/*.go),Bash(wc),Bash(yq),BashOutput,Edit,Edit(/tmp/*),Edit(/tmp/gh-aw/agent/*),Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,MultiEdit(/tmp/*),MultiEdit(/tmp/gh-aw/agent/*),MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookEdit,NotebookRead,Read,Read(/tmp/*),Read(/tmp/gh-aw/agent/*),Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,Write,Write(/tmp/*),Write(/tmp/gh-aw/agent/*),Write(/tmp/gh-aw/cache-memory/*),mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__issue_read,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users,mcp__safeoutputs,mcp__serena'\'' --debug-file /tmp/gh-aw/agent-stdio.log --verbose --permission-mode acceptEdits --output-format stream-json --mcp-config "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt${GH_AW_MODEL_AGENT_CLAUDE:+ --model "$GH_AW_MODEL_AGENT_CLAUDE"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} BASH_DEFAULT_TIMEOUT_MS: 60000 diff --git a/.github/workflows/spec-librarian.lock.yml b/.github/workflows/spec-librarian.lock.yml index 8cd0579a224..a08617b5fb9 100644 --- a/.github/workflows/spec-librarian.lock.yml +++ b/.github/workflows/spec-librarian.lock.yml @@ -825,6 +825,7 @@ jobs: # --allow-tool github # --allow-tool safeoutputs # --allow-tool serena + # --allow-tool shell(awk) # --allow-tool shell(cat pkg/**/*.go) # --allow-tool shell(cat pkg/*/*.go) # --allow-tool shell(cat pkg/*/README.md) @@ -916,7 +917,7 @@ jobs: fi # shellcheck disable=SC1003 sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GH_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull --difc-proxy-host host.docker.internal:18443 --difc-proxy-ca-cert /tmp/gh-aw/difc-proxy-tls/ca.crt \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-tool github --allow-tool safeoutputs --allow-tool serena --allow-tool '\''shell(cat pkg/**/*.go)'\'' --allow-tool '\''shell(cat pkg/*/*.go)'\'' --allow-tool '\''shell(cat pkg/*/README.md)'\'' --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(find pkg -maxdepth 1 -type d)'\'' --allow-tool '\''shell(find pkg -name "*.go" ! -name "*_test.go" -type f)'\'' --allow-tool '\''shell(find pkg -name "README.md" -type f)'\'' --allow-tool '\''shell(find pkg -type f -name "*.go" ! -name "*_test.go")'\'' --allow-tool '\''shell(find pkg/ -maxdepth 1 -ls)'\'' --allow-tool '\''shell(find pkg/* -maxdepth 0 -type d)'\'' --allow-tool '\''shell(find pkg/workflow/ -maxdepth 1 -ls)'\'' --allow-tool '\''shell(gh:*)'\'' --allow-tool '\''shell(git log --oneline --since="30 days ago" -- pkg/*)'\'' --allow-tool '\''shell(git log --oneline --since="7 days ago" -- pkg/*/README.md)'\'' --allow-tool '\''shell(git log -1 --format=%H -- pkg/*)'\'' --allow-tool '\''shell(grep -r "func " pkg --include="*.go")'\'' --allow-tool '\''shell(grep -rn "const [A-Z]" pkg --include="*.go")'\'' --allow-tool '\''shell(grep -rn "func [A-Z]" pkg --include="*.go")'\'' --allow-tool '\''shell(grep -rn "import " pkg --include="*.go")'\'' --allow-tool '\''shell(grep -rn "package " pkg --include="*.go")'\'' --allow-tool '\''shell(grep -rn "type [A-Z]" pkg --include="*.go")'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head -n * pkg/**/*.go)'\'' --allow-tool '\''shell(head -n * pkg/*/*.go)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(printf)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(safeoutputs:*)'\'' --allow-tool '\''shell(serena:*)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc -l pkg/**/*.go)'\'' --allow-tool '\''shell(wc -l pkg/*/*.go)'\'' --allow-tool '\''shell(wc -l pkg/*/README.md)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-tool github --allow-tool safeoutputs --allow-tool serena --allow-tool '\''shell(awk)'\'' --allow-tool '\''shell(cat pkg/**/*.go)'\'' --allow-tool '\''shell(cat pkg/*/*.go)'\'' --allow-tool '\''shell(cat pkg/*/README.md)'\'' --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(find pkg -maxdepth 1 -type d)'\'' --allow-tool '\''shell(find pkg -name "*.go" ! -name "*_test.go" -type f)'\'' --allow-tool '\''shell(find pkg -name "README.md" -type f)'\'' --allow-tool '\''shell(find pkg -type f -name "*.go" ! -name "*_test.go")'\'' --allow-tool '\''shell(find pkg/ -maxdepth 1 -ls)'\'' --allow-tool '\''shell(find pkg/* -maxdepth 0 -type d)'\'' --allow-tool '\''shell(find pkg/workflow/ -maxdepth 1 -ls)'\'' --allow-tool '\''shell(gh:*)'\'' --allow-tool '\''shell(git log --oneline --since="30 days ago" -- pkg/*)'\'' --allow-tool '\''shell(git log --oneline --since="7 days ago" -- pkg/*/README.md)'\'' --allow-tool '\''shell(git log -1 --format=%H -- pkg/*)'\'' --allow-tool '\''shell(grep -r "func " pkg --include="*.go")'\'' --allow-tool '\''shell(grep -rn "const [A-Z]" pkg --include="*.go")'\'' --allow-tool '\''shell(grep -rn "func [A-Z]" pkg --include="*.go")'\'' --allow-tool '\''shell(grep -rn "import " pkg --include="*.go")'\'' --allow-tool '\''shell(grep -rn "package " pkg --include="*.go")'\'' --allow-tool '\''shell(grep -rn "type [A-Z]" pkg --include="*.go")'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head -n * pkg/**/*.go)'\'' --allow-tool '\''shell(head -n * pkg/*/*.go)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(printf)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(safeoutputs:*)'\'' --allow-tool '\''shell(serena:*)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc -l pkg/**/*.go)'\'' --allow-tool '\''shell(wc -l pkg/*/*.go)'\'' --allow-tool '\''shell(wc -l pkg/*/README.md)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE diff --git a/actions/setup-cli/install.sh b/actions/setup-cli/install.sh index c7a5ed2ffed..1c565c9e893 100755 --- a/actions/setup-cli/install.sh +++ b/actions/setup-cli/install.sh @@ -1,7 +1,7 @@ #!/bin/bash set +o histexpand -# Kept in sync with actions/setup-cli/install.sh — edit this file, then copy to that path. +# Script sync note: install-gh-aw.sh is canonical. actions/setup-cli/install.sh is copied from install-gh-aw.sh. # Script to download and install gh-aw binary for the current OS and architecture # Supports: Linux, macOS (Darwin), FreeBSD, Windows (Git Bash/MSYS/Cygwin) From 457d957d569b70f103bad47f547f0329cddcfdad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:45:15 +0000 Subject: [PATCH 3/7] fix: treat update_issue target:triggering outside issue context as soft skip - Add MCP-level validation in updateIssueHandler to reject update_issue when target is triggering (or unset) and there's no issue context - Belt-and-suspenders: patch update_handler_factory to propagate shouldFail:false from resolveTarget as skipped:true result, preventing any existing NDJSON entries from failing the Process Safe Outputs step - Add tests for both MCP handler validation and factory soft-skip logic Fixes #40017 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/safe_outputs_handlers.cjs | 41 ++++++++++ .../setup/js/safe_outputs_handlers.test.cjs | 82 +++++++++++++++++++ .../setup/js/safe_outputs_tools_loader.cjs | 1 + actions/setup/js/update_handler_factory.cjs | 13 ++- .../setup/js/update_handler_factory.test.cjs | 46 +++++++++++ 5 files changed, 182 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index 0449d0f3351..eabbdbc835a 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -25,6 +25,7 @@ const { sanitizeTitle, applyTitlePrefix } = require("./sanitize_title.cjs"); const { parseDeduplicateByTitle, normalizeTitleForDedup, findDuplicateByTitle } = require("./issue_title_dedup.cjs"); const { validateCreatePullRequestIntent, validatePushToPullRequestBranchIntent, validateCreateIssueIntent, validateAddCommentIntent } = require("./intent_probe.cjs"); const { globPatternToRegex } = require("./glob_pattern_helpers.cjs"); +const { resolveInvocationContext } = require("./invocation_context_helpers.cjs"); /** * Read and parse a JSON file. * @param {string} filePath @@ -1857,6 +1858,45 @@ function createHandlers(server, appendSafeOutput, config = {}) { }; }; + /** + * Handler for update_issue tool + * Spec cross-reference: Safe Output Outcome Evaluation §update_issue. + * Per Safe Outputs Specification MCE1: Enforces context constraints during tool invocation + * to provide immediate feedback to the LLM before recording to NDJSON. + * Rejects `target: triggering` (the default) when the workflow has no issue context + * (e.g. on schedule or push events), so the agent receives an actionable error + * instead of a downstream Process Safe Outputs failure. + */ + const updateIssueHandler = args => { + const updateIssueConfig = getSafeOutputsToolConfig(config, "update_issue"); + const effectiveTarget = updateIssueConfig.target || "triggering"; + + if (effectiveTarget === "triggering") { + let invocationContext; + try { + invocationContext = resolveInvocationContext(context); + } catch { + // If context resolution fails fall through and let the downstream handler deal with it. + return defaultHandler("update_issue")(args || {}); + } + const effectiveEventName = invocationContext?.eventName || context.eventName; + const effectivePayload = invocationContext?.eventPayload || context.payload; + const isIssueCommentOnPR = effectiveEventName === "issue_comment" && Boolean(effectivePayload?.issue?.pull_request); + const isIssueContext = effectiveEventName === "issues" || (effectiveEventName === "issue_comment" && !isIssueCommentOnPR); + + if (!isIssueContext) { + return buildIntentErrorResponse( + `update_issue requires an issue context but the workflow is running on a "${effectiveEventName}" event. ` + + `The update-issue handler uses target: triggering which only applies when an issue triggered the workflow. ` + + `To report results from this workflow, use create_discussion or create_issue instead. ` + + `If you need to update a specific issue, the workflow must configure update-issue: target: '*' and you must supply issue_number.` + ); + } + } + + return defaultHandler("update_issue")(args || {}); + }; + /** * Handler for update_pull_request tool * Spec cross-reference: Safe Output Outcome Evaluation §update_pull_request. @@ -1888,6 +1928,7 @@ function createHandlers(server, appendSafeOutput, config = {}) { addCommentHandler, createPullRequestReviewCommentHandler, submitPullRequestReviewHandler, + updateIssueHandler, updatePullRequestHandler, }; } diff --git a/actions/setup/js/safe_outputs_handlers.test.cjs b/actions/setup/js/safe_outputs_handlers.test.cjs index 27375fac868..f7f22b54a54 100644 --- a/actions/setup/js/safe_outputs_handlers.test.cjs +++ b/actions/setup/js/safe_outputs_handlers.test.cjs @@ -2318,6 +2318,88 @@ describe("safe_outputs_handlers", () => { } }); }); + + describe("updateIssueHandler", () => { + it("should return intent error when target is triggering (default) and not in issue context", () => { + // global.context has eventName: "push" (not an issue context) + const result = handlers.updateIssueHandler({ body: "Updated body" }); + expect(result.isError).toBe(true); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("error"); + expect(responseData.error).toContain("update_issue"); + expect(responseData.error).toContain('"push"'); + expect(mockAppendSafeOutput).not.toHaveBeenCalled(); + }); + + it("should return intent error on schedule event with default target", () => { + const savedContext = global.context; + global.context = { ...global.context, eventName: "schedule", payload: {} }; + try { + const result = handlers.updateIssueHandler({ body: "Report" }); + expect(result.isError).toBe(true); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("error"); + expect(responseData.error).toContain('"schedule"'); + expect(responseData.error).toContain("create_discussion"); + expect(mockAppendSafeOutput).not.toHaveBeenCalled(); + } finally { + global.context = savedContext; + } + }); + + it("should write entry and return success when in issue context with default target", () => { + const savedContext = global.context; + global.context = { ...global.context, eventName: "issues", payload: { issue: { number: 42 } } }; + try { + const result = handlers.updateIssueHandler({ body: "Updated body" }); + expect(result.isError).toBeUndefined(); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("success"); + expect(mockAppendSafeOutput).toHaveBeenCalledWith(expect.objectContaining({ type: "update_issue", body: "Updated body" })); + } finally { + global.context = savedContext; + } + }); + + it("should write entry and return success when target is '*' regardless of non-issue context", () => { + // global.context has eventName: "push" (not an issue context) but target is '*' + const wildcardHandlers = createHandlers(mockServer, mockAppendSafeOutput, { + "update-issue": { target: "*" }, + }); + const result = wildcardHandlers.updateIssueHandler({ issue_number: 42, body: "Updated body" }); + expect(result.isError).toBeUndefined(); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("success"); + expect(mockAppendSafeOutput).toHaveBeenCalledWith(expect.objectContaining({ type: "update_issue", issue_number: 42, body: "Updated body" })); + }); + + it("should write entry and return success on workflow_dispatch with issue aw_context", () => { + const savedContext = global.context; + global.context = { + ...global.context, + eventName: "workflow_dispatch", + payload: { + inputs: { + aw_context: JSON.stringify({ + event_type: "issue_comment", + item_type: "issue", + item_number: 99, + repo: "test-owner/test-repo", + }), + }, + }, + }; + try { + const result = handlers.updateIssueHandler({ body: "Issue update from dispatch" }); + expect(result.isError).toBeUndefined(); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("success"); + expect(mockAppendSafeOutput).toHaveBeenCalledWith(expect.objectContaining({ type: "update_issue" })); + } finally { + global.context = savedContext; + } + }); + }); }); describe("hasUpdatePullRequestFields", () => { diff --git a/actions/setup/js/safe_outputs_tools_loader.cjs b/actions/setup/js/safe_outputs_tools_loader.cjs index 76d9fcc9805..0781349bb3c 100644 --- a/actions/setup/js/safe_outputs_tools_loader.cjs +++ b/actions/setup/js/safe_outputs_tools_loader.cjs @@ -130,6 +130,7 @@ function attachHandlers(tools, handlers, logger) { add_comment: handlers.addCommentHandler, create_pull_request_review_comment: handlers.createPullRequestReviewCommentHandler, submit_pull_request_review: handlers.submitPullRequestReviewHandler, + update_issue: handlers.updateIssueHandler, update_pull_request: handlers.updatePullRequestHandler, }; diff --git a/actions/setup/js/update_handler_factory.cjs b/actions/setup/js/update_handler_factory.cjs index 35cac8684c0..e635a4650e0 100644 --- a/actions/setup/js/update_handler_factory.cjs +++ b/actions/setup/js/update_handler_factory.cjs @@ -72,7 +72,7 @@ function createStandardResolveNumber(config) { }); if (!targetResult.success) { - return { success: false, error: targetResult.error }; + return { success: false, shouldFail: targetResult.shouldFail, error: targetResult.error }; } return { success: true, number: targetResult.number }; @@ -195,6 +195,17 @@ function createUpdateHandlerFactory(handlerConfig) { const itemNumberResult = resolveItemNumber(item, updateTarget, effectiveContext, resolvedTemporaryIds); if (!itemNumberResult.success) { + // shouldFail:false means the target cannot be resolved in this context but it is not + // an error (e.g. target:triggering on a schedule run has no triggering issue). + // Treat it as a soft skip so it does not count toward fatal failures. + if (itemNumberResult.shouldFail === false) { + core.info(itemNumberResult.error); + return { + success: false, + skipped: true, + error: itemNumberResult.error, + }; + } core.warning(itemNumberResult.error); return { success: false, diff --git a/actions/setup/js/update_handler_factory.test.cjs b/actions/setup/js/update_handler_factory.test.cjs index 0b3c55517c6..e59ef8944ff 100644 --- a/actions/setup/js/update_handler_factory.test.cjs +++ b/actions/setup/js/update_handler_factory.test.cjs @@ -481,6 +481,23 @@ describe("update_handler_factory.cjs", () => { expect(result.error).toBeDefined(); }); + it("should propagate shouldFail:false when not in issue context (schedule event)", async () => { + const resolveNumber = factoryModule.createStandardResolveNumber({ + itemType: "update_issue", + itemNumberField: "issue_number", + supportsPR: false, + supportsIssue: true, + }); + + // schedule event — no issue context, resolveTarget returns shouldFail:false + const scheduleContext = { ...mockContext, eventName: "schedule", payload: {} }; + const result = resolveNumber({}, "triggering", scheduleContext); + + expect(result.success).toBe(false); + expect(result.shouldFail).toBe(false); + expect(result.error).toContain("issue context"); + }); + it("should resolve temporary ID in issue_number field", async () => { const resolveNumber = factoryModule.createStandardResolveNumber({ itemType: "update_issue", @@ -884,6 +901,35 @@ describe("update_handler_factory.cjs", () => { expect(mockExecuteUpdate).not.toHaveBeenCalled(); }); + it("should return skipped result when resolveItemNumber returns shouldFail:false", async () => { + const mockExecuteUpdate = vi.fn(); + + const handlerFactory = factoryModule.createUpdateHandlerFactory({ + itemType: "update_issue", + itemTypeName: "issue", + supportsPR: false, + resolveItemNumber: vi.fn().mockReturnValue({ + success: false, + shouldFail: false, + error: 'Target is "triggering" but not running in issue context, skipping update_issue', + }), + buildUpdateData: vi.fn(), + executeUpdate: mockExecuteUpdate, + formatSuccessResult: vi.fn(), + }); + + const handler = await handlerFactory({}); + const result = await handler({ body: "Report body" }); + + expect(result.success).toBe(false); + expect(result.skipped).toBe(true); + expect(result.error).toContain("issue context"); + expect(mockExecuteUpdate).not.toHaveBeenCalled(); + // Should be logged as info (not error/warning) + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("issue context")); + expect(mockCore.warning).not.toHaveBeenCalledWith(expect.stringContaining("issue context")); + }); + it("should call itemFilter and skip update when filter returns a result", async () => { const mockExecuteUpdate = vi.fn(); const mockItemFilter = vi.fn().mockResolvedValue({ From 2c0a5d18ef9991c7cac8400e27019daaf3c988a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 12:22:36 +0000 Subject: [PATCH 4/7] fix: add MCP-level target:triggering context validation for add_comment and update_pull_request Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/safe_outputs_handlers.cjs | 61 ++++ .../setup/js/safe_outputs_handlers.test.cjs | 331 ++++++++++++++---- actions/setup/setup.sh | 1 + 3 files changed, 331 insertions(+), 62 deletions(-) diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index eabbdbc835a..5b5e21f8ade 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -1640,6 +1640,38 @@ function createHandlers(server, appendSafeOutput, config = {}) { ); } + // Reject target:triggering early when no explicit item number and no issue/PR/discussion context. + // Per Safe Outputs Specification MCE1: provides actionable feedback before writing to NDJSON. + // Mirrors update_issue validation; explicit item_number bypasses this check because the + // downstream handler resolves explicit numbers before falling back to triggering context. + const effectiveAddCommentTarget = addCommentConfig.target || "triggering"; + const hasExplicitItemNumber = args?.item_number != null || args?.issue_number != null || args?.["pr-number"] != null; + if (effectiveAddCommentTarget === "triggering" && !hasExplicitItemNumber) { + let addCommentInvocationContext; + try { + addCommentInvocationContext = resolveInvocationContext(context); + } catch { + // Context resolution failed; skip validation and let downstream handle gracefully + } + if (addCommentInvocationContext !== undefined) { + const effectiveEventName = addCommentInvocationContext?.eventName || context.eventName; + const effectivePayload = addCommentInvocationContext?.eventPayload || context.payload; + const prEventNames = new Set(["pull_request", "pull_request_target", "pull_request_review", "pull_request_review_comment"]); + const isIssueCommentOnPR = effectiveEventName === "issue_comment" && Boolean(effectivePayload?.issue?.pull_request); + const isIssueContext = effectiveEventName === "issues" || (effectiveEventName === "issue_comment" && !isIssueCommentOnPR); + const isPRContext = prEventNames.has(effectiveEventName) || isIssueCommentOnPR; + const isDiscussionContext = effectiveEventName === "discussion" || effectiveEventName === "discussion_comment"; + if (!isIssueContext && !isPRContext && !isDiscussionContext) { + return buildIntentErrorResponse( + `add_comment requires an issue, pull request, or discussion context but the workflow is running on a "${effectiveEventName}" event. ` + + `The add-comment handler uses target: triggering which only applies when an issue, pull request, or discussion triggered the workflow. ` + + `To report results from this workflow, use create_discussion or create_issue instead. ` + + `If you need to comment on a specific item, provide an explicit item_number.` + ); + } + } + } + // Build the entry with a temporary_id const entry = { ...(args || {}), type: "add_comment" }; const wildcardTargetValidationError = validateWildcardTargetRequirement(entry); @@ -1904,6 +1936,9 @@ function createHandlers(server, appendSafeOutput, config = {}) { * to provide immediate feedback to the LLM before recording to NDJSON. * Uses hasUpdatePullRequestFields to validate that at least one of 'title', 'body', * or 'update_branch' is provided before recording to NDJSON. + * Rejects `target: triggering` (the default) when the workflow has no pull request context + * (e.g. on schedule or push events), so the agent receives an actionable error + * instead of a downstream Process Safe Outputs failure. */ const updatePullRequestHandler = args => { if (!hasUpdatePullRequestFields(args)) { @@ -1913,6 +1948,32 @@ function createHandlers(server, appendSafeOutput, config = {}) { }; } + const updatePRConfig = getSafeOutputsToolConfig(config, "update_pull_request"); + const effectivePRTarget = updatePRConfig.target || "triggering"; + if (effectivePRTarget === "triggering") { + let invocationContext; + try { + invocationContext = resolveInvocationContext(context); + } catch { + // If context resolution fails fall through and let the downstream handler deal with it. + return defaultHandler("update_pull_request")(args || {}); + } + const effectiveEventName = invocationContext?.eventName || context.eventName; + const effectivePayload = invocationContext?.eventPayload || context.payload; + const prEventNames = new Set(["pull_request", "pull_request_target", "pull_request_review", "pull_request_review_comment"]); + const isIssueCommentOnPR = effectiveEventName === "issue_comment" && Boolean(effectivePayload?.issue?.pull_request); + const isPRContext = prEventNames.has(effectiveEventName) || isIssueCommentOnPR; + + if (!isPRContext) { + return buildIntentErrorResponse( + `update_pull_request requires a pull request context but the workflow is running on a "${effectiveEventName}" event. ` + + `The update-pull-request handler uses target: triggering which only applies when a pull request triggered the workflow. ` + + `To report results from this workflow, use create_discussion or create_issue instead. ` + + `If you need to update a specific pull request, the workflow must configure update-pull-request: target: '*' and you must supply pull_request_number.` + ); + } + } + return defaultHandler("update_pull_request")(args || {}); }; diff --git a/actions/setup/js/safe_outputs_handlers.test.cjs b/actions/setup/js/safe_outputs_handlers.test.cjs index f7f22b54a54..d238e1aefc6 100644 --- a/actions/setup/js/safe_outputs_handlers.test.cjs +++ b/actions/setup/js/safe_outputs_handlers.test.cjs @@ -1672,41 +1672,65 @@ describe("safe_outputs_handlers", () => { describe("addCommentHandler", () => { it("should auto-generate a temporary_id when not provided", () => { - const result = handlers.addCommentHandler({ body: "Valid comment body" }); + const savedContext = global.context; + global.context = { ...global.context, eventName: "issues", payload: { issue: { number: 5 } } }; + try { + const result = handlers.addCommentHandler({ body: "Valid comment body" }); - expect(result).toHaveProperty("content"); - const responseData = JSON.parse(result.content[0].text); - expect(responseData.result).toBe("success"); - expect(responseData.temporary_id).toBeDefined(); - expect(responseData.temporary_id).toMatch(/^aw_[A-Za-z0-9]{3,12}$/); + expect(result).toHaveProperty("content"); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("success"); + expect(responseData.temporary_id).toBeDefined(); + expect(responseData.temporary_id).toMatch(/^aw_[A-Za-z0-9]{3,12}$/); + } finally { + global.context = savedContext; + } }); it("should use the provided temporary_id when given", () => { - const result = handlers.addCommentHandler({ body: "Valid comment body", temporary_id: "aw_abc123" }); + const savedContext = global.context; + global.context = { ...global.context, eventName: "issues", payload: { issue: { number: 5 } } }; + try { + const result = handlers.addCommentHandler({ body: "Valid comment body", temporary_id: "aw_abc123" }); - expect(result).toHaveProperty("content"); - const responseData = JSON.parse(result.content[0].text); - expect(responseData.result).toBe("success"); - expect(responseData.temporary_id).toBe("aw_abc123"); + expect(result).toHaveProperty("content"); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("success"); + expect(responseData.temporary_id).toBe("aw_abc123"); + } finally { + global.context = savedContext; + } }); it("should return comment reference using temporary_id", () => { - const result = handlers.addCommentHandler({ body: "Valid comment body" }); + const savedContext = global.context; + global.context = { ...global.context, eventName: "issues", payload: { issue: { number: 5 } } }; + try { + const result = handlers.addCommentHandler({ body: "Valid comment body" }); - const responseData = JSON.parse(result.content[0].text); - expect(responseData.comment).toBe(`#${responseData.temporary_id}`); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.comment).toBe(`#${responseData.temporary_id}`); + } finally { + global.context = savedContext; + } }); it("should record the temporary_id in the NDJSON entry", () => { - handlers.addCommentHandler({ body: "Valid comment body", temporary_id: "aw_test01" }); + const savedContext = global.context; + global.context = { ...global.context, eventName: "issues", payload: { issue: { number: 5 } } }; + try { + handlers.addCommentHandler({ body: "Valid comment body", temporary_id: "aw_test01" }); - expect(mockAppendSafeOutput).toHaveBeenCalledWith( - expect.objectContaining({ - type: "add_comment", - body: "Valid comment body", - temporary_id: "aw_test01", - }) - ); + expect(mockAppendSafeOutput).toHaveBeenCalledWith( + expect.objectContaining({ + type: "add_comment", + body: "Valid comment body", + temporary_id: "aw_test01", + }) + ); + } finally { + global.context = savedContext; + } }); it("should throw validation error for oversized comment body", () => { @@ -1716,14 +1740,20 @@ describe("safe_outputs_handlers", () => { }); it("should reject obvious exploratory placeholder comments before recording them", () => { - const result = handlers.addCommentHandler({ body: "test" }); + const savedContext = global.context; + global.context = { ...global.context, eventName: "issues", payload: { issue: { number: 5 } } }; + try { + const result = handlers.addCommentHandler({ body: "test" }); - expect(result.isError).toBe(true); - const responseData = JSON.parse(result.content[0].text); - expect(responseData.result).toBe("error"); - expect(responseData.error).toContain("Refusing to record an exploratory comment"); - expect(responseData.error).toContain("noop or report_incomplete"); - expect(mockAppendSafeOutput).not.toHaveBeenCalled(); + expect(result.isError).toBe(true); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("error"); + expect(responseData.error).toContain("Refusing to record an exploratory comment"); + expect(responseData.error).toContain("noop or report_incomplete"); + expect(mockAppendSafeOutput).not.toHaveBeenCalled(); + } finally { + global.context = savedContext; + } }); it("should require explicit item_number when add_comment target is '*'", () => { @@ -1744,6 +1774,7 @@ describe("safe_outputs_handlers", () => { it("should refuse reply_to_id when discussions are not enabled in config", () => { // Default handlers have no discussions: true in config + // Discussion check precedes context check so this error surfaces regardless of event context const result = handlers.addCommentHandler({ body: "Reply to a discussion thread", reply_to_id: "DC_kwDOABcD1M4AaBbC", @@ -1759,24 +1790,110 @@ describe("safe_outputs_handlers", () => { }); it("should allow reply_to_id when discussions are enabled in config", () => { - const discussionHandlers = createHandlers(mockServer, mockAppendSafeOutput, { - "add-comment": { enabled: true, discussions: true }, - }); + const savedContext = global.context; + global.context = { ...global.context, eventName: "discussion", payload: { discussion: { number: 3 } } }; + try { + const discussionHandlers = createHandlers(mockServer, mockAppendSafeOutput, { + "add-comment": { enabled: true, discussions: true }, + }); - const result = discussionHandlers.addCommentHandler({ - body: "Reply to a discussion thread with real content that is not a test placeholder", - reply_to_id: "DC_kwDOABcD1M4AaBbC", - }); + const result = discussionHandlers.addCommentHandler({ + body: "Reply to a discussion thread with real content that is not a test placeholder", + reply_to_id: "DC_kwDOABcD1M4AaBbC", + }); + + expect(result.isError).toBeUndefined(); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("success"); + expect(mockAppendSafeOutput).toHaveBeenCalledWith( + expect.objectContaining({ + type: "add_comment", + reply_to_id: "DC_kwDOABcD1M4AaBbC", + }) + ); + } finally { + global.context = savedContext; + } + }); + + it("should return intent error when target is triggering (default) and not in issue/PR/discussion context", () => { + // global.context has eventName: "push" (no issue/PR/discussion context) + const result = handlers.addCommentHandler({ body: "A real comment body that is substantive" }); + expect(result.isError).toBe(true); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("error"); + expect(responseData.error).toContain("add_comment"); + expect(responseData.error).toContain('"push"'); + expect(mockAppendSafeOutput).not.toHaveBeenCalled(); + }); + + it("should return intent error on schedule event with default target", () => { + const savedContext = global.context; + global.context = { ...global.context, eventName: "schedule", payload: {} }; + try { + const result = handlers.addCommentHandler({ body: "A real comment body that is substantive" }); + expect(result.isError).toBe(true); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("error"); + expect(responseData.error).toContain('"schedule"'); + expect(responseData.error).toContain("create_discussion"); + expect(mockAppendSafeOutput).not.toHaveBeenCalled(); + } finally { + global.context = savedContext; + } + }); + + it("should write entry when target is triggering and in PR context", () => { + const savedContext = global.context; + global.context = { ...global.context, eventName: "pull_request", payload: { pull_request: { number: 7 } } }; + try { + const result = handlers.addCommentHandler({ body: "A real comment body for this pull request" }); + expect(result.isError).toBeUndefined(); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("success"); + expect(mockAppendSafeOutput).toHaveBeenCalledWith(expect.objectContaining({ type: "add_comment" })); + } finally { + global.context = savedContext; + } + }); + it("should write entry when explicit item_number bypasses context check in non-issue/PR event", () => { + // push context but explicit item_number provided — downstream handles it directly + const result = handlers.addCommentHandler({ + body: "A real comment body that is substantive enough", + item_number: 42, + }); expect(result.isError).toBeUndefined(); const responseData = JSON.parse(result.content[0].text); expect(responseData.result).toBe("success"); - expect(mockAppendSafeOutput).toHaveBeenCalledWith( - expect.objectContaining({ - type: "add_comment", - reply_to_id: "DC_kwDOABcD1M4AaBbC", - }) - ); + expect(mockAppendSafeOutput).toHaveBeenCalledWith(expect.objectContaining({ type: "add_comment", item_number: 42 })); + }); + + it("should write entry on workflow_dispatch with issue aw_context", () => { + const savedContext = global.context; + global.context = { + ...global.context, + eventName: "workflow_dispatch", + payload: { + inputs: { + aw_context: JSON.stringify({ + event_type: "issue_comment", + item_type: "issue", + item_number: 99, + repo: "test-owner/test-repo", + }), + }, + }, + }; + try { + const result = handlers.addCommentHandler({ body: "Comment from dispatch with real content" }); + expect(result.isError).toBeUndefined(); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("success"); + expect(mockAppendSafeOutput).toHaveBeenCalledWith(expect.objectContaining({ type: "add_comment" })); + } finally { + global.context = savedContext; + } }); }); @@ -2276,35 +2393,59 @@ describe("safe_outputs_handlers", () => { }); it("should write entry and return success when title is provided", () => { - const result = handlers.updatePullRequestHandler({ title: "New Title" }); - expect(result).toHaveProperty("content"); - const data = JSON.parse(result.content[0].text); - expect(data.result).toBe("success"); - expect(mockAppendSafeOutput).toHaveBeenCalledWith(expect.objectContaining({ type: "update_pull_request", title: "New Title" })); + const savedContext = global.context; + global.context = { ...global.context, eventName: "pull_request", payload: { pull_request: { number: 7 } } }; + try { + const result = handlers.updatePullRequestHandler({ title: "New Title" }); + expect(result).toHaveProperty("content"); + const data = JSON.parse(result.content[0].text); + expect(data.result).toBe("success"); + expect(mockAppendSafeOutput).toHaveBeenCalledWith(expect.objectContaining({ type: "update_pull_request", title: "New Title" })); + } finally { + global.context = savedContext; + } }); it("should write entry and return success when body is provided", () => { - const result = handlers.updatePullRequestHandler({ body: "Updated body" }); - expect(result).toHaveProperty("content"); - const data = JSON.parse(result.content[0].text); - expect(data.result).toBe("success"); - expect(mockAppendSafeOutput).toHaveBeenCalledWith(expect.objectContaining({ type: "update_pull_request", body: "Updated body" })); + const savedContext = global.context; + global.context = { ...global.context, eventName: "pull_request", payload: { pull_request: { number: 7 } } }; + try { + const result = handlers.updatePullRequestHandler({ body: "Updated body" }); + expect(result).toHaveProperty("content"); + const data = JSON.parse(result.content[0].text); + expect(data.result).toBe("success"); + expect(mockAppendSafeOutput).toHaveBeenCalledWith(expect.objectContaining({ type: "update_pull_request", body: "Updated body" })); + } finally { + global.context = savedContext; + } }); it("should write entry and return success when update_branch is true", () => { - const result = handlers.updatePullRequestHandler({ update_branch: true }); - expect(result).toHaveProperty("content"); - const data = JSON.parse(result.content[0].text); - expect(data.result).toBe("success"); - expect(mockAppendSafeOutput).toHaveBeenCalledWith(expect.objectContaining({ type: "update_pull_request", update_branch: true })); + const savedContext = global.context; + global.context = { ...global.context, eventName: "pull_request", payload: { pull_request: { number: 7 } } }; + try { + const result = handlers.updatePullRequestHandler({ update_branch: true }); + expect(result).toHaveProperty("content"); + const data = JSON.parse(result.content[0].text); + expect(data.result).toBe("success"); + expect(mockAppendSafeOutput).toHaveBeenCalledWith(expect.objectContaining({ type: "update_pull_request", update_branch: true })); + } finally { + global.context = savedContext; + } }); it("should write entry and return success when both title and body are provided", () => { - const result = handlers.updatePullRequestHandler({ title: "New Title", body: "New body" }); - expect(result).toHaveProperty("content"); - const data = JSON.parse(result.content[0].text); - expect(data.result).toBe("success"); - expect(mockAppendSafeOutput).toHaveBeenCalledWith(expect.objectContaining({ type: "update_pull_request", title: "New Title", body: "New body" })); + const savedContext = global.context; + global.context = { ...global.context, eventName: "pull_request", payload: { pull_request: { number: 7 } } }; + try { + const result = handlers.updatePullRequestHandler({ title: "New Title", body: "New body" }); + expect(result).toHaveProperty("content"); + const data = JSON.parse(result.content[0].text); + expect(data.result).toBe("success"); + expect(mockAppendSafeOutput).toHaveBeenCalledWith(expect.objectContaining({ type: "update_pull_request", title: "New Title", body: "New body" })); + } finally { + global.context = savedContext; + } }); it("error message should mention all required fields", () => { @@ -2317,6 +2458,72 @@ describe("safe_outputs_handlers", () => { expect(err.message).toContain("'update_branch'"); } }); + + it("should return intent error when target is triggering (default) and not in PR context", () => { + // global.context has eventName: "push" (not a PR context) + const result = handlers.updatePullRequestHandler({ title: "Update title" }); + expect(result.isError).toBe(true); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("error"); + expect(responseData.error).toContain("update_pull_request"); + expect(responseData.error).toContain('"push"'); + expect(mockAppendSafeOutput).not.toHaveBeenCalled(); + }); + + it("should return intent error on schedule event with default target", () => { + const savedContext = global.context; + global.context = { ...global.context, eventName: "schedule", payload: {} }; + try { + const result = handlers.updatePullRequestHandler({ body: "Report" }); + expect(result.isError).toBe(true); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("error"); + expect(responseData.error).toContain('"schedule"'); + expect(responseData.error).toContain("create_discussion"); + expect(mockAppendSafeOutput).not.toHaveBeenCalled(); + } finally { + global.context = savedContext; + } + }); + + it("should write entry and return success when target is '*' regardless of non-PR context", () => { + // global.context has eventName: "push" (not a PR context) but target is '*' + const wildcardHandlers = createHandlers(mockServer, mockAppendSafeOutput, { + "update-pull-request": { target: "*" }, + }); + const result = wildcardHandlers.updatePullRequestHandler({ pull_request_number: 7, title: "New Title" }); + expect(result.isError).toBeUndefined(); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("success"); + expect(mockAppendSafeOutput).toHaveBeenCalledWith(expect.objectContaining({ type: "update_pull_request", pull_request_number: 7, title: "New Title" })); + }); + + it("should write entry and return success on workflow_dispatch with PR aw_context", () => { + const savedContext = global.context; + global.context = { + ...global.context, + eventName: "workflow_dispatch", + payload: { + inputs: { + aw_context: JSON.stringify({ + event_type: "pull_request", + item_type: "pull_request", + item_number: 7, + repo: "test-owner/test-repo", + }), + }, + }, + }; + try { + const result = handlers.updatePullRequestHandler({ title: "PR update from dispatch" }); + expect(result.isError).toBeUndefined(); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("success"); + expect(mockAppendSafeOutput).toHaveBeenCalledWith(expect.objectContaining({ type: "update_pull_request" })); + } finally { + global.context = savedContext; + } + }); }); describe("updateIssueHandler", () => { diff --git a/actions/setup/setup.sh b/actions/setup/setup.sh index 30dd6c7bd78..514d15b3bb9 100755 --- a/actions/setup/setup.sh +++ b/actions/setup/setup.sh @@ -336,6 +336,7 @@ SAFE_OUTPUTS_FILES=( "levenshtein_distance.cjs" "markdown_code_region_balancer.cjs" "temporary_id.cjs" + "invocation_context_helpers.cjs" ) SAFE_OUTPUTS_COUNT=0 From be30ef56f8ca695284e23e8d87f8a8532843ebc9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 12:25:37 +0000 Subject: [PATCH 5/7] fix: improve test isolation and rename invocationContext for consistency Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/safe_outputs_handlers.cjs | 10 ++--- .../setup/js/safe_outputs_handlers.test.cjs | 42 ++++++++++++------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index 5b5e21f8ade..f8bca8e9636 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -1647,15 +1647,15 @@ function createHandlers(server, appendSafeOutput, config = {}) { const effectiveAddCommentTarget = addCommentConfig.target || "triggering"; const hasExplicitItemNumber = args?.item_number != null || args?.issue_number != null || args?.["pr-number"] != null; if (effectiveAddCommentTarget === "triggering" && !hasExplicitItemNumber) { - let addCommentInvocationContext; + let invocationContext; try { - addCommentInvocationContext = resolveInvocationContext(context); + invocationContext = resolveInvocationContext(context); } catch { // Context resolution failed; skip validation and let downstream handle gracefully } - if (addCommentInvocationContext !== undefined) { - const effectiveEventName = addCommentInvocationContext?.eventName || context.eventName; - const effectivePayload = addCommentInvocationContext?.eventPayload || context.payload; + if (invocationContext !== undefined) { + const effectiveEventName = invocationContext?.eventName || context.eventName; + const effectivePayload = invocationContext?.eventPayload || context.payload; const prEventNames = new Set(["pull_request", "pull_request_target", "pull_request_review", "pull_request_review_comment"]); const isIssueCommentOnPR = effectiveEventName === "issue_comment" && Boolean(effectivePayload?.issue?.pull_request); const isIssueContext = effectiveEventName === "issues" || (effectiveEventName === "issue_comment" && !isIssueCommentOnPR); diff --git a/actions/setup/js/safe_outputs_handlers.test.cjs b/actions/setup/js/safe_outputs_handlers.test.cjs index d238e1aefc6..3a9684d47dd 100644 --- a/actions/setup/js/safe_outputs_handlers.test.cjs +++ b/actions/setup/js/safe_outputs_handlers.test.cjs @@ -1817,14 +1817,19 @@ describe("safe_outputs_handlers", () => { }); it("should return intent error when target is triggering (default) and not in issue/PR/discussion context", () => { - // global.context has eventName: "push" (no issue/PR/discussion context) - const result = handlers.addCommentHandler({ body: "A real comment body that is substantive" }); - expect(result.isError).toBe(true); - const responseData = JSON.parse(result.content[0].text); - expect(responseData.result).toBe("error"); - expect(responseData.error).toContain("add_comment"); - expect(responseData.error).toContain('"push"'); - expect(mockAppendSafeOutput).not.toHaveBeenCalled(); + const savedContext = global.context; + global.context = { ...global.context, eventName: "push", payload: {} }; + try { + const result = handlers.addCommentHandler({ body: "A real comment body that is substantive" }); + expect(result.isError).toBe(true); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("error"); + expect(responseData.error).toContain("add_comment"); + expect(responseData.error).toContain('"push"'); + expect(mockAppendSafeOutput).not.toHaveBeenCalled(); + } finally { + global.context = savedContext; + } }); it("should return intent error on schedule event with default target", () => { @@ -2460,14 +2465,19 @@ describe("safe_outputs_handlers", () => { }); it("should return intent error when target is triggering (default) and not in PR context", () => { - // global.context has eventName: "push" (not a PR context) - const result = handlers.updatePullRequestHandler({ title: "Update title" }); - expect(result.isError).toBe(true); - const responseData = JSON.parse(result.content[0].text); - expect(responseData.result).toBe("error"); - expect(responseData.error).toContain("update_pull_request"); - expect(responseData.error).toContain('"push"'); - expect(mockAppendSafeOutput).not.toHaveBeenCalled(); + const savedContext = global.context; + global.context = { ...global.context, eventName: "push", payload: {} }; + try { + const result = handlers.updatePullRequestHandler({ title: "Update title" }); + expect(result.isError).toBe(true); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("error"); + expect(responseData.error).toContain("update_pull_request"); + expect(responseData.error).toContain('"push"'); + expect(mockAppendSafeOutput).not.toHaveBeenCalled(); + } finally { + global.context = savedContext; + } }); it("should return intent error on schedule event with default target", () => { From 132323ff65fafe55c7a753369a427fc86f420400 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:29:40 +0000 Subject: [PATCH 6/7] fix: address github-actions review feedback on safe_outputs_handlers Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/safe_outputs_handlers.cjs | 60 +++++--- .../setup/js/safe_outputs_handlers.test.cjs | 135 ++++++++++++++++-- 2 files changed, 168 insertions(+), 27 deletions(-) diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index f8bca8e9636..29e0b31d5c4 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -26,6 +26,23 @@ const { parseDeduplicateByTitle, normalizeTitleForDedup, findDuplicateByTitle } const { validateCreatePullRequestIntent, validatePushToPullRequestBranchIntent, validateCreateIssueIntent, validateAddCommentIntent } = require("./intent_probe.cjs"); const { globPatternToRegex } = require("./glob_pattern_helpers.cjs"); const { resolveInvocationContext } = require("./invocation_context_helpers.cjs"); + +/** PR event names used for target:triggering context validation across all safe-output handlers. */ +const PR_EVENT_NAMES = new Set(["pull_request", "pull_request_target", "pull_request_review", "pull_request_review_comment"]); + +/** + * Resolve effective event name and payload from an invocation context, + * falling back to the raw GitHub Actions context. + * @param {ReturnType | null | undefined} invocationContext + * @param {any} rawContext + */ +function resolveEffectiveContext(invocationContext, rawContext) { + return { + effectiveEventName: invocationContext?.eventName || rawContext.eventName, + effectivePayload: invocationContext?.eventPayload || rawContext.payload, + }; +} + /** * Read and parse a JSON file. * @param {string} filePath @@ -1647,19 +1664,21 @@ function createHandlers(server, appendSafeOutput, config = {}) { const effectiveAddCommentTarget = addCommentConfig.target || "triggering"; const hasExplicitItemNumber = args?.item_number != null || args?.issue_number != null || args?.["pr-number"] != null; if (effectiveAddCommentTarget === "triggering" && !hasExplicitItemNumber) { - let invocationContext; + let invocationContext = null; try { invocationContext = resolveInvocationContext(context); - } catch { - // Context resolution failed; skip validation and let downstream handle gracefully + } catch (err) { + // A validation error (e.g. disallowed target_repo / SEC-005) is a real failure — surface it. + if (err?.message?.startsWith(ERR_VALIDATION)) { + return buildIntentErrorResponse(err.message); + } + // Unexpected structural error: skip validation and let downstream handle gracefully. } - if (invocationContext !== undefined) { - const effectiveEventName = invocationContext?.eventName || context.eventName; - const effectivePayload = invocationContext?.eventPayload || context.payload; - const prEventNames = new Set(["pull_request", "pull_request_target", "pull_request_review", "pull_request_review_comment"]); + if (invocationContext != null) { + const { effectiveEventName, effectivePayload } = resolveEffectiveContext(invocationContext, context); const isIssueCommentOnPR = effectiveEventName === "issue_comment" && Boolean(effectivePayload?.issue?.pull_request); const isIssueContext = effectiveEventName === "issues" || (effectiveEventName === "issue_comment" && !isIssueCommentOnPR); - const isPRContext = prEventNames.has(effectiveEventName) || isIssueCommentOnPR; + const isPRContext = PR_EVENT_NAMES.has(effectiveEventName) || isIssueCommentOnPR; const isDiscussionContext = effectiveEventName === "discussion" || effectiveEventName === "discussion_comment"; if (!isIssueContext && !isPRContext && !isDiscussionContext) { return buildIntentErrorResponse( @@ -1907,12 +1926,15 @@ function createHandlers(server, appendSafeOutput, config = {}) { let invocationContext; try { invocationContext = resolveInvocationContext(context); - } catch { - // If context resolution fails fall through and let the downstream handler deal with it. + } catch (err) { + // A validation error (e.g. disallowed target_repo / SEC-005) is a real failure — surface it. + if (err?.message?.startsWith(ERR_VALIDATION)) { + return buildIntentErrorResponse(err.message); + } + // Unexpected structural error: fall through and let the downstream handler deal with it. return defaultHandler("update_issue")(args || {}); } - const effectiveEventName = invocationContext?.eventName || context.eventName; - const effectivePayload = invocationContext?.eventPayload || context.payload; + const { effectiveEventName, effectivePayload } = resolveEffectiveContext(invocationContext, context); const isIssueCommentOnPR = effectiveEventName === "issue_comment" && Boolean(effectivePayload?.issue?.pull_request); const isIssueContext = effectiveEventName === "issues" || (effectiveEventName === "issue_comment" && !isIssueCommentOnPR); @@ -1954,15 +1976,17 @@ function createHandlers(server, appendSafeOutput, config = {}) { let invocationContext; try { invocationContext = resolveInvocationContext(context); - } catch { - // If context resolution fails fall through and let the downstream handler deal with it. + } catch (err) { + // A validation error (e.g. disallowed target_repo / SEC-005) is a real failure — surface it. + if (err?.message?.startsWith(ERR_VALIDATION)) { + return buildIntentErrorResponse(err.message); + } + // Unexpected structural error: fall through and let the downstream handler deal with it. return defaultHandler("update_pull_request")(args || {}); } - const effectiveEventName = invocationContext?.eventName || context.eventName; - const effectivePayload = invocationContext?.eventPayload || context.payload; - const prEventNames = new Set(["pull_request", "pull_request_target", "pull_request_review", "pull_request_review_comment"]); + const { effectiveEventName, effectivePayload } = resolveEffectiveContext(invocationContext, context); const isIssueCommentOnPR = effectiveEventName === "issue_comment" && Boolean(effectivePayload?.issue?.pull_request); - const isPRContext = prEventNames.has(effectiveEventName) || isIssueCommentOnPR; + const isPRContext = PR_EVENT_NAMES.has(effectiveEventName) || isIssueCommentOnPR; if (!isPRContext) { return buildIntentErrorResponse( diff --git a/actions/setup/js/safe_outputs_handlers.test.cjs b/actions/setup/js/safe_outputs_handlers.test.cjs index 3a9684d47dd..934b02a10b2 100644 --- a/actions/setup/js/safe_outputs_handlers.test.cjs +++ b/actions/setup/js/safe_outputs_handlers.test.cjs @@ -1862,16 +1862,39 @@ describe("safe_outputs_handlers", () => { } }); + it("should write entry when issue_comment fires on a PR (valid PR context for add_comment)", () => { + const savedContext = global.context; + global.context = { + ...global.context, + eventName: "issue_comment", + payload: { issue: { number: 7, pull_request: { url: "https://api.github.com/repos/test-owner/test-repo/pulls/7" } } }, + }; + try { + const result = handlers.addCommentHandler({ body: "A real comment body for this PR comment thread" }); + expect(result.isError).toBeUndefined(); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("success"); + expect(mockAppendSafeOutput).toHaveBeenCalledWith(expect.objectContaining({ type: "add_comment" })); + } finally { + global.context = savedContext; + } + }); + it("should write entry when explicit item_number bypasses context check in non-issue/PR event", () => { - // push context but explicit item_number provided — downstream handles it directly - const result = handlers.addCommentHandler({ - body: "A real comment body that is substantive enough", - item_number: 42, - }); - expect(result.isError).toBeUndefined(); - const responseData = JSON.parse(result.content[0].text); - expect(responseData.result).toBe("success"); - expect(mockAppendSafeOutput).toHaveBeenCalledWith(expect.objectContaining({ type: "add_comment", item_number: 42 })); + const savedContext = global.context; + global.context = { ...global.context, eventName: "push", payload: {} }; + try { + const result = handlers.addCommentHandler({ + body: "A real comment body that is substantive enough", + item_number: 42, + }); + expect(result.isError).toBeUndefined(); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("success"); + expect(mockAppendSafeOutput).toHaveBeenCalledWith(expect.objectContaining({ type: "add_comment", item_number: 42 })); + } finally { + global.context = savedContext; + } }); it("should write entry on workflow_dispatch with issue aw_context", () => { @@ -1900,6 +1923,25 @@ describe("safe_outputs_handlers", () => { global.context = savedContext; } }); + + it("should return intent error on workflow_dispatch with no event_name override", () => { + const savedContext = global.context; + global.context = { + ...global.context, + eventName: "workflow_dispatch", + payload: { inputs: {} }, // no event_name, no aw_context + }; + try { + const result = handlers.addCommentHandler({ body: "A real comment body that is substantive" }); + expect(result.isError).toBe(true); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("error"); + expect(responseData.error).toContain('"workflow_dispatch"'); + expect(mockAppendSafeOutput).not.toHaveBeenCalled(); + } finally { + global.context = savedContext; + } + }); }); describe("createIssueHandler", () => { @@ -2534,6 +2576,43 @@ describe("safe_outputs_handlers", () => { global.context = savedContext; } }); + + it("should return intent error on workflow_dispatch with no event_name override", () => { + const savedContext = global.context; + global.context = { + ...global.context, + eventName: "workflow_dispatch", + payload: { inputs: {} }, // no event_name, no aw_context + }; + try { + const result = handlers.updatePullRequestHandler({ title: "No context title" }); + expect(result.isError).toBe(true); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("error"); + expect(responseData.error).toContain('"workflow_dispatch"'); + expect(mockAppendSafeOutput).not.toHaveBeenCalled(); + } finally { + global.context = savedContext; + } + }); + + it("should write entry when issue_comment fires on a PR (PR context for update_pull_request)", () => { + const savedContext = global.context; + global.context = { + ...global.context, + eventName: "issue_comment", + payload: { issue: { number: 7, pull_request: { url: "https://api.github.com/repos/test-owner/test-repo/pulls/7" } } }, + }; + try { + const result = handlers.updatePullRequestHandler({ title: "PR update from issue_comment" }); + expect(result.isError).toBeUndefined(); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("success"); + expect(mockAppendSafeOutput).toHaveBeenCalledWith(expect.objectContaining({ type: "update_pull_request" })); + } finally { + global.context = savedContext; + } + }); }); describe("updateIssueHandler", () => { @@ -2616,6 +2695,44 @@ describe("safe_outputs_handlers", () => { global.context = savedContext; } }); + + it("should return intent error for issue_comment on a PR (not issue context)", () => { + const savedContext = global.context; + global.context = { + ...global.context, + eventName: "issue_comment", + payload: { issue: { number: 7, pull_request: { url: "https://api.github.com/repos/test-owner/test-repo/pulls/7" } } }, + }; + try { + const result = handlers.updateIssueHandler({ body: "Update body" }); + expect(result.isError).toBe(true); + const data = JSON.parse(result.content[0].text); + expect(data.result).toBe("error"); + expect(data.error).toContain("issue context"); + expect(mockAppendSafeOutput).not.toHaveBeenCalled(); + } finally { + global.context = savedContext; + } + }); + + it("should return intent error on workflow_dispatch with no event_name override", () => { + const savedContext = global.context; + global.context = { + ...global.context, + eventName: "workflow_dispatch", + payload: { inputs: {} }, // no event_name, no aw_context + }; + try { + const result = handlers.updateIssueHandler({ body: "Issue update no context" }); + expect(result.isError).toBe(true); + const responseData = JSON.parse(result.content[0].text); + expect(responseData.result).toBe("error"); + expect(responseData.error).toContain('"workflow_dispatch"'); + expect(mockAppendSafeOutput).not.toHaveBeenCalled(); + } finally { + global.context = savedContext; + } + }); }); }); From 18fb30eb375459e5970bcec20435201aa3c30e28 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:33:26 +0000 Subject: [PATCH 7/7] fix: unify null-guard pattern across all three triggering-context handlers Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/safe_outputs_handlers.cjs | 58 +++++++++++----------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index 29e0b31d5c4..846041ef455 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -1923,7 +1923,7 @@ function createHandlers(server, appendSafeOutput, config = {}) { const effectiveTarget = updateIssueConfig.target || "triggering"; if (effectiveTarget === "triggering") { - let invocationContext; + let invocationContext = null; try { invocationContext = resolveInvocationContext(context); } catch (err) { @@ -1931,20 +1931,21 @@ function createHandlers(server, appendSafeOutput, config = {}) { if (err?.message?.startsWith(ERR_VALIDATION)) { return buildIntentErrorResponse(err.message); } - // Unexpected structural error: fall through and let the downstream handler deal with it. - return defaultHandler("update_issue")(args || {}); + // Unexpected structural error: skip validation and let downstream handle gracefully. } - const { effectiveEventName, effectivePayload } = resolveEffectiveContext(invocationContext, context); - const isIssueCommentOnPR = effectiveEventName === "issue_comment" && Boolean(effectivePayload?.issue?.pull_request); - const isIssueContext = effectiveEventName === "issues" || (effectiveEventName === "issue_comment" && !isIssueCommentOnPR); - - if (!isIssueContext) { - return buildIntentErrorResponse( - `update_issue requires an issue context but the workflow is running on a "${effectiveEventName}" event. ` + - `The update-issue handler uses target: triggering which only applies when an issue triggered the workflow. ` + - `To report results from this workflow, use create_discussion or create_issue instead. ` + - `If you need to update a specific issue, the workflow must configure update-issue: target: '*' and you must supply issue_number.` - ); + if (invocationContext != null) { + const { effectiveEventName, effectivePayload } = resolveEffectiveContext(invocationContext, context); + const isIssueCommentOnPR = effectiveEventName === "issue_comment" && Boolean(effectivePayload?.issue?.pull_request); + const isIssueContext = effectiveEventName === "issues" || (effectiveEventName === "issue_comment" && !isIssueCommentOnPR); + + if (!isIssueContext) { + return buildIntentErrorResponse( + `update_issue requires an issue context but the workflow is running on a "${effectiveEventName}" event. ` + + `The update-issue handler uses target: triggering which only applies when an issue triggered the workflow. ` + + `To report results from this workflow, use create_discussion or create_issue instead. ` + + `If you need to update a specific issue, the workflow must configure update-issue: target: '*' and you must supply issue_number.` + ); + } } } @@ -1973,7 +1974,7 @@ function createHandlers(server, appendSafeOutput, config = {}) { const updatePRConfig = getSafeOutputsToolConfig(config, "update_pull_request"); const effectivePRTarget = updatePRConfig.target || "triggering"; if (effectivePRTarget === "triggering") { - let invocationContext; + let invocationContext = null; try { invocationContext = resolveInvocationContext(context); } catch (err) { @@ -1981,20 +1982,21 @@ function createHandlers(server, appendSafeOutput, config = {}) { if (err?.message?.startsWith(ERR_VALIDATION)) { return buildIntentErrorResponse(err.message); } - // Unexpected structural error: fall through and let the downstream handler deal with it. - return defaultHandler("update_pull_request")(args || {}); + // Unexpected structural error: skip validation and let downstream handle gracefully. } - const { effectiveEventName, effectivePayload } = resolveEffectiveContext(invocationContext, context); - const isIssueCommentOnPR = effectiveEventName === "issue_comment" && Boolean(effectivePayload?.issue?.pull_request); - const isPRContext = PR_EVENT_NAMES.has(effectiveEventName) || isIssueCommentOnPR; - - if (!isPRContext) { - return buildIntentErrorResponse( - `update_pull_request requires a pull request context but the workflow is running on a "${effectiveEventName}" event. ` + - `The update-pull-request handler uses target: triggering which only applies when a pull request triggered the workflow. ` + - `To report results from this workflow, use create_discussion or create_issue instead. ` + - `If you need to update a specific pull request, the workflow must configure update-pull-request: target: '*' and you must supply pull_request_number.` - ); + if (invocationContext != null) { + const { effectiveEventName, effectivePayload } = resolveEffectiveContext(invocationContext, context); + const isIssueCommentOnPR = effectiveEventName === "issue_comment" && Boolean(effectivePayload?.issue?.pull_request); + const isPRContext = PR_EVENT_NAMES.has(effectiveEventName) || isIssueCommentOnPR; + + if (!isPRContext) { + return buildIntentErrorResponse( + `update_pull_request requires a pull request context but the workflow is running on a "${effectiveEventName}" event. ` + + `The update-pull-request handler uses target: triggering which only applies when a pull request triggered the workflow. ` + + `To report results from this workflow, use create_discussion or create_issue instead. ` + + `If you need to update a specific pull request, the workflow must configure update-pull-request: target: '*' and you must supply pull_request_number.` + ); + } } }