Skip to content

Feature Request: Cross-repo workflow_call validation and docs #20249

@mvdbos

Description

@mvdbos

Feature Request: Cross-repo workflow_call validation and docs

Use Case

We are building a multi-tenant agentic platform in a central platform repo, triggered by users across thousands of application repos. Today an external compute backend bridges the cross-repo trigger gap (webhook → repository_dispatch). A cross-repo workflow_call stub eliminates that entirely:

# .github/workflows/platform-relay.yml — in application repo
on:
  issue_comment:
    types: [created]
jobs:
  relay:
    uses: <org>/platform-repo/.github/workflows/platform-gateway.lock.yml@v1
    with:
      issue_number: ${{ github.event.issue.number }}
      source_repo: ${{ github.repository }}
    secrets: inherit

Additional and crucial benefit: secrets: inherit gives the caller's COPILOT_GITHUB_TOKEN, so premium requests bill to the caller, not to the platform.


Confirmed Blocker: runtime-import fails cross-repo

The compiled lock file contains a {{#runtime-import .github/workflows/<name>.md}} directive that reads the workflow's Markdown source from the checked-out workspace at runtime. Cross-repo, actions/checkout (without explicit repository:) checks out the caller's repo, because github.repository is the caller in workflow_call context. The platform repo's .md file is not on disk. interpolate_prompt.cjs fails:

ERR_SYSTEM: Runtime import file not found:
  /home/runner/work/consumer-repo/consumer-repo/.github/workflows/cross-repo-probe.md

Analysis

Root Cause

generateCheckoutGitHubFolderForActivation() in pkg/workflow/compiler_activation_job.go (line ~429) emits an actions/checkout step without an explicit repository: parameter. In a workflow_call context, this checks out the caller's repo instead of the callee's (platform) repo. The callee's .md files are not on disk, so processRuntimeImport() in actions/setup/js/runtime_import.cjs throws "Runtime import file not found".

Design Decision: Conditional Checkout (not Inlining)

Two approaches were evaluated:

  1. Compile-time inlining — embed .md content into the lock file when workflow_call is present. Loses edit-without-recompile, increases lock file size.
  2. Conditional callee checkout ✅ — modify the checkout step to use ${{ github.event_name == 'workflow_call' && github.action_repository || github.repository }}. Preserves runtime-import, minimal change, works with mixed triggers.

Option 2 was chosen because:

  • It preserves the runtime-import pattern (edit-without-recompile)
  • It's a small, additive compiler change
  • The explicit event_name check is defensively correct — doesn't depend on knowledge of when github.action_repository is populated
  • It works with any trigger combination (workflow_call + workflow_dispatch, workflow_call + issue_comment, etc.)

Tested Cross-repo Behaviors (all confirmed working)

  1. GITHUB_REPOSITORY = caller's repo ✅
  2. GITHUB_ACTION_REPOSITORY = callee repo ✅
  3. GITHUB_WORKFLOW_REF contains callee reference ✅
  4. All GH_AW vars set ✅
  5. ./actions/setup resolves to pinned remote action in release mode ✅

The only remaining failure mode is the runtime-import step, which this plan fixes.


Implementation Plan

Please implement the following changes:

1. Add hasWorkflowCallTrigger() helper (pkg/workflow/compiler_workflow_call.go)

Add a new exported helper function alongside the existing injectWorkflowCallOutputs() function. Follow the same detection pattern already used at line 31 (strings.Contains(onSection, "workflow_call")):

// hasWorkflowCallTrigger checks if the on section contains a workflow_call trigger.
// Used to detect cross-repo reusable workflow usage for checkout and error handling.
func hasWorkflowCallTrigger(onSection string) bool {
    return strings.Contains(onSection, "workflow_call")
}

2. Modify activation job checkout (pkg/workflow/compiler_activation_job.go)

In generateCheckoutGitHubFolderForActivation() (currently starts at line ~407):

Add a new conditional block after the existing action-tag check (line ~417) and before the default checkout block (line ~428). When workflow_call is detected in data.On, emit a checkout step with a conditional repository: parameter:

// For workflow_call triggers, checkout the callee repository using a conditional expression.
// github.action_repository points to the callee (platform) repo during workflow_call;
// for other event types the explicit event_name check short-circuits to falsy and we
// fall back to github.repository. This supports mixed triggers (e.g., workflow_call + workflow_dispatch).
if hasWorkflowCallTrigger(data.On) {
    compilerActivationJobLog.Print("Adding cross-repo-aware .github checkout for workflow_call trigger")
    return []string{
        "      - name: Checkout .github and .agents folders\n",
        fmt.Sprintf("        uses: %s\n", GetActionPin("actions/checkout")),
        "        with:\n",
        "          repository: ${{ github.event_name == 'workflow_call' && github.action_repository || github.repository }}\n",
        "          sparse-checkout: |\n",
        "            .github\n",
        "            .agents\n",
        "          sparse-checkout-cone-mode: true\n",
        "          fetch-depth: 1\n",
        "          persist-credentials: false\n",
    }
}

The existing default checkout block (without repository:) remains unchanged as the fallback for workflows without workflow_call.

3. Add cross-repo secret guidance step (pkg/workflow/compiler_activation_job.go)

In buildActivationJob() (starts at line ~18):

Add a new conditional block after the secret validation step (after line ~61, after the else branch that logs "Skipped validate-secret step"). When workflow_call is a trigger, inject an additional step that only runs on failure in a workflow_call context:

// Add cross-repo setup guidance when workflow_call is a trigger.
// This step only runs when secret validation fails in a workflow_call context,
// providing actionable guidance to the caller team about configuring secrets.
if hasWorkflowCallTrigger(data.On) {
    compilerActivationJobLog.Print("Adding cross-repo setup guidance step for workflow_call trigger")
    steps = append(steps, "      - name: Cross-repo setup guidance\n")
    steps = append(steps, "        if: failure() && github.event_name == 'workflow_call'\n")
    steps = append(steps, "        run: |\n")
    steps = append(steps, "          echo \"::error::COPILOT_GITHUB_TOKEN must be configured in the CALLER repository's secrets.\"\n")
    steps = append(steps, "          echo \"::error::For cross-repo workflow_call, secrets must be set in the repository that triggers the workflow.\"\n")
    steps = append(steps, "          echo \"::error::See: https://github.github.com/gh-aw/patterns/central-repo-ops/#cross-repo-setup\"\n")
}

4. Add tests for trigger detection (pkg/workflow/compiler_workflow_call_test.go)

Add a new test function TestHasWorkflowCallTrigger in the existing test file. Test cases:

  • workflow_call present in map format → true
  • workflow_call present with inputs → true
  • workflow_call absent (only push + workflow_dispatch) → false
  • workflow_call with other triggers (issue_comment + workflow_call) → true
  • Empty string → false
  • workflow_dispatch only (not workflow_call) → false

5. Add tests for cross-repo checkout generation (pkg/workflow/compiler_activation_job_test.go)

Add a new test function TestGenerateCheckoutGitHubFolderForActivation_WorkflowCall. Test cases:

  • workflow_call trigger present → checkout includes repository: with expression github.event_name == 'workflow_call' && github.action_repository || github.repository
  • workflow_call trigger with inputs + mixed triggers → same cross-repo checkout
  • No workflow_call trigger → checkout does NOT include repository: parameter
  • issue_comment only → checkout does NOT include repository:
  • Action-tag specified with workflow_call → no checkout emitted (existing behavior preserved)

All test cases should verify:

  • .github and .agents are in sparse-checkout
  • persist-credentials: false is present
  • fetch-depth: 1 is present

6. Update glossary (docs/src/content/docs/reference/glossary.md)

Replace the current ### Trigger File entry (line ~217) with an expanded version that mentions cross-repo usage:

A plain GitHub Actions workflow (.yml) that separates trigger definitions from agentic workflow logic. Calls a compiled orchestrator's workflow_call entry point in response to any GitHub event (issues, pushes, labels, manual dispatch). Decouples trigger changes from the compilation cycle — updating when an orchestrator runs requires editing only the trigger file, not recompiling the agentic workflow.

Trigger files can live in the same repository as the orchestrator or in a different repository (cross-repo workflow_call). Cross-repo usage requires the callee repository to be public, internal, or to have explicitly granted Actions access. When using secrets: inherit, the caller's secrets are passed through — including COPILOT_GITHUB_TOKEN, which must be configured in the caller's repository. See CentralRepoOps.

7. Update central-repo-ops docs (docs/src/content/docs/patterns/central-repo-ops.mdx)

Add a new ## Cross-Repository Trigger File section after the existing Trigger File section. Include:

  • Example caller stub — the platform-relay.yml YAML shown above
  • Repository visibility — callee must be public, internal, or have explicitly granted Actions access; explain how to grant access from Settings → Actions → General
  • Secrets configurationCOPILOT_GITHUB_TOKEN must be in the caller repo; explain secrets: inherit passes the caller's secrets
  • Billing — premium Copilot requests bill to the caller's token, not the platform's
  • How it works — brief explanation that the compiler detects workflow_call and generates a cross-repo-aware checkout using github.action_repository

8. Follow Guidelines

  • Use error message format: [what's wrong]. [what's expected]. [example] for any new validation errors
  • Follow file organization patterns from scratchpad/code-organization.md
  • Run make agent-finish before completing (build, test, recompile, format, lint)
  • No new dependencies required (no license concerns)

What Does NOT Change

  • No runtime JS changes (runtime_import.cjs, interpolate_prompt.cjs untouched)
  • No changes to validate_multi_secret.sh
  • Same-repo workflow_call continues to work identically
  • Existing triggers, agent steps, safe-outputs, conclusion logic, lock file format — all untouched
  • No new frontmatter fields or compiler flags

Impact Assessment

Low risk. The change is additive:

  • Only modifies behavior when workflow_call is present in the on: section
  • The conditional expression correctly falls back to github.repository for all non-workflow_call events
  • The cross-repo guidance step only runs on failure() AND workflow_call — invisible in all other cases
  • Existing compiled lock files are unaffected (no workflow_call → no change)

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions