Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions .github/workflows/reusable-code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ on:
event_payload:
required: true
type: string
trigger_source:
required: false
type: string
default: ""
description: "GitHub username that triggered /fs-code; empty for label-triggered runs"
mint_url:
required: true
type: string
Expand Down Expand Up @@ -199,6 +204,7 @@ jobs:
REPO_FULL_NAME: ${{ inputs.source_repo }}
CODE_ALLOWED_TARGET_BRANCHES: ''
TARGET_BRANCH: main
TRIGGER_SOURCE: ${{ inputs.trigger_source }}
with:
agent: code
version: ${{ inputs.fullsend_version }}
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/reusable-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ jobs:
if [[ "${ISSUE_IS_PR}" == "false" ]]; then
if [[ "${COMMENT_USER_TYPE}" != "Bot" ]] && is_authorized; then
STAGE="code"
TRIGGER_SOURCE="${COMMENT_USER_LOGIN}"
fi
fi
;;
Expand Down Expand Up @@ -460,6 +461,7 @@ jobs:
event_type: ${{ github.event_name }}
source_repo: ${{ github.repository }}
event_payload: ${{ needs.route.outputs.event_payload }}
trigger_source: ${{ needs.route.outputs.trigger_source }}
install_mode: ${{ inputs.install_mode }}
mint_url: ${{ inputs.mint_url }}
gcp_region: ${{ inputs.gcp_region }}
Expand Down
5 changes: 3 additions & 2 deletions internal/harness/scaffold_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,9 @@ func TestResolveForge_ScaffoldRunnerEnvMerge(t *testing.T) {
},
{
file: "code.yaml",
topLevelKeys: []string{"CODE_ALLOWED_TARGET_BRANCHES", "FULLSEND_OUTPUT_SCHEMA", "FULLSEND_OUTPUT_FILE"},
forgeGithubKeys: []string{"PUSH_TOKEN", "PUSH_TOKEN_SOURCE", "REPO_FULL_NAME", "ISSUE_NUMBER", "REPO_DIR"},
topLevelKeys: []string{},
forgeGithubKeys: []string{},
envRunnerKeys: []string{"CODE_ALLOWED_TARGET_BRANCHES", "TRIGGER_SOURCE", "FULLSEND_OUTPUT_SCHEMA", "FULLSEND_OUTPUT_FILE", "PUSH_TOKEN", "PUSH_TOKEN_SOURCE", "REPO_FULL_NAME", "ISSUE_NUMBER", "REPO_DIR"},
},
{
file: "review.yaml",
Expand Down
5 changes: 5 additions & 0 deletions internal/scaffold/fullsend-repo/.github/workflows/code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ on:
event_payload:
required: true
type: string
trigger_source:
required: false
type: string
description: "GitHub username that triggered /fs-code; empty for label-triggered runs"

concurrency:
group: fullsend-code-${{ inputs.source_repo }}-${{ fromJSON(inputs.event_payload).issue.number }}
Expand All @@ -34,6 +38,7 @@ jobs:
event_type: ${{ inputs.event_type }}
source_repo: ${{ inputs.source_repo }}
event_payload: ${{ inputs.event_payload }}
trigger_source: ${{ inputs.trigger_source || '' }}
mint_url: ${{ vars.FULLSEND_MINT_URL }}
gcp_region: ${{ vars.FULLSEND_GCP_REGION }}
install_mode: per-org
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ jobs:
if [[ "${ISSUE_HAS_PR}" == "false" ]]; then
if [[ "${COMMENT_USER_TYPE}" != "Bot" ]] && is_authorized; then
STAGE="code"
TRIGGER_SOURCE="${COMMENT_USER_LOGIN}"
fi
fi
;;
Expand Down Expand Up @@ -446,7 +447,7 @@ jobs:
-f event_payload="$EVENT_PAYLOAD"
)

if [[ -n "${TRIGGER_SOURCE:-}" ]] && [[ "$STAGE" == "fix" ]]; then
if [[ -n "${TRIGGER_SOURCE:-}" ]] && [[ "$STAGE" == "fix" || "$STAGE" == "code" ]]; then
DISPATCH_ARGS+=(-f trigger_source="$TRIGGER_SOURCE")
fi

Expand Down
26 changes: 14 additions & 12 deletions internal/scaffold/fullsend-repo/harness/code.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,24 @@ skills:
plugins:
- plugins/gopls-lsp

# Environment variables available to pre/post scripts on the runner.
# These are expanded from the runner environment and NEVER enter the sandbox.
runner_env:
CODE_ALLOWED_TARGET_BRANCHES: "${CODE_ALLOWED_TARGET_BRANCHES}"
FULLSEND_OUTPUT_SCHEMA: ${FULLSEND_DIR}/schemas/code-result.schema.json
FULLSEND_OUTPUT_FILE: code-result.json
# Runner env for pre/post scripts (ADR 0055 env.runner).
env:
runner:
CODE_ALLOWED_TARGET_BRANCHES: "${CODE_ALLOWED_TARGET_BRANCHES}"
TRIGGER_SOURCE: "${TRIGGER_SOURCE}"
FULLSEND_OUTPUT_SCHEMA: ${FULLSEND_DIR}/schemas/code-result.schema.json
FULLSEND_OUTPUT_FILE: code-result.json

timeout_minutes: 35

forge:
github:
pre_script: scripts/pre-code.sh
post_script: scripts/post-code.sh
runner_env:
PUSH_TOKEN: "${PUSH_TOKEN}"
PUSH_TOKEN_SOURCE: "${PUSH_TOKEN_SOURCE}"
REPO_FULL_NAME: "${REPO_FULL_NAME}"
ISSUE_NUMBER: "${ISSUE_NUMBER}"
REPO_DIR: "${GITHUB_WORKSPACE}/target-repo"
env:
runner:
PUSH_TOKEN: "${PUSH_TOKEN}"
PUSH_TOKEN_SOURCE: "${PUSH_TOKEN_SOURCE}"
REPO_FULL_NAME: "${REPO_FULL_NAME}"
ISSUE_NUMBER: "${ISSUE_NUMBER}"
REPO_DIR: "${GITHUB_WORKSPACE}/target-repo"
134 changes: 134 additions & 0 deletions internal/scaffold/fullsend-repo/scripts/post-code-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,140 @@ run_precommit_retry_test "precommit-passes-with-unstaged" \
run_precommit_retry_test "precommit-retry-passes-but-left-unstaged" \
"1" "yes" "0" "blocked:retry-left-unstaged" "yes"

# ---------------------------------------------------------------------------
# Test helpers — reimplement PR assignee resolution from post-code.sh
# ---------------------------------------------------------------------------
is_human_github_user() {
local login="${1:-}"
if [[ -z "${login}" ]]; then
return 1
fi
case "${login}" in
app/*|dependabot) return 1 ;;
esac
if [[ "${login}" =~ \[bot\]$ ]]; then
return 1
fi
return 0
}

resolve_pr_assignee_from_context() {
local trigger_source="$1"
local issue_json="$2"

if is_human_github_user "${trigger_source}"; then
echo "${trigger_source}"
return 0
fi

if [[ -z "${issue_json}" ]]; then
return 1
fi

local human_assignee
human_assignee="$(echo "${issue_json}" | jq -r '
[.assignees[].login | select(
(startswith("app/") | not) and
(test("\\[bot\\]$") | not) and
(. != "dependabot")
)] | .[0] // empty
')"
if [[ -n "${human_assignee}" ]]; then
echo "${human_assignee}"
return 0
fi

local author_login
author_login="$(echo "${issue_json}" | jq -r '.author.login // empty')"
if is_human_github_user "${author_login}"; then
echo "${author_login}"
return 0
fi

return 1
}

decide_assign_action() {
local trigger_source="$1"
local issue_json="$2"
local existing_assignee_count="$3"

if [[ "${existing_assignee_count}" != "0" ]]; then
echo "skip:has-assignees"
return 0
fi

local assignee
assignee="$(resolve_pr_assignee_from_context "${trigger_source}" "${issue_json}" || true)"
if [[ -z "${assignee}" ]]; then
echo "skip:no-candidate"
return 0
fi

echo "assign:${assignee}"
}

run_assignee_test() {
local test_name="$1"
local trigger_source="$2"
local issue_json="$3"
local existing_count="$4"
local expected="$5"

local actual
actual="$(decide_assign_action "${trigger_source}" "${issue_json}" "${existing_count}")"

if [ "${actual}" != "${expected}" ]; then
echo "FAIL: ${test_name}"
echo " trigger_source: '${trigger_source}'"
echo " existing_count: '${existing_count}'"
echo " expected: '${expected}'"
echo " actual: '${actual}'"
FAILURES=$((FAILURES + 1))
return
fi

echo "PASS: ${test_name}"
}

HUMAN_ISSUE_JSON='{
"assignees": [{"login": "alice"}],
"author": {"login": "bob"}
}'

BOT_ISSUE_JSON='{
"assignees": [{"login": "fullsend-ai-triage[bot]"}],
"author": {"login": "app/fullsend-ai-retro"}
}'

# Human /fs-code invoker wins over issue assignee and author
run_assignee_test "trigger-source-human-wins" \
"ifireball" "${HUMAN_ISSUE_JSON}" "0" "assign:ifireball"

# Bot trigger falls through to first human assignee
run_assignee_test "bot-trigger-uses-human-assignee" \
"fullsend-ai-coder[bot]" "${HUMAN_ISSUE_JSON}" "0" "assign:alice"

# Bot trigger with no human assignee uses human author
run_assignee_test "bot-trigger-uses-human-author" \
"" '{"assignees": [], "author": {"login": "bob"}}' "0" "assign:bob"

# All candidates are bots — no assignment
run_assignee_test "all-bot-candidates-no-assign" \
"fullsend-ai-coder[bot]" "${BOT_ISSUE_JSON}" "0" "skip:no-candidate"

# PR already has assignees — no reassignment
run_assignee_test "existing-assignees-skip" \
"ifireball" "${HUMAN_ISSUE_JSON}" "1" "skip:has-assignees"

# dependabot filtered from assignee chain
run_assignee_test "dependabot-filtered" \
"" '{"assignees": [{"login": "dependabot[bot]"}], "author": {"login": "dependabot"}}' "0" "skip:no-candidate"

# dependabotb is human (not matched by dependabot[bot] glob bug)
run_assignee_test "dependabotb-is-human" \
"" '{"assignees": [], "author": {"login": "dependabotb"}}' "0" "assign:dependabotb"

# --- Summary ---

echo ""
Expand Down
95 changes: 95 additions & 0 deletions internal/scaffold/fullsend-repo/scripts/post-code.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#
# Optional environment variables:
# PUSH_TOKEN_SOURCE — "github-app" (for logging; default: unknown)
# TRIGGER_SOURCE — GitHub username that triggered /fs-code (empty for label triggers)
# CODE_ALLOWED_TARGET_BRANCHES
# — comma-separated list of branches the agent may target,
# or "*" for any. When unset, only the repo's default
Expand All @@ -43,6 +44,97 @@ set -euo pipefail
GITLEAKS_VERSION="8.30.1"
GITLEAKS_SHA256="551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb"

# ---------------------------------------------------------------------------
# Helper: human GitHub user detection (for PR assignee resolution)
# ---------------------------------------------------------------------------
is_human_github_user() {
local login="${1:-}"
if [[ -z "${login}" ]]; then
return 1
fi
case "${login}" in
app/*|dependabot) return 1 ;;
esac
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
if [[ "${login}" =~ \[bot\]$ ]]; then
return 1
fi
return 0
}

# Resolve a human assignee for the PR using precedence:
# 1. TRIGGER_SOURCE (/fs-code invoker) when human
# 2. First human issue assignee
# 3. Human issue author
# Prints the login on stdout, or nothing when no human matches.
resolve_pr_assignee() {
if is_human_github_user "${TRIGGER_SOURCE:-}"; then
echo "${TRIGGER_SOURCE}"
return 0
fi

local issue_json
issue_json="$(gh issue view "${ISSUE_NUMBER}" --repo "${REPO_FULL_NAME}" \
--json assignees,author 2>/dev/null || true)"
if [[ -z "${issue_json}" ]]; then
return 1
fi

local human_assignee
human_assignee="$(echo "${issue_json}" | jq -r '
[.assignees[].login | select(
(startswith("app/") | not) and
(test("\\[bot\\]$") | not) and
(. != "dependabot")
)] | .[0] // empty
')"
if [[ -n "${human_assignee}" ]]; then
echo "${human_assignee}"
return 0
fi

local author_login
author_login="$(echo "${issue_json}" | jq -r '.author.login // empty')"
if is_human_github_user "${author_login}"; then
echo "${author_login}"
return 0
fi

return 1
}

# Best-effort PR assignee: skip when the PR already has assignees.
maybe_assign_pr() {
local pr_number="$1"
local existing_count
if ! existing_count="$(gh pr view "${pr_number}" --repo "${REPO_FULL_NAME}" \
--json assignees --jq '.assignees | length' 2>/dev/null)"; then
echo "::warning::Could not read assignees for PR #${pr_number} — skipping assignment"
return 0
fi
if [[ "${existing_count}" != "0" ]]; then
echo "PR #${pr_number} already has assignees — skipping assignment"
return 0
fi
Comment thread
qodo-code-review[bot] marked this conversation as resolved.

local assignee
assignee="$(resolve_pr_assignee || true)"
if [[ -z "${assignee}" ]]; then
echo "No human assignee candidate — leaving PR #${pr_number} unassigned"
return 0
fi

echo "Assigning PR #${pr_number} to ${assignee}..."
local assign_err
assign_err="$(gh pr edit "${pr_number}" --repo "${REPO_FULL_NAME}" \
--add-assignee "${assignee}" 2>&1)" || {
echo "::warning::Failed to assign PR #${pr_number} to ${assignee} — continuing"
if [[ -n "${assign_err}" ]]; then
# Strip :: sequences so API error text cannot inject workflow commands.
echo "::warning::${assign_err//::/ }"
fi
Comment thread
ifireball marked this conversation as resolved.
}
}

# ---------------------------------------------------------------------------
# Setup
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -435,6 +527,7 @@ if [ -n "${EXISTING_PR_NUM}" ]; then
echo "PR #${EXISTING_PR_NUM} already exists — branch updated with new commits"
echo "PR: ${EXISTING_PR_URL}"
echo "pr_url=${EXISTING_PR_URL}" >> "${GITHUB_OUTPUT:-/dev/null}"
maybe_assign_pr "${EXISTING_PR_NUM}"
exit 0
fi

Expand Down Expand Up @@ -516,3 +609,5 @@ gh issue edit "${PR_NUMBER_FROM_URL}" \
--repo "${REPO_FULL_NAME}" \
--add-label "ready-for-review" 2>/dev/null || \
echo "::warning::Failed to apply ready-for-review label to PR #${PR_NUMBER_FROM_URL}"

maybe_assign_pr "${PR_NUMBER_FROM_URL}"
Loading
Loading