Conversation
…rkflows (#concurrency) Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/b2acf6df-c3c1-43a4-9e4f-880d4df1f0c7
…mber capture Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/b2acf6df-c3c1-43a4-9e4f-880d4df1f0c7
There was a problem hiding this comment.
Pull request overview
Prevents GitHub App safe-outputs from inadvertently cancelling the originating run when App-authored issue_comment events re-trigger the same workflow and collide in a shared workflow-level concurrency group.
Changes:
- Adds bot-actor isolation to auto-generated workflow concurrency group keys when
safe-outputs.github-appis used alongside issue_comment-capable triggers. - Emits a compiler warning when custom workflow-level concurrency +
cancel-in-progress: trueis used in the risky configuration (since auto-fix can’t be applied). - Adds unit tests covering risk detection and bot-isolated concurrency key generation.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| pkg/workflow/concurrency.go | Detects bot self-cancel risk and injects bot-isolated concurrency key logic into auto-generation. |
| pkg/workflow/compiler.go | Warns users when custom concurrency + cancel-in-progress can self-cancel under the risky trigger/safe-outputs combination. |
| pkg/workflow/concurrency_test.go | Adds tests for risk detection and generated bot-isolated concurrency strings. |
Comments suppressed due to low confidence (1)
pkg/workflow/compiler.go:241
- The new custom-concurrency warning triggers solely on
cancel-in-progress: true+hasBotSelfCancelRisk(...). As written, it will still warn even if the user’s customconcurrency.groupalready includes bot isolation (or otherwise avoids collisions, e.g. always usesgithub.run_id). This produces a misleading warning and suggests an unnecessary change. Consider extracting the group viaextractConcurrencyGroupFromYAML(...)and suppressing the warning when the group expression already contains an equivalent bot-isolation guard (e.g.contains(github.actor, '[bot]') && github.run_id) or already guarantees uniqueness for bot-triggered runs.
if workflowData.Concurrency != "" &&
strings.Contains(workflowData.Concurrency, "cancel-in-progress: true") &&
hasBotSelfCancelRisk(workflowData) {
fmt.Fprintln(os.Stderr, formatCompilerMessage(markdownPath, "warning",
"Custom workflow-level concurrency with cancel-in-progress: true may cause self-cancellation.\n"+
"safe-outputs.github-app can post comments that re-trigger this workflow via issue_comment,\n"+
"and those passive bot-authored runs can collide with the primary run's concurrency group.\n"+
"Add `contains(github.actor, '[bot]') && github.run_id ||` at the start of your concurrency\n"+
"group expression to route bot-triggered runs to a unique key and prevent self-cancellation.\n"+
"See: https://gh.io/gh-aw/reference/concurrency for details."))
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Warn when the user has specified custom workflow-level concurrency with cancel-in-progress: true | ||
| // AND the workflow has the bot self-cancel risk combination (issue_comment triggers + GitHub App | ||
| // safe-outputs). In this case the auto-generated bot-actor isolation cannot be applied because the | ||
| // user's concurrency expression is preserved as-is. The user must add the bot-actor isolation | ||
| // themselves (e.g. prepend `contains(github.actor, '[bot]') && github.run_id ||` to their group key). | ||
| if workflowData.Concurrency != "" && | ||
| strings.Contains(workflowData.Concurrency, "cancel-in-progress: true") && | ||
| hasBotSelfCancelRisk(workflowData) { | ||
| fmt.Fprintln(os.Stderr, formatCompilerMessage(markdownPath, "warning", | ||
| "Custom workflow-level concurrency with cancel-in-progress: true may cause self-cancellation.\n"+ | ||
| "safe-outputs.github-app can post comments that re-trigger this workflow via issue_comment,\n"+ | ||
| "and those passive bot-authored runs can collide with the primary run's concurrency group.\n"+ | ||
| "Add `contains(github.actor, '[bot]') && github.run_id ||` at the start of your concurrency\n"+ | ||
| "group expression to route bot-triggered runs to a unique key and prevent self-cancellation.\n"+ | ||
| "See: https://gh.io/gh-aw/reference/concurrency for details.")) | ||
| c.IncrementWarningCount() | ||
| } |
There was a problem hiding this comment.
This new warning path isn’t covered by tests. There are existing warning-count tests in this package; adding a unit test that asserts (1) warning is emitted/incremented for a risky workflow with custom concurrency + cancel-in-progress: true, and (2) no warning when bot isolation is already present in the custom group, would help prevent regressions.
This issue also appears on line 232 of the same file.
| // When bot risk is detected, prepend the bot-actor isolation check. | ||
| if botRisk { | ||
| keys = append(keys, "${{ contains(github.actor, '[bot]') && github.run_id || github.event.issue.number || github.event.pull_request.number || github.run_id }}") | ||
| } else { | ||
| keys = append(keys, "${{ github.event.issue.number || github.event.pull_request.number || github.run_id }}") | ||
| } |
There was a problem hiding this comment.
buildConcurrencyGroupKeys hardcodes a bot-isolated expression for the command/slash_command branch instead of reusing the new botIsolatedConcurrencyKey/entityKey helper. This duplicates the bot-isolation logic and makes future edits easy to miss or diverge. Consider using the shared helper to build the expression for this branch as well (passing the issue/PR primary parts and github.run_id tail).
| // When bot risk is detected, prepend the bot-actor isolation check. | |
| if botRisk { | |
| keys = append(keys, "${{ contains(github.actor, '[bot]') && github.run_id || github.event.issue.number || github.event.pull_request.number || github.run_id }}") | |
| } else { | |
| keys = append(keys, "${{ github.event.issue.number || github.event.pull_request.number || github.run_id }}") | |
| } | |
| // Bot-actor isolation is applied via entityKey when botRisk is detected. | |
| keys = append(keys, entityKey( | |
| []string{"github.event.issue.number", "github.event.pull_request.number"}, | |
| []string{"github.run_id"}, | |
| )) |
When a workflow combines
safe-outputs.github-appwithissue_comment-capable triggers (slash_command,command:, explicitissue_comment:), App-authored comments from safe-outputs re-trigger the same workflow. Those passive bot runs resolve to the same workflow-level concurrency group as the primary run, causingcancel-in-progress: trueto cancel the originating run mid-flight.Changes
Auto-fix: bot-actor isolation in generated concurrency keys (
concurrency.go)When the dangerous combination is detected, the auto-generated concurrency group key is prefixed with
contains(github.actor, '[bot]') && github.run_id, routing all bot-triggered runs to a unique per-run key:Applied automatically for
slash_command,command:, and explicitissue_comment:triggers whensafe-outputs.github-appis configured. Workflows with customconcurrency:are not modified.New helpers:
hasIssueCommentTrigger()— detects issue_comment-capable triggershasBotSelfCancelRisk()— detects the dangerous combinationbotIsolatedConcurrencyKey()— builds the bot-isolated key expressionWarning for custom concurrency (
compiler.go)When the user provides their own
concurrency:block withcancel-in-progress: trueand the dangerous combination is present, a compiler warning is emitted explaining the risk and the required fix (contains(github.actor, '[bot]') && github.run_id ||prefix).Tests (
concurrency_test.go)TestHasBotSelfCancelRisk— detection logic for all trigger/safe-outputs combinationsTestBotActorIsolationInConcurrencyKeys— verifies bot prefix applied/absent correctlyTestGenerateConcurrencyConfigWithBotIsolation— end-to-end concurrency string outputWarning
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
https://api.github.com/graphql/usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw GOMOD GOMODCACHE go env ck '**/*.cjs' '*GOINSECURE GO111MODULE 64/bin/go GOINSECURE GOMOD ode-gyp-bin/node-json go(http block)/usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw GOMOD GOMODCACHE rtcfg env ripts/lint_errorGOINSECURE GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE ache/go/1.25.0/xGOPROXY(http block)/usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw GOMOD GOMODCACHE rtcfg env ternal/tools/actGOINSECURE GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE ache/go/1.25.0/xGOPROXY(http block)https://api.github.com/orgs/test-owner/actions/secrets/usr/bin/gh gh api /orgs/test-owner/actions/secrets --jq .secrets[].name ck '**/*.cjs' '*GOINSECURE GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE 64/bin/go GOINSECURE fa04f7f04dc6e954-C GOMODCACHE go(http block)https://api.github.com/repos/actions/ai-inference/git/ref/tags/v1/usr/bin/gh gh api /repos/actions/ai-inference/git/ref/tags/v1 --jq .object.sha --show-toplevel sh /usr/bin/git "prettier" --chegit go 64/bin/go git conf�� user.email test@example.com /opt/hostedtoolcache/node/24.14.0/x64/bin/node -json GO111MODULE 64/bin/go /opt/hostedtoolcache/node/24.14.0/x64/bin/node(http block)https://api.github.com/repos/actions/checkout/git/ref/tags/v3/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v3 --jq .object.sha 5852749/b441/styles.test GOPROXY(http block)https://api.github.com/repos/actions/checkout/git/ref/tags/v5/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha -json GO111MODULE ache/go/1.25.0/x64/bin/go GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE ache/go/1.25.0/x64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha needs.build.outputs.version go /usr/bin/git le-frontmatter.mgit GO111MODULE 64/bin/go git rev-�� --show-toplevel go /usr/bin/git -json GO111MODULE 64/bin/go git(http block)/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v5 --jq .object.sha --show-toplevel go /usr/bin/git -json GO111MODULE /opt/hostedtoolc--show-toplevel git rev-�� --show-toplevel go 0/x64/bin/node -json GO111MODULE /opt/hostedtoolc--show-toplevel git(http block)https://api.github.com/repos/actions/checkout/git/ref/tags/v6/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v6 --jq .object.sha -test.paniconexit0 -test.v=true /usr/lib/git-core/git -test.timeout=10git -test.run=^Test -test.short=true--show-toplevel /usr/lib/git-core/git main�� nt/action/git/ref/tags/v999.999.999 --auto /usr/bin/git --detach **/*.ts 64/bin/go git(http block)/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v6 --jq .object.sha /tmp/go-build1525852749/b447/timeutil.test -importcfg /usr/bin/git -s -w -buildmode=exe git chec�� .github/workflows/test.md -extld=gcc /usr/bin/git --check **/*.cjs 64/bin/go git(http block)/usr/bin/gh gh api /repos/actions/checkout/git/ref/tags/v6 --jq .object.sha --show-toplevel go /usr/bin/git -json GO111MODULE ache/go/1.25.0/x--show-toplevel git rev-�� --show-toplevel go /usr/bin/git 5231-52614/test-git GO111MODULE 0/x64/bin/node git(http block)https://api.github.com/repos/actions/github-script/git/ref/tags/v8/usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE erignore env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)/usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE sh(http block)/usr/bin/gh gh api /repos/actions/github-script/git/ref/tags/v8 --jq .object.sha -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE node(http block)https://api.github.com/repos/actions/setup-go/git/ref/tags/v4/usr/bin/gh gh api /repos/actions/setup-go/git/ref/tags/v4 --jq .object.sha -bool -buildtags /usr/bin/git -errorsas -ifaceassert -nilfunc git -C /tmp/gh-aw-test-runs/20260325-035231-52614/test-225390722 rev-parse /usr/bin/git @{u} **/*.cjs nch,headSha,disp--show-toplevel git(http block)https://api.github.com/repos/actions/setup-node/git/ref/tags/v4/usr/bin/gh gh api /repos/actions/setup-node/git/ref/tags/v4 --jq .object.sha /tmp/TestHashConsistency_GoAndJavaScript3124821442/001/test-frontmatter-with-nes--detach go /usr/bin/git -json GO111MODULE 64/bin/go git rev-�� --show-toplevel node /opt/hostedtoolcache/node/24.14.0/x64/bin/node --check **/*.cjs 64/bin/go node(http block)https://api.github.com/repos/actions/upload-artifact/git/ref/tags/v4/usr/bin/gh gh api /repos/actions/upload-artifact/git/ref/tags/v4 --jq .object.sha 5852749/b434/_pkg_.a GO111MODULE 5852749/b434=> GOINSECURE b/gh-aw/pkg/slicrev-parse GOMODCACHE bash --no�� ithub-script/git/ref/tags/v8 GOPROXY /home/REDACTED/.local/bin/bash GOSUMDB GOWORK 64/bin/go 5852749/b434/importcfg(http block)https://api.github.com/repos/github/gh-aw-actions/git/ref/tags/v1.0.0/usr/bin/gh gh api /repos/github/gh-aw-actions/git/ref/tags/v1.0.0 --jq .object.sha -json GO111MODULE /opt/pipx_bin/bash GOINSECURE GOMOD GOMODCACHE bash --no�� --noprofile GOPROXY 5852749/b438/stringutil.test GOSUMDB GOWORK 64/bin/go 5852749/b438/stringutil.test(http block)https://api.github.com/repos/github/gh-aw-actions/git/ref/tags/v1.2.3/usr/bin/gh gh api /repos/github/gh-aw-actions/git/ref/tags/v1.2.3 --jq .object.sha 5231-52614/test-225390722 GO111MODULE /snap/bin/bash l GOMOD GOMODCACHE bash --no�� k/gh-aw/gh-aw/.github/workflows GOPROXY ache/go/1.25.0/x64/pkg/tool/linux_amd64/vet GOSUMDB GOWORK 64/bin/go ache/go/1.25.0/x64/pkg/tool/linux_amd64/vet(http block)https://api.github.com/repos/github/gh-aw/actions/runs/1/artifacts/usr/bin/gh gh run download 1 --dir test-logs/run-1 GO111MODULE x_amd64/compile GOINSECURE GOMOD GOMODCACHE x_amd64/compile env -json GO111MODULE 0/x64/bin/bash GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/github/gh-aw/actions/runs/12345/artifacts/usr/bin/gh gh run download 12345 --dir test-logs/run-12345 GO111MODULE x_amd64/vet GOINSECURE GOMOD GOMODCACHE x_amd64/vet env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/github/gh-aw/actions/runs/12346/artifacts/usr/bin/gh gh run download 12346 --dir test-logs/run-12346 GO111MODULE x_amd64/vet Action pins syngit GOMOD GOMODCACHE x_amd64/vet env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/github/gh-aw/actions/runs/2/artifacts/usr/bin/gh gh run download 2 --dir test-logs/run-2 GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE x_amd64/link GOINSECURE GOMOD GOMODCACHE x_amd64/link(http block)https://api.github.com/repos/github/gh-aw/actions/runs/3/artifacts/usr/bin/gh gh run download 3 --dir test-logs/run-3 GO111MODULE x_amd64/link GOINSECURE GOMOD GOMODCACHE x_amd64/link env -json GO111MODULE ache/go/1.25.0/x64/bin/go GOINSECURE GOMOD GOMODCACHE ef/N6GE9dzJuLpfUe9tz4e_/ThKvzodBlPIPkS6j74YO(http block)https://api.github.com/repos/github/gh-aw/actions/runs/4/artifacts/usr/bin/gh gh run download 4 --dir test-logs/run-4 GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE ache/go/1.25.0/x64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/github/gh-aw/actions/runs/5/artifacts/usr/bin/gh gh run download 5 --dir test-logs/run-5 GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE ache/go/1.25.0/x64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/github/gh-aw/actions/workflows/usr/bin/gh gh workflow list --json name,state,path 16cd4a5c5f3f40b6GOINSECURE GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE ache/go/1.25.0/xGOPROXY env 233397/b373/_pkgGOSUMDB GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)/usr/bin/gh gh run list --json databaseId,number,url,status,conclusion,workflowName,createdAt,startedAt,updatedAt,event,headBranch,headSha,displayTitle --workflow nonexistent-workflow-12345 --limit 100 GOMOD GOMODCACHE go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE node(http block)/usr/bin/gh gh run list --json databaseId,number,url,status,conclusion,workflowName,createdAt,startedAt,updatedAt,event,headBranch,headSha,displayTitle --workflow nonexistent-workflow-12345 --limit 6 GOMOD GOMODCACHE go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/github/gh-aw/git/ref/tags/v1.0.0/usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v1.0.0 --jq .object.sha -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE ache/go/1.25.0/x64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/github/gh-aw/git/ref/tags/v1.2.3/usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v1.2.3 --jq .object.sha -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE node(http block)https://api.github.com/repos/github/gh-aw/git/ref/tags/v2.0.0/usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v2.0.0 --jq .object.sha -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go env -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE node(http block)/usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v2.0.0 --jq .object.sha -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE node /opt�� run lint:cjs 64/bin/go GOSUMDB GOWORK 64/bin/go sh(http block)/usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v2.0.0 --jq .object.sha -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE sh -c npx prettier --c-errorsas GOPROXY 64/bin/go GOSUMDB GOWORK 64/bin/go sh(http block)https://api.github.com/repos/github/gh-aw/git/ref/tags/v3.0.0/usr/bin/gh gh api /repos/github/gh-aw/git/ref/tags/v3.0.0 --jq .object.sha -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE /bin/sh -c cd actions/setup-errorsas GOPROXY 64/bin/go GOSUMDB GOWORK 64/bin/go sh(http block)https://api.github.com/repos/nonexistent/action/git/ref/tags/v999.999.999/usr/bin/gh gh api /repos/nonexistent/action/git/ref/tags/v999.999.999 --jq .object.sha -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE Ah/irCVNlwFVr1e4kQn-6MP/w6Oxv7UArev-parse env ithout_min-integrity3475756308/001 GO111MODULE ache/go/1.25.0/x64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/nonexistent/repo/actions/runs/12345/usr/bin/gh gh run view 12345 --repo nonexistent/repo --json status,conclusion GOINSECURE GOMOD GOMODCACHE go env 1795751090/.github/workflows GO111MODULE ache/go/1.25.0/x64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/owner/repo/actions/workflows/usr/bin/gh gh workflow list --json name,state,path --repo owner/repo 64/bin/go GOINSECURE GOMOD GOMODCACHE ache/go/1.25.0/xGOPROXY env 233397/b413/_pkgGOSUMDB GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)/usr/bin/gh gh workflow list --json name,state,path --repo owner/repo 64/bin/go GOINSECURE GOMOD GOMODCACHE ache/go/1.25.0/xGOPROXY env 233397/b393/_pkgGOSUMDB GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/owner/repo/contents/file.md/tmp/go-build1525852749/b402/cli.test /tmp/go-build1525852749/b402/cli.test -test.testlogfile=/tmp/go-build1525852749/b402/testlog.txt -test.paniconexit0 -test.v=true -test.parallel=4 -test.timeout=10m0s -test.run=^Test -test.short=true GOINSECURE GOMOD ode-gyp-bin/sh go env ck 'scripts/**/*GOINSECURE GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)https://api.github.com/repos/test-owner/test-repo/actions/secrets/usr/bin/gh gh api /repos/test-owner/test-repo/actions/secrets --jq .secrets[].name -json GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE erignore env e=false GO111MODULE 64/bin/go GOINSECURE GOMOD GOMODCACHE go(http block)If you need me to access, download, or install something from one of these locations, you can either:
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.