31. Reusable workflows for action-installed distribution
Date: 2026-05-06
Status
Accepted
Context
fullsend admin install copies ~80 files from the Go binary's embedded scaffold (internal/scaffold/fullsend-repo/) into each org's .fullsend repo. This includes agent workflows (78–305 lines each), four composite actions (fullsend, mint-token, validate-enrollment, setup-gcp), setup scripts, and a dispatcher. When a bug is fixed or a security patch lands in the scaffold, every org must re-run fullsend admin install to pick up the change. Workflow drift across orgs is the norm.
The dispatch chain established in ADR 0026 — shim → dispatch.yml → agent workflows — is preserved. Only the agent workflows themselves change from full copies to thin callers.
Options
Option A: Scaffold copies (status quo)
fullsend admin install writes full agent workflows into .fullsend. Each org gets its own copy. Updates require re-running install in every org.
Option B: Published composite actions only
Publish the four composite actions at fullsend-ai/fullsend@v0. Agent workflows in .fullsend replace uses: ./.github/actions/* with published references. Infrastructure logic (checkout, token minting, GCP auth, sandbox setup) stays duplicated in each org's workflow files.
Option C: Reusable workflows + published composite actions
Publish reusable workflows (workflow_call) and composite actions from fullsend-ai/fullsend. Agent workflows in .fullsend shrink to ~40–70 line thin callers that delegate infrastructure logic upstream via workflow_call with explicit secret passthrough. Org-specific content (agents, harness, env, policies, scripts) stays local.
Decision
Use Option C. Publish reusable workflows (fullsend-ai/fullsend/.github/workflows/reusable-{agent}.yml) and composite actions (fullsend-ai/fullsend@v0, plus mint-token, validate-enrollment, setup-gcp under .github/actions/).
Thin callers in .fullsend use workflow_call to invoke upstream reusable workflows. Since workflow_call runs the callee in the caller's repo context, the reusable workflow has access to .fullsend secrets and checks out the config repo directly. Secrets pass via explicit secrets: declarations — each thin caller lists only the secrets the reusable workflow needs (GCP credentials), following least-privilege over blanket secrets: inherit. Org-specific vars.* values (mint URL, GCP region, auth mode) are automatically available in the reusable workflow from the caller's context, but thin callers pass them as explicit inputs.* to make the interface contract self-documenting, ensure missing variables surface visibly with org-appropriate defaults, and provide an auditable manifest of configuration flowing across the trust boundary. Each reusable workflow also declares agent-specific inputs (e.g., trigger_source, pr_number, instruction for the fix workflow) beyond the common set.
Composite actions referenced via fully-qualified paths (fullsend-ai/fullsend/.github/actions/*@v0) in reusable workflows resolve to the upstream repo, not the caller's repo. This is why the four composite actions must live upstream. run: steps execute in the caller's workspace, so scripts in .fullsend/scripts/ remain accessible.
dispatch.yml is enhanced with centralized routing logic (ADR 0034). Thin callers retain # fullsend-stage: markers so dispatch can route events to the correct agent workflow.
The dispatch chain uses 1 level of workflow_call nesting (limit is 4):
shim ──workflow_call──> .fullsend/dispatch.yml
──workflow_dispatch──> .fullsend/code.yml (thin caller)
──workflow_call──> reusable-code.yml (level 1)
──uses──> fullsend-ai/fullsend@v0 (composite action)The workflow_dispatch from dispatch to thin caller is an API call (gh workflow run), which starts a new workflow run and resets the workflow_call nesting counter. The reusable workflow sees only 1 level of nesting from the thin caller's perspective.
Consequences
- Infrastructure patches (token minting, GCP auth, sandbox setup) ship once upstream and propagate to all orgs on next workflow run — no re-install required.
fullsend-ai/fullsendmust remain public forworkflow_callanduses:references to resolve (it already is).- Thin callers map
vars.*to explicitinputs.*even thoughvarsare automatically available from the caller's context. This makes the reusable workflow's parameter contract visible in the caller's YAML, surfaces missing variables with explicit defaults instead of silent empty strings, and creates a natural review checkpoint when the upstream interface changes. - Thin callers pin upstream by tag (
@v0) or SHA — orgs control when they adopt upstream changes. SHA pinning recommended for production since tags are mutable. - Stage-based dispatch (ADR 0026), shim workflows, and org-specific content (agents, harness, policies, scripts) are unchanged.
- Trust boundary shift: Thin callers pass GCP credential secrets to reusable workflow code hosted in
fullsend-ai/fullsend(a public repo) via explicitsecrets:declarations (notsecrets: inherit, limiting exposure to only the secrets the reusable workflow declares). Under the scaffold-copy model, secrets are consumed by code in the org's own repo. Under the reusable-workflow model, secrets flow to upstream code. SHA pinning (not just tag pinning) gives orgs full control over which upstream code runs with their secrets. This aligns with the project's threat model priority on external injection — a compromised upstream ref could affect all downstream orgs simultaneously, making SHA pinning the recommended default for production installations. - Upstream availability:
fullsend-ai/fullsendbecomes a runtime dependency for all downstream orgs. If the repo is unavailable or a pinned ref is deleted, downstream workflow runs fail. Scaffold copies are immune to upstream outages after install. - Cross-repo debugging: When reusable workflows fail, the call stack spans two repositories (the org's
.fullsendandfullsend-ai/fullsend). Developers must inspect both the thin caller and the upstream workflow to diagnose failures. GitHub's workflow run UI shows reusable workflow steps inline, which partially mitigates this. - GitHub-specific mechanism:
workflow_callandsecrets:passthrough are GitHub Actions primitives with no direct equivalent in other CI systems. Multi-forge support (ADR 0028, deprecated) will need its own distribution mechanism (e.g., GitLab CI/CD Components orinclude:) independent of this ADR. - Scaffold output changes:
fullsend admin installwill emit thin callers (~40–70 lines each) instead of full agent workflows (78–305 lines each). This is a user-visible change — orgs runningadmin installafter this change ships will see substantially different workflow files in.fullsend. - Token generation uses OIDC: Reusable workflows use the
mint-tokencomposite action for OIDC-based token minting (ADR 0029). Each reusable workflow requests a scoped token for its role (triage, coder, review, fullsend) — no PEMs or App secrets in the calling repo. The fix workflow reuses the coder role.
