|
14 | 14 | # |
15 | 15 | # SPDX-License-Identifier: Apache-2.0 |
16 | 16 |
|
17 | | -# lint-workflow-size: max-lines=280 |
18 | | -# fullsend shim workflow (workflow_call mode) |
19 | | -# Routes events to agent workflows in .fullsend via workflow_call. |
20 | | -# No secrets are needed in the enrolled repo — agents fetch scoped tokens |
21 | | -# from the centralized token mint using GitHub OIDC. |
| 17 | +# This file is managed by fullsend. Do not edit it directly. |
| 18 | +# Upstream: https://github.com/fullsend-ai/fullsend/blob/main/internal/scaffold/fullsend-repo/.github/workflows/fullsend.yaml |
| 19 | +--- |
| 20 | +# fullsend shim workflow (per-repo installation mode) |
| 21 | +# Routes events to agent workflows via reusable-dispatch.yml. |
| 22 | +# All agent execution happens in this repo's context — no external |
| 23 | +# config repo is needed. |
22 | 24 | # |
23 | 25 | # Security: pull_request_target runs the BASE branch version of this workflow, |
24 | 26 | # preventing PRs from modifying it to exfiltrate credentials. |
25 | 27 | # This shim never checks out PR code, so it is not vulnerable to "pwn request" |
26 | 28 | # attacks. |
27 | 29 | # |
28 | | -# conforma is replaced by reconcile-repos.sh at deploy time. |
| 30 | +# Routing: this shim forwards the raw event context to reusable-dispatch.yml, |
| 31 | +# which determines the stage and runs the agent inline (ADR 62). |
| 32 | +# Adding a new stage requires only a job in reusable-dispatch.yml — zero changes to this repo. |
29 | 33 | # |
30 | | -# Routing: this shim forwards the raw event context to dispatch.yml, which |
31 | | -# determines the stage from event_type + event_action + payload fields. |
32 | | -# Adding a new stage requires only a case branch in dispatch.yml — zero |
33 | | -# changes to enrolled repos. |
| 34 | +# Concurrency: per-role cancel-in-progress groups live in reusable-dispatch.yml |
| 35 | +# stage jobs with -agent- suffix. Roles operate independently (#2452). |
34 | 36 | name: fullsend |
35 | 37 |
|
36 | | -permissions: |
37 | | - actions: write |
38 | | - id-token: write |
39 | | - contents: read |
40 | | - pull-requests: read |
41 | | - |
42 | 38 | on: |
43 | 39 | issues: |
44 | 40 | types: [opened, edited, labeled] |
45 | 41 | issue_comment: |
46 | 42 | types: [created] |
47 | 43 | pull_request_target: |
48 | | - types: [opened, synchronize, ready_for_review, closed] |
| 44 | + types: [opened, synchronize, ready_for_review, closed, labeled, unlabeled] |
49 | 45 | pull_request_review: |
50 | 46 | types: [submitted] |
51 | 47 |
|
| 48 | +permissions: {} |
| 49 | + |
52 | 50 | jobs: |
53 | 51 | dispatch: |
54 | | - concurrency: |
55 | | - group: fullsend-dispatch-${{ github.event.issue.number || github.event.pull_request.number }} |
56 | | - cancel-in-progress: false |
57 | 52 | if: >- |
58 | | - github.event_name != 'issue_comment' |
59 | | - || github.event.comment.user.type != 'Bot' |
60 | | - uses: conforma/.fullsend/.github/workflows/dispatch.yml@cd011389fdff80dc8ef963f82f7f200901e62f5c # main |
| 53 | + (github.event_name != 'pull_request_target' && github.event_name != 'pull_request_review' |
| 54 | + || github.event.pull_request.head.ref != 'fullsend/scaffold-install') |
| 55 | + && (github.event_name != 'issue_comment' |
| 56 | + || (startsWith(github.event.comment.body, '/fs-') |
| 57 | + && github.event.comment.user.type != 'Bot')) |
| 58 | + permissions: |
| 59 | + actions: write |
| 60 | + id-token: write |
| 61 | + contents: write |
| 62 | + issues: write |
| 63 | + packages: read |
| 64 | + pull-requests: write |
| 65 | + uses: fullsend-ai/fullsend/.github/workflows/reusable-dispatch.yml@main |
61 | 66 | with: |
62 | 67 | event_action: ${{ github.event.action }} |
| 68 | + install_mode: per-repo |
| 69 | + mint_url: ${{ vars.FULLSEND_MINT_URL }} |
| 70 | + gcp_region: ${{ vars.FULLSEND_GCP_REGION }} |
| 71 | + project_number: ${{ vars.FULLSEND_PROJECT_NUMBER }} |
| 72 | + runner_image: ubuntu-24.04 |
| 73 | + secrets: |
| 74 | + FULLSEND_GCP_WIF_PROVIDER: ${{ secrets.FULLSEND_GCP_WIF_PROVIDER }} |
| 75 | + FULLSEND_GCP_PROJECT_ID: ${{ secrets.FULLSEND_GCP_PROJECT_ID }} |
| 76 | + FULLSEND_OPENAI_API_KEY: ${{ secrets.FULLSEND_OPENAI_API_KEY }} |
| 77 | + OTEL_EXPORTER_OTLP_TRACES_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_TRACES_HEADERS }} |
| 78 | + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }} |
63 | 79 |
|
64 | 80 | stop-fix: |
| 81 | + # Job-level if: is intentionally coarse — it only screens for the |
| 82 | + # /fs-fix-stop command on a PR from a non-bot. The authoritative |
| 83 | + # authorization decision (collaborator permission API + PR-author escape |
| 84 | + # hatch) is made in the step below, so a maintainer whose author_association |
| 85 | + # is not MEMBER (e.g. private org membership) is not filtered out (ADR 0054). |
65 | 86 | if: >- |
66 | 87 | github.event_name == 'issue_comment' |
67 | 88 | && github.event.issue.pull_request |
68 | 89 | && github.event.comment.user.type != 'Bot' |
69 | 90 | && github.event.comment.body == '/fs-fix-stop' |
70 | | - && ( |
71 | | - github.event.comment.author_association == 'OWNER' |
72 | | - || github.event.comment.author_association == 'MEMBER' |
73 | | - || github.event.comment.author_association == 'COLLABORATOR' |
74 | | - || github.event.comment.author_association == 'CONTRIBUTOR' |
75 | | - || github.event.comment.user.login == github.event.issue.user.login |
76 | | - ) |
77 | | - runs-on: ubuntu-latest |
| 91 | + runs-on: ubuntu-24.04 |
78 | 92 | permissions: |
79 | 93 | contents: read |
80 | 94 | issues: write |
|
85 | 99 | GH_TOKEN: ${{ github.token }} |
86 | 100 | PR_NUMBER: ${{ github.event.issue.number }} |
87 | 101 | REPO: ${{ github.repository }} |
| 102 | + COMMENT_USER_LOGIN: ${{ github.event.comment.user.login }} |
| 103 | + ISSUE_USER_LOGIN: ${{ github.event.issue.user.login }} |
88 | 104 | run: | |
| 105 | + set -euo pipefail |
| 106 | + # ADR 0054: authorize via the collaborator permission API |
| 107 | + # (admin|maintain|write), not author_association — the latter grants |
| 108 | + # contributor status to anyone with a single merged PR (issue #5421). |
| 109 | + # Mirrors has_repo_permission() in dispatch.yml; keep the two in sync. |
| 110 | + # The PR author may always stop the fix agent on their own PR. |
| 111 | + authorized=false |
| 112 | + if [[ -n "$COMMENT_USER_LOGIN" && "$COMMENT_USER_LOGIN" == "$ISSUE_USER_LOGIN" ]]; then |
| 113 | + authorized=true |
| 114 | + else |
| 115 | + if api_err=$(mktemp); then |
| 116 | + if role=$(gh api "repos/$REPO/collaborators/$COMMENT_USER_LOGIN/permission" \ |
| 117 | + --jq '.role_name' 2>"$api_err"); then |
| 118 | + case "$role" in |
| 119 | + admin|maintain|write) authorized=true ;; |
| 120 | + esac |
| 121 | + else |
| 122 | + echo "::warning::Permission API call failed for $COMMENT_USER_LOGIN: $(cat "$api_err")" |
| 123 | + fi |
| 124 | + rm -f "$api_err" |
| 125 | + else |
| 126 | + echo "::warning::Failed to create temp file for permission check of $COMMENT_USER_LOGIN" |
| 127 | + fi |
| 128 | + fi |
| 129 | + if [[ "$authorized" != "true" ]]; then |
| 130 | + echo "::notice::User $COMMENT_USER_LOGIN is not authorized to stop the fix agent (requires write access or PR authorship)" |
| 131 | + exit 0 |
| 132 | + fi |
89 | 133 | gh label create "fullsend-no-fix" --repo "$REPO" \ |
90 | 134 | --description "Skip bot-triggered fix agent runs" --color "FBCA04" \ |
91 | 135 | --force 2>/dev/null || true |
|
0 commit comments