Skip to content
Open
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
11 changes: 9 additions & 2 deletions .github/workflows/reusable-code.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Reusable code agent workflow. Called by thin callers in .fullsend repos
# via workflow_call. Runs in the caller's repo context (secrets, checkout).
# Reusable code agent workflow. Called by dispatch workflows via workflow_call.
# Runs in the caller's repo context (secrets, checkout).
#
# Concurrency: agent-scoped cancel-in-progress group (distinct from dispatch/thin
# caller groups so workflow_call parent runs are not cancelled).
name: Code Agent

on:
Expand Down Expand Up @@ -39,6 +42,10 @@ on:
FULLSEND_GCP_PROJECT_ID:
required: true

concurrency:
group: fullsend-code-agent-${{ inputs.source_repo }}-${{ fromJSON(inputs.event_payload).issue.number }}
cancel-in-progress: true

jobs:
code:
name: Code
Expand Down
23 changes: 23 additions & 0 deletions .github/workflows/reusable-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
# workflow_call jobs. This is the per-repo equivalent of the per-org
# dispatch.yml + thin caller pair.
#
# Concurrency: each stage job declares a per-role cancel-in-progress dispatch group.
# Reusable stage workflows use distinct agent-scoped groups (fullsend-{stage}-agent-…)
# so workflow_call parents are not cancelled. Roles operate independently — review
# dispatches do not cancel triage, code, fix, etc.
#
# Flow: shim (per-repo) → reusable-dispatch.yml → reusable-{stage}.yml
# Nesting: 3 levels of workflow_call (within GitHub's 4-level limit)
#
Expand Down Expand Up @@ -360,6 +365,9 @@ jobs:
name: Triage
needs: route
if: needs.route.outputs.stage == 'triage'
concurrency:
group: fullsend-triage-${{ github.repository }}-${{ github.event.issue.number }}
cancel-in-progress: true
# @v0 is hardcoded — GHA does not support expressions in uses:.
# fullsend_ai_ref controls the ref for composite actions inside stage workflows.
uses: fullsend-ai/fullsend/.github/workflows/reusable-triage.yml@v0
Expand All @@ -380,6 +388,9 @@ jobs:
name: Code
needs: route
if: needs.route.outputs.stage == 'code'
concurrency:
group: fullsend-code-${{ github.repository }}-${{ github.event.issue.number }}
cancel-in-progress: true
uses: fullsend-ai/fullsend/.github/workflows/reusable-code.yml@v0
with:
event_type: ${{ github.event_name }}
Expand All @@ -398,6 +409,9 @@ jobs:
name: Review
needs: route
if: needs.route.outputs.stage == 'review'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] edge-case

The review stage concurrency group uses github.event.pull_request.number || github.event.issue.number. When triggered by issues/labeled vs pull_request_target, different concurrency keys are produced for linked issue/PR pairs with different numbers. Two concurrent review runs are possible for the same logical work item.

concurrency:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] edge-case

The review stage concurrency group uses github.event.pull_request.number || github.event.issue.number. When review is triggered by issues/labeled (ready-for-review), the key uses the issue number; when triggered by pull_request_target, it uses the PR number. For a linked issue/PR pair where the numbers differ, the two triggers produce different concurrency keys and will not cancel each other. Two concurrent review runs are possible for the same logical work item. The same concern applies to fix and retro stages.

group: fullsend-review-${{ github.repository }}-${{ github.event.pull_request.number || github.event.issue.number }}
cancel-in-progress: true
uses: fullsend-ai/fullsend/.github/workflows/reusable-review.yml@v0
with:
event_type: ${{ github.event_name }}
Expand All @@ -416,6 +430,9 @@ jobs:
name: Fix
needs: route
if: needs.route.outputs.stage == 'fix'
concurrency:
group: fullsend-fix-${{ github.repository }}-${{ github.event.pull_request.number || github.event.issue.number }}
cancel-in-progress: true
uses: fullsend-ai/fullsend/.github/workflows/reusable-fix.yml@v0
with:
event_type: ${{ github.event_name }}
Expand All @@ -435,6 +452,9 @@ jobs:
name: Retro
needs: route
if: needs.route.outputs.stage == 'retro'
concurrency:
group: fullsend-retro-${{ github.repository }}-${{ github.event.pull_request.number || github.event.issue.number }}
cancel-in-progress: true
uses: fullsend-ai/fullsend/.github/workflows/reusable-retro.yml@v0
with:
event_type: ${{ github.event_name }}
Expand All @@ -453,6 +473,9 @@ jobs:
name: Prioritize
needs: route
if: needs.route.outputs.stage == 'prioritize'
concurrency:
group: fullsend-prioritize-${{ github.repository }}-${{ github.event.issue.number }}
cancel-in-progress: true
uses: fullsend-ai/fullsend/.github/workflows/reusable-prioritize.yml@v0
with:
event_type: ${{ github.event_name }}
Expand Down
16 changes: 14 additions & 2 deletions .github/workflows/reusable-fix.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Reusable fix agent workflow. Called by thin callers in .fullsend repos
# via workflow_call. Runs in the caller's repo context (secrets, checkout).
# Reusable fix agent workflow. Called by dispatch workflows via workflow_call.
# Runs in the caller's repo context (secrets, checkout).
#
# Concurrency: agent-scoped cancel-in-progress group (distinct from dispatch/thin
# caller groups so workflow_call parent runs are not cancelled).
name: Fix Agent

on:
Expand Down Expand Up @@ -51,6 +54,15 @@ on:
FULLSEND_GCP_PROJECT_ID:
required: true

concurrency:
group: >-
fullsend-fix-agent-${{ inputs.source_repo }}-${{
fromJSON(inputs.event_payload).pull_request.number
|| fromJSON(inputs.event_payload).issue.number
|| inputs.pr_number
}}
cancel-in-progress: true

jobs:
fix:
name: Fix
Expand Down
11 changes: 9 additions & 2 deletions .github/workflows/reusable-prioritize.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Reusable prioritize agent workflow. Called by thin callers in .fullsend repos
# via workflow_call. Runs in the caller's repo context (secrets, checkout).
# Reusable prioritize agent workflow. Called by dispatch workflows via workflow_call.
# Runs in the caller's repo context (secrets, checkout).
#
# Concurrency: agent-scoped cancel-in-progress group (distinct from dispatch/thin
# caller groups so workflow_call parent runs are not cancelled).
name: Prioritize Agent

on:
Expand Down Expand Up @@ -43,6 +46,10 @@ on:
FULLSEND_GCP_PROJECT_ID:
required: true

concurrency:
group: fullsend-prioritize-agent-${{ inputs.source_repo }}-${{ fromJSON(inputs.event_payload).issue.number }}
cancel-in-progress: true

jobs:
prioritize:
name: Prioritize
Expand Down
11 changes: 9 additions & 2 deletions .github/workflows/reusable-retro.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Reusable retro agent workflow. Called by thin callers in .fullsend repos
# via workflow_call. Runs in the caller's repo context (secrets, checkout).
# Reusable retro agent workflow. Called by dispatch workflows via workflow_call.
# Runs in the caller's repo context (secrets, checkout).
#
# Concurrency: agent-scoped cancel-in-progress group (distinct from dispatch/thin
# caller groups so workflow_call parent runs are not cancelled).
name: Retro Agent

on:
Expand Down Expand Up @@ -39,6 +42,10 @@ on:
FULLSEND_GCP_PROJECT_ID:
required: true

concurrency:
group: fullsend-retro-agent-${{ inputs.source_repo }}-${{ fromJSON(inputs.event_payload).pull_request.number || fromJSON(inputs.event_payload).issue.number }}
cancel-in-progress: true

jobs:
retro:
name: Retro
Expand Down
11 changes: 9 additions & 2 deletions .github/workflows/reusable-review.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Reusable review agent workflow. Called by thin callers in .fullsend repos
# via workflow_call. Runs in the caller's repo context (secrets, checkout).
# Reusable review agent workflow. Called by dispatch workflows via workflow_call.
# Runs in the caller's repo context (secrets, checkout).
#
# Concurrency: agent-scoped cancel-in-progress group (distinct from dispatch/thin
# caller groups so workflow_call parent runs are not cancelled).
name: Review Agent

on:
Expand Down Expand Up @@ -39,6 +42,10 @@ on:
FULLSEND_GCP_PROJECT_ID:
required: true

concurrency:
group: fullsend-review-agent-${{ inputs.source_repo }}-${{ fromJSON(inputs.event_payload).pull_request.number || fromJSON(inputs.event_payload).issue.number }}
cancel-in-progress: true

jobs:
review:
name: Review
Expand Down
12 changes: 10 additions & 2 deletions .github/workflows/reusable-triage.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Reusable triage agent workflow. Called by thin callers in .fullsend repos
# via workflow_call. Runs in the caller's repo context (secrets, checkout).
# Reusable triage agent workflow. Called by dispatch workflows via workflow_call.
# Runs in the caller's repo context (secrets, checkout).
#
# Concurrency: agent-scoped cancel-in-progress group (distinct from dispatch/thin
# caller groups so workflow_call parent runs are not cancelled). Latest agent
# run for an issue wins.
name: Triage Agent

on:
Expand Down Expand Up @@ -39,6 +43,10 @@ on:
FULLSEND_GCP_PROJECT_ID:
required: true

concurrency:
group: fullsend-triage-agent-${{ inputs.source_repo }}-${{ fromJSON(inputs.event_payload).issue.number }}
cancel-in-progress: true

jobs:
triage:
name: Triage
Expand Down
5 changes: 5 additions & 0 deletions docs/ADRs/0034-centralized-shim-routing-via-dispatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,8 @@ The `stage` input to `dispatch.yml` becomes optional. When provided
the same routing script. Per-org shims could also adopt
`reusable-fullsend.yml` directly, eliminating `dispatch.yml` as a
routing layer entirely — see ADR 0033 Open Questions.

## Implementation note (#981)

Per-role dispatch concurrency is configured in repository workflows; the routing
model in this ADR is unchanged.
5 changes: 5 additions & 0 deletions docs/ADRs/0041-synchronous-workflow-call-event-dispatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,8 @@ re-evaluate whether a discovery mechanism is needed.
`needs:` / concurrency review ([#504](https://github.com/fullsend-ai/fullsend/issues/504)).
- Discovery may be revisited after [ADR 0038](0038-universal-harness-access.md)
agent architecture changes land.

## Implementation note (#981)

Per-repo concurrency follow-up ([#504](https://github.com/fullsend-ai/fullsend/issues/504))
is addressed in workflow configuration; the dispatch shape in this ADR is unchanged.
7 changes: 4 additions & 3 deletions internal/scaffold/fullsend-repo/templates/shim-per-repo.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
# which determines the stage and conditionally calls the appropriate
# reusable-{stage}.yml workflow. Adding a new stage requires only a case
# branch in reusable-dispatch.yml — zero changes to this repo.
#
# Concurrency: per-role cancel-in-progress groups live in reusable-dispatch.yml
# stage jobs and agent-scoped groups on reusable-{stage}.yml — not on this shim.
# A monolithic shim group would serialize unrelated roles and drop pending runs (#2452).
name: fullsend

permissions:
Expand All @@ -35,9 +39,6 @@ on:

jobs:
dispatch:
concurrency:
group: fullsend-dispatch-${{ github.event.issue.number || github.event.pull_request.number }}
cancel-in-progress: false
if: >-
github.event_name != 'issue_comment'
|| github.event.comment.user.type != 'Bot'
Expand Down
15 changes: 15 additions & 0 deletions internal/scaffold/scaffold_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,21 @@ func TestShimWorkflowCallTemplateContent(t *testing.T) {
assert.NotContains(t, s, "curl")
}

func TestShimPerRepoTemplateContent(t *testing.T) {
content, err := FullsendRepoFile("templates/shim-per-repo.yaml")
require.NoError(t, err)
s := string(content)
assert.True(t, strings.HasPrefix(s, "---\n"), "per-repo shim must start with YAML document start marker")
assert.Contains(t, s, "dispatch:")
assert.Contains(t, s, "stop-fix:")
assert.Contains(t, s, "__REUSABLE_DISPATCH__")
assert.Contains(t, s, "install_mode: per-repo")
// Per-role concurrency lives in reusable-dispatch.yml, not a monolithic shim group (#2452).
assert.NotContains(t, s, "fullsend-dispatch-${{")
assert.NotContains(t, s, "concurrency:")
assert.Contains(t, s, "per-role cancel-in-progress groups live in reusable-dispatch.yml")
}

func TestShimTriggerParity(t *testing.T) {
// Both shim templates must declare the same event trigger types so that
// per-repo and workflow-call installation modes have identical behavior.
Expand Down
Loading
Loading