Workflow Contracts
Dispatch routing: GitHub stage routing lives in the repo's reusable-dispatch.yml (at .github/workflows/reusable-dispatch.yml), which per-repo shims call. The per-org scaffold dispatch.yml was removed with per-org mode (#8067), so there is no second copy to keep in sync. The GitLab scaffold no longer uses a native merge_request_event dispatch path (see #7322); the leftover fullsend-dispatch.yml stub was dropped in #7707 and the version marker now lives in fullsend-pipeline.yml. GitLab events use a hybrid dispatch model. Where the webhook fast path is provisioned, a project webhook delivers issue, merge-request, and note events conditionally through a pipeline-trigger URL to fullsend-dispatcher.yml; the cron poller (fullsend-poll.yml) remains the backstop for every event, including MR open, merge, and closed-unmerged. Provisioning is deferred, and polling is the active path, whenever a readiness gate is unmet (for example the dispatcher, wrapper, or default-branch protection is not ready, or the project has other jobs or dispatcher execution configuration a trigger pipeline would run) or the trigger token's owner would hold more than Developer access. Either way the pipelines run as API-triggered pipelines on the protected default branch. Protected CI/CD variables are unavailable on unprotected MR refs, which is why native MR pipelines cannot run agent stages. When built-in harness triggers land (#2896-2901), poller routing can be replaced by fullsend dispatch --input-driver json.
Secret threading: GHA reusable workflows do not inherit secrets — they must be explicitly forwarded by every caller. (Repository and organization-level vars are automatically visible inside called reusable workflows and do not need forwarding.) When any reusable workflow (.github/workflows/reusable-*.yml) adds a new secrets: or inputs: (workflow_call) entry, trace the caller chains and ensure every hop forwards the new entry:
- Per-repo chain: shim template →
reusable-dispatch.yml(stage logic for triage/code/review/fix/retro/prioritize is inlined directly as jobs per ADR 62 — there is no separatereusable-<agent>.ymlhop for the dispatch path; thread new secrets/inputs into the relevant inline job). Exception:prioritize.ymlis installed per-repo as a thin caller that receivesworkflow_dispatchfrom the scheduler and calls the remaining standalonereusable-prioritize.ymldirectly, bypassing the dispatch shim. The per-org scaffold thin callers and standalone stage workflows were removed (ADR 44).
Silent failures and required-flag consistency: Omitting a secret that is required: true at every hop in the chain fails loudly at workflow-call validation time and self-enforces. However, a secret whose required flag is false at any upstream hop can still arrive as an empty string at a downstream required: true consumer — GitHub Actions' required-secret validation only checks key presence, not that the resolved value is non-empty. Per-repo GCP inputs are optional because an OpenAI-only repository does not use them; for the fleet harnesses' optional GCP mount, fullsend run rejects missing credentials before the pre-script on Vertex. Treat a missing forwarding hop as a correctness bug, not a cosmetic issue.
OWNERS-file authorization: has_repo_permission in reusable-dispatch.yml supports an opt-in OWNERS-file path gated by owners_file in the authorization providers list in .fullsend/config.yaml. When enabled, the function checks the repo-root OWNERS (and OWNERS_ALIASES if present) before falling back to the collaborator API. Approvers get write-equivalent access; reviewers get triage-equivalent. TestDispatchPerStageAuthorization checks has_repo_permission including OWNERS role-mapping invariants. The Go harness-dispatch path (internal/harnessdispatch/core.go) has equivalent OWNERS resolution via internal/owners; changes to the OWNERS schema or role mapping must be kept in sync across both implementations.
Security — consuming threaded inputs: When a newly-threaded entry carries user- or event-controlled data, consume it via env: in the final run: step — never interpolate ${{ ... }} directly into a shell block (see the Security note atop reusable-dispatch.yml). This prevents the GHA script-injection class of bugs the project defends against elsewhere.
When reviewing PRs: If a diff adds or renames a secrets: or inputs: entry in a reusable workflow, check that all callers have been updated. Flag a missing forwarding hop as a medium-severity or higher finding. New secrets/inputs must be forwarded only to the hop(s)/stage(s) that need them — do not use secrets: inherit as a substitute for explicit forwarding (OTEL_EXPORTER_OTLP_TRACES_HEADERS, for example, is explicitly forwarded to every inline stage job in reusable-dispatch.yml because each stage runs an agent that emits traces). workflow_call_alignment_test.go already automates much of this verification — see TestWorkflowCallInputAlignment (validates required inputs/secrets are threaded through the scaffold thin callers) and TestOTELHeadersSecretThreading (bespoke test for optional secrets). For new optional secrets/inputs, extend those tests or add a similar one rather than relying solely on manual tracing.
