Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 33 additions & 9 deletions .github/workflows/reusable-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ jobs:
runs-on: ${{ inputs.runner_image }}
permissions:
contents: read
issues: read
pull-requests: read
outputs:
stage: ${{ steps.role-check.outputs.skipped != 'true' && steps.agent-check.outputs.skipped != 'true' && steps.pr-check.outputs.skipped != 'true' && steps.route.outputs.stage || '' }}
Expand Down Expand Up @@ -330,15 +331,38 @@ jobs:
SOURCE_REPO: ${{ github.repository }}
run: |
set -euo pipefail
BOT_LOGIN="fullsend-ai[bot]"
CODER_BOT_LOGIN="fullsend-ai-coder[bot]"
MENTIONING_PRS="$(gh pr list --repo "${SOURCE_REPO}" --state open \
--search "${ISSUE_NUMBER} in:title,body" \
--json number,author \
--jq "[.[] | select(.author.login != \"${BOT_LOGIN}\" and .author.login != \"${CODER_BOT_LOGIN}\")] | .[].number" \
2>/dev/null || true)"
if [[ -n "${MENTIONING_PRS}" ]]; then
echo "::notice::Open PR(s) mentioning issue #${ISSUE_NUMBER} found — skipping code dispatch"
# Query issues for PRs that actually close them (Fixes/Closes/Resolves
# keywords), not a substring search over title/body — the latter
# false-positives on any PR that merely references the issue (#5575).
# closedByPullRequestsReferences also returns MERGED PRs regardless of
# includeClosedPrs, so filter on .state explicitly. GraphQL's Bot.login
# omits the REST "[bot]" suffix, so match on __typename == "Bot" rather
# than a literal "fullsend-ai-coder[bot]" string — see
# docs/contributing/bot-identities.md for the login.
OWNER="${SOURCE_REPO%%/*}"
REPO_NAME="${SOURCE_REPO##*/}"
GQL_ERR=$(mktemp)
if ! LINKED_PRS=$(gh api graphql -f query='
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
issue(number: $number) {
closedByPullRequestsReferences(first: 20) {
nodes { number state author { login __typename } }
Comment thread
waynesun09 marked this conversation as resolved.
Resolved
}
}
}
}' -f owner="${OWNER}" -f repo="${REPO_NAME}" -F number="${ISSUE_NUMBER}" \
--jq '[.data.repository.issue.closedByPullRequestsReferences.nodes[]
| select(.state == "OPEN")
| select(.author.__typename != "Bot" or .author.login != "fullsend-ai-coder")]
Comment thread
waynesun09 marked this conversation as resolved.
Resolved
| .[].number' \
Comment thread
waynesun09 marked this conversation as resolved.
Resolved
Comment thread
waynesun09 marked this conversation as resolved.
Resolved
Comment thread
waynesun09 marked this conversation as resolved.
Resolved
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Resolved
2>"${GQL_ERR}"); then
Comment thread
waynesun09 marked this conversation as resolved.
Resolved
echo "::warning::Linked-PR query failed for issue #${ISSUE_NUMBER}: $(tr '\n' ' ' < "${GQL_ERR}" | sed 's/::/__/g')"
Comment thread
waynesun09 marked this conversation as resolved.
Resolved
Comment thread
waynesun09 marked this conversation as resolved.
Resolved
LINKED_PRS=""
Comment thread
waynesun09 marked this conversation as resolved.
Resolved
fi
Comment thread
waynesun09 marked this conversation as resolved.
Resolved
rm -f "${GQL_ERR}"
Comment thread
waynesun09 marked this conversation as resolved.
Resolved
if [[ -n "${LINKED_PRS}" ]]; then
echo "::notice::Open PR(s) linked to close issue #${ISSUE_NUMBER} found — skipping code dispatch"
echo "skipped=true" >> "${GITHUB_OUTPUT}"
fi

Expand Down
2 changes: 1 addition & 1 deletion docs/agents/code.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Triggered when the `ready-to-code` label is applied to an issue or via `/fs-code

The code agent follows a three-phase pipeline: pre-script, sandbox execution, post-script.

1. **Pre-script** validates inputs on the runner before sandbox creation. It also checks for open PRs linked to the issue.
1. **Pre-script** validates inputs on the runner before sandbox creation. It also checks for open PRs that close the issue (via `Fixes`/`Closes`/`Resolves` keywords).
2. **Sandbox** — the agent reads the issue, explores the codebase, writes code, runs tests and linters, and commits locally. It has no network access (enforced by OpenShell).
3. **Post-script** runs on the runner: it performs protected path checks, secret scanning, pre-commit checks, pushes the branch, and creates the PR.

Expand Down
2 changes: 2 additions & 0 deletions docs/contributing/bot-identities.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ Fullsend agents authenticate as GitHub Apps; the table below also includes non-a
| renovate | `renovate-fullsend[bot]` | Dependency updates (not a fullsend agent) |

When referencing bot identities in code (e.g., trusted actor lists, dispatch filters), always verify the login name against this table. Do not assume each agent role has a unique app identity — the fix agent reuses `fullsend-ai-coder[bot]`, not a separate `fullsend-ai-fix[bot]`.

**REST vs. GraphQL login format:** the `[bot]` suffix above is the REST/App-slug form. GitHub's GraphQL API omits it — a bot author's `login` field comes back as `fullsend-ai-coder`, not `fullsend-ai-coder[bot]`, with `__typename: "Bot"`. Comparing a GraphQL-sourced login against a literal `"...[bot]"` string never matches (see #5575) — match on `__typename == "Bot"` plus the un-suffixed login instead.
52 changes: 42 additions & 10 deletions internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
# lint-workflow-size: max-lines=580
# lint-workflow-size: max-lines=610
# Dispatcher workflow that routes events to agent workflows based on stage.
# Routing logic determines the stage from event context — the shim only
# forwards the raw event. Adding a new stage requires only a case branch
Expand All @@ -26,6 +26,7 @@ jobs:
actions: write
contents: read
id-token: write
issues: read
pull-requests: read
outputs:
dispatched_count: ${{ steps.dispatch.outputs.dispatched_count }}
Expand Down Expand Up @@ -266,15 +267,46 @@ jobs:
SOURCE_REPO: ${{ github.repository }}
run: |
set -euo pipefail
BOT_LOGIN="fullsend-ai[bot]"
CODER_BOT_LOGIN="fullsend-ai-coder[bot]"
MENTIONING_PRS="$(gh pr list --repo "${SOURCE_REPO}" --state open \
--search "${ISSUE_NUMBER} in:title,body" \
--json number,author \
--jq "[.[] | select(.author.login != \"${BOT_LOGIN}\" and .author.login != \"${CODER_BOT_LOGIN}\")] | .[].number" \
2>/dev/null || true)"
if [[ -n "${MENTIONING_PRS}" ]]; then
echo "::notice::Open PR(s) mentioning issue #${ISSUE_NUMBER} found — skipping code dispatch"
# Query issues for PRs that actually close them (Fixes/Closes/Resolves
# keywords), not a substring search over title/body — the latter
# false-positives on any PR that merely references the issue (#5575).
# closedByPullRequestsReferences also returns MERGED PRs regardless of
# includeClosedPrs, so filter on .state explicitly. GraphQL's Bot.login
# omits the REST "[bot]" suffix, so match on __typename == "Bot" rather
# than a literal "fullsend-ai-coder[bot]" string — see
# docs/contributing/bot-identities.md for the login.
#
# NOTE (org/workflow_call mode): this job's token lacks `issues: read`
Comment thread
waynesun09 marked this conversation as resolved.
Resolved
# (neither this job nor the shim-workflow-call.yaml caller grants it),
# so this query is denied and the guard fails open BY DESIGN —
# pre-code.sh still dedups on a minted app token. Do NOT add the scope
# to this job alone: a called workflow cannot exceed its caller's
# grant, so repos with un-reconciled shims would hard-fail. Roll out
# shim-first if ever wanted (per-repo mode already grants it).
OWNER="${SOURCE_REPO%%/*}"
REPO_NAME="${SOURCE_REPO##*/}"
GQL_ERR=$(mktemp)
if ! LINKED_PRS=$(gh api graphql -f query='
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
issue(number: $number) {
closedByPullRequestsReferences(first: 20) {
nodes { number state author { login __typename } }
}
}
}
}' -f owner="${OWNER}" -f repo="${REPO_NAME}" -F number="${ISSUE_NUMBER}" \
--jq '[.data.repository.issue.closedByPullRequestsReferences.nodes[]
| select(.state == "OPEN")
| select(.author.__typename != "Bot" or .author.login != "fullsend-ai-coder")]
| .[].number' \
2>"${GQL_ERR}"); then
echo "::warning::Linked-PR query failed for issue #${ISSUE_NUMBER}: $(tr '\n' ' ' < "${GQL_ERR}" | sed 's/::/__/g')"
LINKED_PRS=""
fi
rm -f "${GQL_ERR}"
if [[ -n "${LINKED_PRS}" ]]; then
echo "::notice::Open PR(s) linked to close issue #${ISSUE_NUMBER} found — skipping code dispatch"
echo "skipped=true" >> "${GITHUB_OUTPUT}"
fi

Expand Down
Loading