Skip to content

fix: forward declared workflow_call inputs in call-workflow fan-out jobs#21085

Merged
pelikhan merged 9 commits intomainfrom
copilot/fix-call-workflow-input-forwarding
Mar 15, 2026
Merged

fix: forward declared workflow_call inputs in call-workflow fan-out jobs#21085
pelikhan merged 9 commits intomainfrom
copilot/fix-call-workflow-input-forwarding

Conversation

Copy link
Contributor

Copilot AI commented Mar 15, 2026

The compiler exposed typed workflow_call.inputs to the agent via MCP tool generation but dropped them when emitting fan-out jobs, forwarding only a single payload string. Workers that used ${{ inputs.source_repo }} or similar received nothing—forcing either post-compile patches or fromJSON(inputs.payload) in frontmatter, which fails strict-mode validation.

Changes

  • compiler_safe_output_jobs.go — after building the default payload entry, resolve the worker file (findWorkflowFile: .lock.yml.yml.md) and extract its workflow_call.inputs. For each input other than payload, emit a fromJSON-derived with: entry. Non-fatal on resolution failure (mirrors the existing permissions extraction pattern).

  • safe_outputs_call_workflow_test.go — unit test TestBuildCallWorkflowJobs_ForwardsDeclaredInputsFromPayload: asserts typed inputs appear as fromJSON(...) entries and payload is not duplicated.

  • call_workflow_compilation_test.go — E2E test TestCallWorkflowCompile_ForwardsTypedInputsAlongsidePayload: compiles a gateway whose worker declares payload, source_repo, and issue_number; asserts the lock file contains all three with: entries.

  • docs/reference/safe-outputs.md — updated Worker Inputs and Compiled Output sections to document both transport mechanisms.

  • smoke-workflow-call.md — added a task-description typed input (under both workflow_call and workflow_dispatch triggers) so the smoke worker exercises the new forwarding path end-to-end.

  • smoke-call-workflow.md — updated orchestrator instructions to pass "smoke test checkout validation" as the task-description argument when calling the worker.

  • Lock files regeneratedsmoke-call-workflow.lock.yml now forwards task-description via fromJSON(...) alongside payload; smoke-workflow-call.lock.yml exposes both inputs in its workflow_call trigger.

Generated output (before → after)

# Before
with:
  payload: ${{ needs.safe_outputs.outputs.call_workflow_payload }}

# After
with:
  payload: ${{ needs.safe_outputs.outputs.call_workflow_payload }}
  source_repo: ${{ fromJSON(needs.safe_outputs.outputs.call_workflow_payload).source_repo }}
  issue_number: ${{ fromJSON(needs.safe_outputs.outputs.call_workflow_payload).issue_number }}

payload remains the canonical transport; the additional entries are compiler-derived projections so worker steps can use ${{ inputs.<name> }} without parsing JSON.


📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.

Copilot AI and others added 2 commits March 15, 2026 14:52
The compiler now reads each worker's declared `workflow_call.inputs` and
emits one `with:` entry per input (beyond the canonical `payload`) using
`fromJSON(needs.safe_outputs.outputs.call_workflow_payload).<inputName>`.
Worker steps can now reference `${{ inputs.<name> }}` directly without
parsing the JSON envelope.

- pkg/workflow/compiler_safe_output_jobs.go: extract inputs via
  findWorkflowFile + extractWorkflowCallInputs / extractMDWorkflowCallInputs
  (non-fatal, mirrors the existing permissions pattern)
- tests: TestBuildCallWorkflowJobs_ForwardsDeclaredInputsFromPayload (unit)
  and TestCallWorkflowCompile_ForwardsTypedInputsAlongsidePayload (E2E)
- docs: updated Worker Inputs and Compiled Output sections

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
@pelikhan
Copy link
Contributor

@copilot update smoke workflow call agentic workflow with an input and ensure the lock file is updated .

Adds a typed `task-description` input to the smoke-workflow-call worker
and updates smoke-call-workflow to pass it. Recompiles both lock files
so the fan-out job now shows the new fromJSON-derived forwarding entry:

  task-description: ${{ fromJSON(needs.safe_outputs.outputs.call_workflow_payload).task-description }}

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 15, 2026

@copilot update smoke workflow call agentic workflow with an input and ensure the lock file is updated .

Done in 0a90528. Added a task-description input to smoke-workflow-call.md (both workflow_call and workflow_dispatch triggers) and updated smoke-call-workflow.md to instruct the agent to pass it. After recompile, the fan-out job in smoke-call-workflow.lock.yml now shows:

with:
  payload: ${{ needs.safe_outputs.outputs.call_workflow_payload }}
  task-description: ${{ fromJSON(needs.safe_outputs.outputs.call_workflow_payload).task-description }}

Copilot AI requested a review from pelikhan March 15, 2026 15:21
@pelikhan pelikhan marked this pull request as ready for review March 15, 2026 16:26
Copilot AI review requested due to automatic review settings March 15, 2026 16:26
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes call-workflow fan-out job generation so that declared workflow_call.inputs on worker workflows (besides the canonical payload) are forwarded into the with: block as fromJSON(...) projections, allowing workers to use ${{ inputs.<name> }} directly.

Changes:

  • Update buildCallWorkflowJobs to always include payload and additionally forward each declared worker input (excluding payload) as fromJSON(needs.safe_outputs.outputs.call_workflow_payload).<inputName>.
  • Add unit + E2E tests asserting typed inputs are forwarded and payload is not duplicated.
  • Update reference docs and smoke workflows/lock files to document and exercise typed input forwarding.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
pkg/workflow/compiler_safe_output_jobs.go Adds typed input extraction from worker workflows and emits fromJSON(...)-derived with: entries in fan-out jobs.
pkg/workflow/safe_outputs_call_workflow_test.go Adds a unit test to verify declared worker inputs are forwarded from the payload into the call job’s with:.
pkg/workflow/call_workflow_compilation_test.go Adds an E2E compile test asserting the generated lock YAML contains both payload and typed input projections.
docs/src/content/docs/reference/safe-outputs.md Documents canonical payload transport plus compiler-derived typed input forwarding and shows updated compiled output examples.
.github/workflows/smoke-workflow-call.md Adds a typed task-description input (and payload) to the smoke worker to exercise the forwarding path.
.github/workflows/smoke-workflow-call.lock.yml Regenerated lock output reflecting the new inputs and their usage in the workflow.
.github/workflows/smoke-call-workflow.md Updates smoke orchestrator instructions to pass task-description when calling the worker.
.github/workflows/smoke-call-workflow.lock.yml Regenerated lock output so the fan-out job forwards task-description alongside payload.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +184 to +206
var workflowInputs map[string]any
var inputErr error
switch {
case fileResult.lockExists:
workflowInputs, inputErr = extractWorkflowCallInputs(fileResult.lockPath)
case fileResult.ymlExists:
workflowInputs, inputErr = extractWorkflowCallInputs(fileResult.ymlPath)
case fileResult.mdExists:
workflowInputs, inputErr = extractMDWorkflowCallInputs(fileResult.mdPath)
}
if inputErr != nil {
compilerSafeOutputJobsLog.Printf("Warning: could not extract workflow_call inputs for '%s': %v. "+
"Typed inputs will not be forwarded in the with: block.", workflowName, inputErr)
} else {
typedInputCount := 0
for inputName := range workflowInputs {
if inputName == "payload" {
continue
}
with[inputName] = fmt.Sprintf("${{ fromJSON(needs.safe_outputs.outputs.call_workflow_payload).%s }}", inputName)
typedInputCount++
}
compilerSafeOutputJobsLog.Printf("Forwarding %d typed inputs for call-workflow job '%s'", typedInputCount, jobName)
Comment on lines +812 to +864
// TestCallWorkflowCompile_ForwardsTypedInputsAlongsidePayload tests that the compiler
// emits fromJSON-derived with: entries for each declared workflow_call input (except payload)
// alongside the canonical payload entry in the generated fan-out job.
func TestCallWorkflowCompile_ForwardsTypedInputsAlongsidePayload(t *testing.T) {
tmpDir := testutil.TempDir(t, "call-workflow-forward-inputs")
workflowsDir := filepath.Join(tmpDir, ".github", "workflows")
require.NoError(t, os.MkdirAll(workflowsDir, 0755))

typedInputs := ` payload:
type: string
required: false
source_repo:
description: Repository to check out
type: string
required: false
issue_number:
description: Issue number to process
type: string
required: false
`
createWorker(t, workflowsDir, "worker-a", typedInputs)

gatewayMD := `---
on: workflow_dispatch
engine: copilot
permissions:
contents: read
safe-outputs:
add-comment:
max: 1
call-workflow:
workflows:
- worker-a
---

# Gateway — typed input forwarding
`
yaml := compileAndReadLock(t, filepath.Join(workflowsDir, "gateway.md"), gatewayMD)

// Canonical payload transport must always be present
assert.Contains(t, yaml, "payload: ${{ needs.safe_outputs.outputs.call_workflow_payload }}",
"Should always forward canonical payload")

// Typed inputs must be derived from the payload via fromJSON
assert.Contains(t, yaml, "fromJSON(needs.safe_outputs.outputs.call_workflow_payload).source_repo",
"Should forward source_repo via fromJSON")
assert.Contains(t, yaml, "fromJSON(needs.safe_outputs.outputs.call_workflow_payload).issue_number",
"Should forward issue_number via fromJSON")

// payload must not appear as a fromJSON entry (it must remain the raw string)
assert.NotContains(t, yaml, "fromJSON(needs.safe_outputs.outputs.call_workflow_payload).payload",
"payload itself must not be duplicated as a fromJSON entry")
}
@pelikhan
Copy link
Contributor

…ow test

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 15, 2026

@copilot lint go https://github.com/github/gh-aw/actions/runs/23113263932/job/67137360816

Fixed in cea4552 — replaced assert.False(t, strings.Contains(...)) with assert.NotContains(...) in safe_outputs_call_workflow_test.go:460 to satisfy the testifylint rule.

@pelikhan
Copy link
Contributor

@copilot merge main

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 15, 2026

@copilot merge main

Done in acf0121. Merged main — resolved one conflict in smoke-workflow-call.lock.yml (metadata hash), rebuilt the binary, and recompiled all lock files.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/graphql
    • Triggering command: /usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw (http block)
  • https://api.github.com/repos/astral-sh/setup-uv/git/ref/tags/eac588ad8def6316056a12d4907a9d4d84ff7a3b
    • Triggering command: /usr/bin/gh gh api /repos/astral-sh/setup-uv/git/ref/tags/eac588ad8def6316056a12d4907a9d4d84ff7a3b --jq .object.sha (http block)
  • https://api.github.com/repos/github/gh-aw
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw --jq .visibility (http block)
  • https://api.github.com/repos/githubnext/agentics/git/ref/tags/
    • Triggering command: /usr/bin/gh gh api /repos/githubnext/agentics/git/ref/tags/# --jq .object.sha (http block)

If you need me to access, download, or install something from one of these locations, you can either:

@pelikhan
Copy link
Contributor

@copilot review comments and suggestions

@pelikhan
Copy link
Contributor

@copilot review comments and suggestions

…2E test

- Add `default` case to the findWorkflowFile result switch to log a
  warning when no worker file exists (all *.Exists flags false), rather
  than silently logging "Forwarding 0 typed inputs"
- Change `else` to `else if workflowInputs != nil` so the forwarding
  block is skipped entirely when the default case fires
- Add TestCallWorkflowCompile_ForwardsHyphenatedInputs E2E test that
  compiles a gateway whose worker declares `task-description` (hyphen)
  and asserts the lock file contains the correct fromJSON projection

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 15, 2026

@copilot review comments and suggestions

Addressed both reviewer suggestions in 2aff7fa:

  1. Missing worker file (compiler_safe_output_jobs.go): Added a default case to the findWorkflowFile result switch that explicitly logs "Warning: no worker file found for '%s'; typed inputs will not be forwarded in the with: block." Changed the subsequent else to else if workflowInputs != nil so the forwarding block is fully skipped (and no misleading "Forwarding 0 typed inputs" is logged) when no file was found.

  2. Hyphenated input names (call_workflow_compilation_test.go): Added TestCallWorkflowCompile_ForwardsHyphenatedInputs — compiles a gateway whose worker declares task-description and asserts the lock file contains fromJSON(needs.safe_outputs.outputs.call_workflow_payload).task-description.

@pelikhan pelikhan merged commit 6de06b7 into main Mar 15, 2026
51 of 53 checks passed
@pelikhan pelikhan deleted the copilot/fix-call-workflow-input-forwarding branch March 15, 2026 20:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

call-workflow fan-out jobs do not forward declared workflow_call.inputs beyond payload

3 participants