Skip to content

Commit 70b0280

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>
1 parent 557c374 commit 70b0280

9 files changed

Lines changed: 87 additions & 43 deletions

File tree

.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/scripts/pre-code-test.sh

Lines changed: 7 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

internal/scaffold/fullsend-repo/skills/finding-agent-runs/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ gh run list --workflow=fullsend.yaml \
8181
Confirm `dispatch-review completed/success`, then find the run:
8282

8383
```bash
84-
gh run list --repo "${DISPATCH_REPO}" --workflow=review.yml --limit 5 \
84+
gh run list --repo "${DISPATCH_REPO}" --workflow=dispatch.yml --limit 5 \
8585
--json databaseId,status,conclusion,createdAt
8686
```
8787

@@ -99,7 +99,7 @@ gh run list --workflow=fullsend.yaml \
9999
Find the actual retro agent run:
100100

101101
```bash
102-
gh run list --repo "${DISPATCH_REPO}" --workflow=retro.yml --limit 5 \
102+
gh run list --repo "${DISPATCH_REPO}" --workflow=dispatch.yml --limit 5 \
103103
--json databaseId,status,conclusion,createdAt
104104
```
105105

internal/scaffold/scaffold_test.go

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,9 @@ func TestDispatchWorkflowContent(t *testing.T) {
231231
assert.Contains(t, s, "install_mode: per-org")
232232
assert.Contains(t, s, "permissions: {}")
233233
assert.Contains(t, s, "sparse-checkout: config.yaml")
234+
assert.Contains(t, s, "repository: ${{ job.workflow_repository }}")
235+
assert.Contains(t, s, "FULLSEND_GCP_WIF_PROVIDER")
236+
assert.Contains(t, s, "FULLSEND_GCP_PROJECT_ID")
234237
assert.Contains(t, s, "set -euo pipefail")
235238
assert.Contains(t, s, "Invalid stage name")
236239
assert.Contains(t, s, `^[a-z][a-z0-9_-]*$`)
@@ -389,14 +392,6 @@ func TestCodeAgentContent(t *testing.T) {
389392
assert.Contains(t, s, "code-implementation")
390393
}
391394

392-
func TestCodeImplementationSkillAPIContractGuidance(t *testing.T) {
393-
content, err := FullsendRepoFile("skills/code-implementation/SKILL.md")
394-
require.NoError(t, err)
395-
s := string(content)
396-
assert.Contains(t, s, "Verify API contracts per code path")
397-
assert.Contains(t, s, "or changes a parameter sent to an external API")
398-
}
399-
400395
func TestSetupGcpActionContent(t *testing.T) {
401396
content, err := FullsendRepoFile(".github/actions/setup-gcp/action.yml")
402397
require.NoError(t, err)

0 commit comments

Comments
 (0)