Skip to content

Cross-repo activation checkout still broken for event-driven relay workflows after #20301 #20567

@johnwilliams-12

Description

@johnwilliams-12

Summary

PR #20301 fixed the activation job checkout for platform workflows that are called by a pure workflow_call relay (i.e. a relay whose outermost trigger is itself on: workflow_call). However, the fix does not cover event-driven relays — relays triggered by a native event (e.g. on: issue_comment, on: push, on: label) that then call the platform workflow via uses:. For these, github.event_name is never 'workflow_call' inside the called workflow, so the conditional expression introduced in #20301 always evaluates false and the activation job checks out the caller's repo instead of the platform repo.

inlined-imports: true remains the only working workaround for event-driven relay patterns.


Agent Analysis

This was traced from a live workflow run where the relay was triggered by an issue_comment event.

Root cause

When the outer relay fires on issue_comment and calls the platform workflow via uses::

# app-repo/.github/workflows/relay.yml
on:
  issue_comment:
    types: [created]
jobs:
  relay:
    uses: my-org/platform-repo/.github/workflows/gateway.lock.yml@main
    with:
      source_repo: ${{ github.repository }}
    secrets: inherit

GitHub propagates the original triggering eventissue_comment — through the workflow_call chain unchanged. Inside gateway.lock.yml, the GitHub Actions context variables are:

Variable Value
github.event_name 'issue_comment'not 'workflow_call'
github.repository 'my-org/app-repo' (the caller)
github.action_repository empty in run: steps

The conditional from #20301, now emitted by generateCheckoutGitHubFolderForActivation():

repository: ${{ github.event_name == 'workflow_call' && github.action_repository || github.repository }}

…evaluates to github.repository = 'my-org/app-repo'. The activation job clones the caller's repo. Any step that references .github/ files — runtime imports, prompt templates — fails immediately with ERR_SYSTEM: Runtime import file not found (or equivalent command not found / ENOENT).

Why github.event_name == 'workflow_call' only works in limited cases

github.event_name is set by the GitHub Actions runner to the event that triggered the outermost workflow in the chain. It is only 'workflow_call' when the relay file itself declares on: workflow_call as its trigger, e.g.:

# relay.yml — pure reusable workflow
on:
  workflow_call:
    inputs:
      source_repo:
        type: string

This is the scenario validated by the smoke-workflow-call smoke test added in #20301. The test calls the gateway with on: workflow_call as the outermost event, so github.event_name is correctly 'workflow_call' inside the called workflow.

For every other relay pattern — the common, real-world deployment where a repo reacts to user events and calls the platform workflow — github.event_name propagates the native event. The fix from #20301 does not apply.


Affected code

  • pkg/workflow/compiler_activation_job.gogenerateCheckoutGitHubFolderForActivation(), specifically the call:
    return cm.GenerateGitHubFolderCheckoutStep(
        "${{ github.event_name == 'workflow_call' && github.action_repository || github.repository }}",
        GetActionPin,
    )
  • pkg/workflow/compiler_activation_job_test.go — the workflowCallRepo constant encodes the broken expression and is asserted against; tests do not cover the event-driven relay scenario
  • docs/src/content/docs/patterns/central-repo-ops.mdx — the "How It Works" section documents the expression as correct without noting this limitation; the smoke test that validates the pattern uses a pure workflow_call relay which masks the gap

Proposed fix

Use github.workflow_ref as the cross-repo discriminator instead of github.event_name.

github.workflow_ref always contains the full owner/repo/.github/workflows/<file>@ref path of the workflow currently running, regardless of the triggering event. It is set by the runner based on which workflow file is executing, not on how that workflow was started.

When a platform workflow (my-org/platform-repo/.github/workflows/gateway.lock.yml) is called cross-repo:

  • github.repository = 'my-org/app-repo' (the caller)
  • github.workflow_ref starts with my-org/platform-repo/...

The platform repo slug is always the owner/repo prefix of github.workflow_ref (before /.github/). When github.repository does not match that prefix, the checkout should target the platform repo, not the caller.

Implementation options

Option A — Two-step: resolve then checkout (no compile-time knowledge required)

Emit a run: step before the checkout that parses GITHUB_WORKFLOW_REF and writes the target repo to GITHUB_OUTPUT:

- name: Resolve workflow host repo
  id: resolve-host
  run: |
    HOST_REPO=$(echo "$GITHUB_WORKFLOW_REF" | cut -d'/' -f1,2)
    if [[ "$HOST_REPO" != "$GITHUB_REPOSITORY" ]]; then
      echo "target_repo=$HOST_REPO" >> "$GITHUB_OUTPUT"
    else
      echo "target_repo=$GITHUB_REPOSITORY" >> "$GITHUB_OUTPUT"
    fi
  env:
    GITHUB_WORKFLOW_REF: ${{ github.workflow_ref }}
    GITHUB_REPOSITORY: ${{ github.repository }}

- name: Checkout .github and .agents folders
  uses: actions/checkout@...
  with:
    repository: ${{ steps.resolve-host.outputs.target_repo }}
    sparse-checkout: |
      .github
      .agents
    sparse-checkout-cone-mode: true
    fetch-depth: 1
    persist-credentials: false

This is safe and correct across all relay patterns. The if [[ "$HOST_REPO" != "$GITHUB_REPOSITORY" ]] guard means same-repo invocations still use github.repository unmodified.

Option B — Inline expression using github.workflow_ref

If the compiler can emit the owner/repo portion of github.workflow_ref as a known literal at compile time (it can, via git remote get-url origin or equivalent), the checkout repository: value can be a constant:

repository: my-org/platform-repo

This avoids the run: step but requires the compiler to know the platform repo slug at compile time. This is already available via the source file path and git context when gh aw compile runs.


Implementation plan

  1. pkg/workflow/compiler_activation_job.gogenerateCheckoutGitHubFolderForActivation():

    • Replace the call to GenerateGitHubFolderCheckoutStep with the broken github.event_name == 'workflow_call' && github.action_repository || github.repository expression
    • Emit a run: step (Option A) that parses GITHUB_WORKFLOW_REF into GITHUB_OUTPUT, followed by a checkout step referencing ${{ steps.resolve-host.outputs.target_repo }}
    • Alternatively, if the compiler has access to the platform repo slug literal, emit it directly (Option B)
    • The !data.InlinedImports guard should remain — when inlined-imports: true, no checkout is needed
  2. pkg/workflow/compiler_activation_job_test.go:

    • Update workflowCallRepo constant or remove it and assert on the new step structure
    • Add a test case "event-driven relay (issue_comment outermost trigger)" that verifies the checkout correctly targets the platform repo even when event_name != 'workflow_call'
    • Verify the generated lock file does not contain the github.event_name == 'workflow_call' guard
  3. pkg/workflow/activation_checkout_test.go (integration):

    • Add a test compiling a workflow with mixed on: [issue_comment, workflow_call] triggers
    • Verify the activation job checkout does not rely on github.event_name == 'workflow_call'
  4. docs/src/content/docs/patterns/central-repo-ops.mdx:

    • In the "How It Works" section, replace the description of the broken expression with the corrected mechanism
    • Add a clear note: the github.event_name == 'workflow_call' guard only works when the relay's outermost trigger is on: workflow_call. Event-driven relays (on: issue_comment, on: push, etc.) require a different approach
    • Document inlined-imports: true as the available workaround for event-driven relays until a fix is released
  5. smoke-water.yml / smoke test coverage:

    • Add a second smoke test where the relay is triggered by a label event (or simulate via workflow_dispatch with an explicit event_name override) so the CI catches regressions for event-driven relay patterns

Current workaround

Set inlined-imports: true in the platform workflow's frontmatter. This causes the compiler to bake all referenced .md content into the lock file at compile time, eliminating the need for the activation job to check out the platform repo's .github/ folder at runtime.

---
inlined-imports: true
on:
  issue_comment:
    types: [created]
  workflow_call:
    inputs: ...
---

The trade-off: any change to imported markdown files requires recompilation and a new commit. Runtime edits to prompt files have no effect until recompiled.


Reproduction

  1. Create a platform workflow with on: [issue_comment, workflow_call]; compile with gh aw compile --strict
  2. In a separate repository, create an event-driven relay:
    on:
      issue_comment:
        types: [created]
    jobs:
      relay:
        uses: my-org/platform-repo/.github/workflows/my-workflow.lock.yml@main
        with:
          source_repo: ${{ github.repository }}
        secrets: inherit
  3. Post an issue comment in the app repo to trigger the relay
  4. Observe the activation job checking out the app repo and failing on any step that references .github/ files from the platform repo

Expected: activation job checks out my-org/platform-repo's .github/ folder
Actual: activation job checks out my-org/app-repo; workflow fails with file-not-found errors

This reproduces on v0.57.2 with the fix from #20301 already applied.

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions