Skip to content

Commit f7bf57e

Browse files
fix(dispatch): address review feedback on workflow_call dispatch
- Per-org stage jobs: explicit OIDC permissions; kill-switch ternary fix - Role mapping: code|fix → coder; prioritize workflow_call secrets - Per-repo reusable-dispatch: prioritize job via .fullsend/prioritize.yml - finding-agent-runs skill: dispatch.yml instead of removed thin workflows - reusable-fix: fail closed when gh pr view fails (bot eligibility) - pre-code-test: restore line-by-line extra_env parsing - e2e: capture issueCreatedAt before issue creation Signed-off-by: Barak Korren <bkorren@redhat.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Barak Korren <bkorren@redhat.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Barak Korren <bkorren@redhat.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Barak Korren <bkorren@redhat.com> Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Barak Korren <bkorren@redhat.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 557c374 commit f7bf57e

12 files changed

Lines changed: 108 additions & 45 deletions

File tree

.github/workflows/reusable-code.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ jobs:
127127
ISSUE_NUMBER: ${{ fromJSON(inputs.event_payload).issue.number }}
128128
REPO_FULL_NAME: ${{ inputs.source_repo }}
129129
GITHUB_ISSUE_URL: ${{ fromJSON(inputs.event_payload).issue.html_url }}
130+
COMMENT_BODY: ${{ fromJSON(inputs.event_payload).comment.body }}
130131
run: bash scripts/pre-code.sh
131132

132133
- name: Setup GCP and prepare credentials

.github/workflows/reusable-dispatch.yml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
contents: read
5050
pull-requests: read
5151
outputs:
52-
stage: ${{ steps.role-check.outputs.skipped == 'true' && '' || steps.route.outputs.stage }}
52+
stage: ${{ steps.role-check.outputs.skipped != 'true' && steps.route.outputs.stage || '' }}
5353
trigger_source: ${{ steps.route.outputs.trigger_source }}
5454
event_payload: ${{ steps.payload.outputs.event_payload }}
5555
steps:
@@ -413,3 +413,19 @@ jobs:
413413
FULLSEND_GCP_WIF_PROVIDER: ${{ secrets.FULLSEND_GCP_WIF_PROVIDER }}
414414

415415
FULLSEND_GCP_PROJECT_ID: ${{ secrets.FULLSEND_GCP_PROJECT_ID }}
416+
417+
prioritize:
418+
name: Prioritize
419+
needs: route
420+
if: needs.route.outputs.stage == 'prioritize'
421+
uses: ./.fullsend/.github/workflows/prioritize.yml
422+
permissions:
423+
contents: read
424+
id-token: write
425+
with:
426+
event_type: ${{ github.event_name }}
427+
source_repo: ${{ github.repository }}
428+
event_payload: ${{ needs.route.outputs.event_payload }}
429+
secrets:
430+
FULLSEND_GCP_WIF_PROVIDER: ${{ secrets.FULLSEND_GCP_WIF_PROVIDER }}
431+
FULLSEND_GCP_PROJECT_ID: ${{ secrets.FULLSEND_GCP_PROJECT_ID }}

.github/workflows/reusable-fix.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,8 +259,8 @@ jobs:
259259
run: |
260260
if [[ "${TRIGGER_SOURCE}" =~ \[bot\]$ ]]; then
261261
PR_INFO=$(gh pr view "${PR_NUM}" --repo "${SOURCE_REPO}" \
262-
--json labels,author --jq '{labels: [.labels[].name], author: .author.login}' 2>/dev/null \
263-
|| echo '{"labels":[],"author":""}')
262+
--json labels,author --jq '{labels: [.labels[].name], author: .author.login}') \
263+
|| { echo "::error::Failed to fetch PR info for #${PR_NUM}"; exit 1; }
264264
265265
HAS_NO_FIX=$(echo "${PR_INFO}" | jq -r '.labels | any(. == "fullsend-no-fix")')
266266
if [[ "${HAS_NO_FIX}" == "true" ]]; then

.pre-commit-config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ repos:
7070
- SC2016
7171
- -ignore
7272
- 'could not read reusable workflow file for "\./\.github/workflows/prioritize\.yml"'
73+
- -ignore
74+
- 'could not read reusable workflow file for "\./\.fullsend/\.github/workflows/prioritize\.yml"'
7375

7476
- repo: local
7577
hooks:

e2e/admin/admin_test.go

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,11 @@ func runTriageDispatchSmokeTest(t *testing.T, env *e2eEnv) {
250250
t.Helper()
251251
ctx := context.Background()
252252

253+
// Capture a lower-bound timestamp *before* creating the issue.
254+
// The shim/dispatch workflows can start very quickly, and we only want to
255+
// filter out runs from earlier test phases.
256+
issueCreatedAt := time.Now()
257+
253258
// File a test issue to trigger the shim workflow.
254259
issueTitle := fmt.Sprintf("e2e-triage-test-%s", env.runID)
255260
issueBody := `## Bug Report
@@ -291,15 +296,14 @@ Files over 64KB save fine if they contain only ASCII characters.`
291296
}
292297
})
293298

294-
// Wait for dispatch.yml to run in .fullsend (shim → dispatch → reusable-triage).
295-
// The shim fires on issues:opened and runs the triage stage synchronously.
296-
// Filter by CreatedAt to avoid false positives from previous runs.
297-
issueCreatedAt := time.Now()
298-
t.Log("Waiting for dispatch workflow to run...")
299-
var dispatchRun *forge.WorkflowRun
299+
// Wait for the enrolled-repo shim (fullsend.yaml). Cross-repo workflow_call
300+
// runs appear on the caller repo, not as separate dispatch.yml runs in .fullsend.
301+
// Chain: fullsend.yaml → .fullsend/dispatch.yml → reusable-triage (sync).
302+
t.Log("Waiting for fullsend shim workflow to run...")
303+
var shimRun *forge.WorkflowRun
300304
for attempt := 0; attempt < 12; attempt++ {
301305
time.Sleep(5 * time.Second)
302-
runs, listErr := env.client.ListWorkflowRuns(ctx, env.org, forge.ConfigRepoName, "dispatch.yml")
306+
runs, listErr := env.client.ListWorkflowRuns(ctx, env.org, testRepo, "fullsend.yaml")
303307
if listErr != nil {
304308
t.Logf("Attempt %d: error listing workflow runs: %v", attempt+1, listErr)
305309
continue
@@ -316,24 +320,24 @@ Files over 64KB save fine if they contain only ASCII characters.`
316320
}
317321
t.Logf("Attempt %d: found run %d (status: %s, conclusion: %s, created: %s)", attempt+1, run.ID, run.Status, run.Conclusion, run.CreatedAt)
318322
r := run // avoid loop variable capture
319-
dispatchRun = &r
323+
shimRun = &r
320324
break
321325
}
322-
if dispatchRun != nil {
326+
if shimRun != nil {
323327
break
324328
}
325-
t.Logf("Attempt %d: no dispatch workflow runs found yet", attempt+1)
329+
t.Logf("Attempt %d: no fullsend shim workflow runs found yet", attempt+1)
326330
}
327-
require.NotNil(t, dispatchRun, "dispatch workflow should have run in .fullsend repo")
331+
require.NotNil(t, shimRun, "fullsend shim workflow should have run in enrolled repo")
328332

329333
// Wait for the workflow run to complete (up to 12 minutes: 10-minute agent
330334
// timeout + sandbox setup overhead).
331-
t.Logf("Waiting for dispatch workflow run %d to complete...", dispatchRun.ID)
335+
t.Logf("Waiting for fullsend shim workflow run %d to complete...", shimRun.ID)
332336
var finalRun *forge.WorkflowRun
333337
deadline := time.Now().Add(12 * time.Minute)
334338
for time.Now().Before(deadline) {
335339
time.Sleep(15 * time.Second)
336-
run, getErr := env.client.GetWorkflowRun(ctx, env.org, forge.ConfigRepoName, dispatchRun.ID)
340+
run, getErr := env.client.GetWorkflowRun(ctx, env.org, testRepo, shimRun.ID)
337341
if getErr != nil {
338342
t.Logf("Error polling workflow run: %v", getErr)
339343
continue
@@ -344,34 +348,34 @@ Files over 64KB save fine if they contain only ASCII characters.`
344348
break
345349
}
346350
}
347-
require.NotNil(t, finalRun, "dispatch workflow run should have completed within deadline")
351+
require.NotNil(t, finalRun, "fullsend shim workflow run should have completed within deadline")
348352

349353
// If the run failed, save logs and artifacts for debugging.
350354
if finalRun.Conclusion != "success" {
351-
runURL := fmt.Sprintf("https://github.com/%s/%s/actions/runs/%d", env.org, forge.ConfigRepoName, finalRun.ID)
352-
fmt.Fprintf(os.Stderr, "::notice::Dispatch workflow run %d failed (conclusion: %s). Downloading debug artifacts. Run URL: %s\n", finalRun.ID, finalRun.Conclusion, runURL)
355+
runURL := fmt.Sprintf("https://github.com/%s/%s/actions/runs/%d", env.org, testRepo, finalRun.ID)
356+
fmt.Fprintf(os.Stderr, "::notice::Fullsend shim run %d failed (conclusion: %s). Downloading debug artifacts. Run URL: %s\n", finalRun.ID, finalRun.Conclusion, runURL)
353357

354-
debugDir := filepath.Join(env.screenshotDir, fmt.Sprintf("dispatch-run-%d", finalRun.ID))
358+
debugDir := filepath.Join(env.screenshotDir, fmt.Sprintf("fullsend-run-%d", finalRun.ID))
355359
_ = os.MkdirAll(debugDir, 0o755)
356360

357361
// Save workflow logs.
358-
logs, logErr := env.client.GetWorkflowRunLogs(ctx, env.org, forge.ConfigRepoName, finalRun.ID)
362+
logs, logErr := env.client.GetWorkflowRunLogs(ctx, env.org, testRepo, finalRun.ID)
359363
if logErr != nil {
360364
t.Logf("Could not fetch run logs: %v", logErr)
361365
} else {
362366
logPath := filepath.Join(debugDir, "workflow-logs.txt")
363367
if writeErr := os.WriteFile(logPath, []byte(logs), 0o644); writeErr != nil {
364368
t.Logf("Could not write logs to %s: %v", logPath, writeErr)
365369
} else {
366-
fmt.Fprintf(os.Stderr, "::notice file=%s::Dispatch run %d workflow logs saved\n", logPath, finalRun.ID)
370+
fmt.Fprintf(os.Stderr, "::notice file=%s::Fullsend shim run %d workflow logs saved\n", logPath, finalRun.ID)
367371
}
368372
t.Logf("Workflow run logs:\n%s", logs)
369373
}
370374

371375
// Download run artifacts (transcripts, etc).
372-
downloadRunArtifacts(ctx, env.token, env.org, forge.ConfigRepoName, finalRun.ID, debugDir, t)
376+
downloadRunArtifacts(ctx, env.token, env.org, testRepo, finalRun.ID, debugDir, t)
373377

374-
t.Fatalf("Dispatch workflow run %d concluded with %q, expected success. Debug artifacts saved to %s", finalRun.ID, finalRun.Conclusion, debugDir)
378+
t.Fatalf("Fullsend shim workflow run %d concluded with %q, expected success. Debug artifacts saved to %s", finalRun.ID, finalRun.Conclusion, debugDir)
375379
}
376380

377381
// Verify the triage agent posted a comment on the issue.

internal/scaffold/fullsend-repo/.github/workflows/dispatch.yml

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@ jobs:
2525
contents: read
2626
pull-requests: read
2727
outputs:
28-
stage: ${{ steps.role-check.outputs.skipped == 'true' && '' || steps.route.outputs.stage }}
28+
stage: ${{ steps.role-check.outputs.skipped != 'true' && steps.route.outputs.stage || '' }}
2929
trigger_source: ${{ steps.route.outputs.trigger_source }}
3030
event_payload: ${{ steps.payload.outputs.event_payload }}
3131
steps:
3232
- name: Checkout config repository
3333
uses: actions/checkout@v6
3434
with:
35+
repository: ${{ job.workflow_repository }}
3536
persist-credentials: false
3637
sparse-checkout: config.yaml
3738
sparse-checkout-cone-mode: false
@@ -231,7 +232,7 @@ jobs:
231232
set -euo pipefail
232233
STAGE_ROLE="$STAGE"
233234
case "$STAGE" in
234-
code) STAGE_ROLE="coder" ;;
235+
code|fix) STAGE_ROLE="coder" ;;
235236
retro|prioritize) STAGE_ROLE="fullsend" ;;
236237
esac
237238
@@ -292,6 +293,9 @@ jobs:
292293
name: Triage
293294
needs: route
294295
if: needs.route.outputs.stage == 'triage'
296+
permissions:
297+
contents: read
298+
id-token: write
295299
uses: fullsend-ai/fullsend/.github/workflows/reusable-triage.yml@v0
296300
with:
297301
event_type: ${{ github.event_name }}
@@ -308,6 +312,9 @@ jobs:
308312
name: Code
309313
needs: route
310314
if: needs.route.outputs.stage == 'code'
315+
permissions:
316+
contents: read
317+
id-token: write
311318
uses: fullsend-ai/fullsend/.github/workflows/reusable-code.yml@v0
312319
with:
313320
event_type: ${{ github.event_name }}
@@ -324,6 +331,9 @@ jobs:
324331
name: Review
325332
needs: route
326333
if: needs.route.outputs.stage == 'review'
334+
permissions:
335+
contents: read
336+
id-token: write
327337
uses: fullsend-ai/fullsend/.github/workflows/reusable-review.yml@v0
328338
with:
329339
event_type: ${{ github.event_name }}
@@ -340,6 +350,9 @@ jobs:
340350
name: Fix
341351
needs: route
342352
if: needs.route.outputs.stage == 'fix'
353+
permissions:
354+
contents: read
355+
id-token: write
343356
uses: fullsend-ai/fullsend/.github/workflows/reusable-fix.yml@v0
344357
with:
345358
event_type: ${{ github.event_name }}
@@ -357,6 +370,9 @@ jobs:
357370
name: Retro
358371
needs: route
359372
if: needs.route.outputs.stage == 'retro'
373+
permissions:
374+
contents: read
375+
id-token: write
360376
uses: fullsend-ai/fullsend/.github/workflows/reusable-retro.yml@v0
361377
with:
362378
event_type: ${{ github.event_name }}
@@ -374,6 +390,12 @@ jobs:
374390
needs: route
375391
if: needs.route.outputs.stage == 'prioritize'
376392
uses: ./.github/workflows/prioritize.yml
393+
permissions:
394+
contents: read
395+
id-token: write
396+
secrets:
397+
FULLSEND_GCP_WIF_PROVIDER: ${{ secrets.FULLSEND_GCP_WIF_PROVIDER }}
398+
FULLSEND_GCP_PROJECT_ID: ${{ secrets.FULLSEND_GCP_PROJECT_ID }}
377399
with:
378400
event_type: ${{ github.event_name }}
379401
source_repo: ${{ github.repository }}

internal/scaffold/fullsend-repo/.github/workflows/prioritize.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ on:
1212
event_payload:
1313
required: true
1414
type: string
15+
secrets:
16+
FULLSEND_GCP_WIF_PROVIDER:
17+
required: true
18+
FULLSEND_GCP_PROJECT_ID:
19+
required: true
1520
workflow_dispatch:
1621
inputs:
1722
event_type:

internal/scaffold/fullsend-repo/agents/triage.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,17 @@ gh pr view BLOCKING_URL --json state,title,body,comments,labels,mergedAt
7676

7777
Use `gh issue view` for `/issues/` URLs and `gh pr view` for `/pull/` URLs. Review the blocker's state, recent comments, and labels to determine whether the dependency has been resolved, is making progress, or remains stalled. If the blocker has been closed or merged, the block may be resolved — proceed with a fresh assessment.
7878

79+
### 2d. Review prior triage analysis
80+
81+
Check whether this issue has already been triaged. Look through the comments you fetched in Step 1 for a prior triage comment — it will contain `<!-- fullsend:triage-agent -->` in its body, or be posted by a user whose login ends in `-triage[bot]`.
82+
83+
If a prior triage comment exists, **accumulate — do not replace:**
84+
85+
- **Preserve all previously identified problems.** Treat every cause documented in the prior analysis as an established finding. Do not silently drop any of them. If you believe a previously identified cause is no longer valid (e.g., already fixed, confirmed misdiagnosis), document this explicitly in `reasoning` — a cause removed without explanation is a regression in analysis quality.
86+
- **Incorporate human-identified problems.** When an issue author or contributor adds a comment that says "the real issue is X", "you also missed Y", or otherwise points to a problem not in the prior analysis, treat it with the same evidentiary weight as a clear error message. If you cannot independently verify the claim, include it as a hypothesis — do not omit it.
87+
- **Your new analysis must be a superset** of the prior analysis. Identified problems accumulate across triage runs; the count of documented problems can only go up, not down (unless a cause is explicitly refuted with reasoning).
88+
- **Re-triaging is about incorporating new information**, not restarting from scratch. If a human comment triggered this re-run, focus your analysis on what that comment changes or adds. Then confirm all previously documented problems are still represented.
89+
7990
## Step 3: Assess information sufficiency
8091

8192
Use this phased approach to evaluate the issue:

internal/scaffold/fullsend-repo/scripts/pre-code-test.sh

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,11 @@ run_test() {
8686
GH_TOKEN="fake-token"
8787
)
8888

89-
# Add extra env vars if provided.
89+
# Add extra env vars if provided (read line-by-line to support values with spaces).
9090
if [[ -n "${extra_env}" ]]; then
91-
for kv in ${extra_env}; do
92-
env_cmd+=("${kv}")
93-
done
91+
while IFS= read -r kv; do
92+
[[ -n "${kv}" ]] && env_cmd+=("${kv}")
93+
done <<< "${extra_env}"
9494
fi
9595

9696
local exit_code=0
@@ -139,9 +139,9 @@ run_test_stdout() {
139139
)
140140

141141
if [[ -n "${extra_env}" ]]; then
142-
for kv in ${extra_env}; do
143-
env_cmd+=("${kv}")
144-
done
142+
while IFS= read -r kv; do
143+
[[ -n "${kv}" ]] && env_cmd+=("${kv}")
144+
done <<< "${extra_env}"
145145
fi
146146

147147
local exit_code=0
@@ -205,6 +205,13 @@ run_test_stdout "force-override-skips-check" \
205205
0 \
206206
"CODE_FORCE=true"
207207

208+
# COMMENT_BODY contains --force → should also skip check.
209+
run_test_stdout "force-override-comment-body" \
210+
"99${TAB}human-dev${TAB}https://github.com/test-org/test-repo/pull/99" \
211+
"Force override" \
212+
0 \
213+
"COMMENT_BODY=/fs-code --force"
214+
208215
# No GH_TOKEN → skips check entirely, exits 0.
209216
run_test_stdout "no-gh-token-skips-check" \
210217
"" \

internal/scaffold/fullsend-repo/scripts/pre-code.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ if [[ -z "${GH_TOKEN:-}" ]]; then
6060
exit 0
6161
fi
6262

63-
# Allow override via CODE_FORCE (set when /fs-code --force is used).
64-
if [[ "${CODE_FORCE:-}" == "true" ]]; then
63+
# Allow override when --force is in the trigger comment or CODE_FORCE is set.
64+
if [[ "${CODE_FORCE:-}" == "true" ]] || [[ "${COMMENT_BODY:-}" == *--force* ]]; then
6565
echo "CODE_FORCE=true — skipping existing-PR check"
6666
exit 0
6767
fi

0 commit comments

Comments
 (0)