Skip to content

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

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)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions