feat: support workflow inputs on expanded reusable workflows (#10614)

Follow-up to #10525; adds support for `on.workflow_call.inputs` to expanded reusable workflows (when no `runs-on` is specified in a job that `uses: ...` another workflow).

The majority of the work for this is done by the `jobparser` library which evaluates inputs automatically when the job is being parsed and stores those inputs on the expanded jobs as the "default" value in `on.workflow_call.inputs`.  Forgejo's role here is just to to ensure that `forgejo.event_name` is set to `"workflow_call"` when a job is dispatched, which causes the runner to use the inputs that are stored -- 34c20aa50f/act/runner/expression.go (L452-L467).

## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Tests

- I added test coverage for Go changes...
  - [x] in their respective `*_test.go` for unit tests.
  - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I added test coverage for JavaScript changes...
  - [ ] in `web_src/js/*.test.js` if it can be unit tested.
  - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)).
- **end-to-end testing**: https://code.forgejo.org/forgejo/end-to-end/pulls/1323

### Documentation

- [x] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
    - https://codeberg.org/forgejo/docs/pulls/1664
- [ ] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [ ] I do not want this change to show in the release notes.
- [x] I want the title to show in the release notes with a link to this pull request.
- [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title.

<!--start release-notes-assistant-->

## Release notes
<!--URL:https://codeberg.org/forgejo/forgejo-->
- Features
  - [PR](https://codeberg.org/forgejo/forgejo/pulls/10614): <!--number 10614 --><!--line 0 --><!--description c3VwcG9ydCB3b3JrZmxvdyBpbnB1dHMgb24gZXhwYW5kZWQgcmV1c2FibGUgd29ya2Zsb3dz-->support workflow inputs on expanded reusable workflows<!--description-->
<!--end release-notes-assistant-->

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10614
Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org>
Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net>
Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
This commit is contained in:
Mathieu Fenniak 2025-12-29 15:37:44 +01:00 committed by Mathieu Fenniak
parent 60fb59a7a0
commit f7d2f51bf7
5 changed files with 103 additions and 11 deletions

View file

@ -287,3 +287,12 @@ func (job *ActionRunJob) IsWorkflowCallOuterJob() (bool, error) {
}
return jobWorkflow.Metadata.WorkflowCallID != "", nil
}
// Check whether the target job was generated as a result of expanding a reusable workflow.
func (job *ActionRunJob) IsWorkflowCallInnerJob() (bool, error) {
jobWorkflow, err := job.DecodeWorkflowPayload()
if err != nil {
return false, fmt.Errorf("failure decoding workflow payload: %w", err)
}
return jobWorkflow.Metadata.WorkflowCallParent != "", nil
}

View file

@ -196,3 +196,40 @@ func TestActionRunJob_IsWorkflowCallOuterJob(t *testing.T) {
})
}
}
func TestActionRunJob_IsWorkflowCallInnerJob(t *testing.T) {
tests := []struct {
name string
job ActionRunJob
isWorkflowCallInnerJob bool
errContains string
}{
{
name: "normal workflow",
job: ActionRunJob{WorkflowPayload: []byte("on: [workflow_dispatch]\nname: workflow")},
isWorkflowCallInnerJob: false,
},
{
name: "inner job",
job: ActionRunJob{WorkflowPayload: []byte("on:\n workflow_call:\nname: workflow\n__metadata:\n workflow_call_parent: b5a9f46f1f2513d7777fde50b169d323a6519e349cc175484c947ac315a209ed\n")},
isWorkflowCallInnerJob: true,
},
{
name: "unparseable workflow",
job: ActionRunJob{WorkflowPayload: []byte("name: []\nincomplete_runs_on: true")},
errContains: "failure unmarshaling WorkflowPayload to SingleWorkflow: yaml: unmarshal errors",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
isWorkflowCallInnerJob, err := tt.job.IsWorkflowCallInnerJob()
if tt.errContains != "" {
assert.ErrorContains(t, err, tt.errContains)
} else {
require.NoError(t, err)
assert.Equal(t, tt.isWorkflowCallInnerJob, isWorkflowCallInnerJob)
}
})
}
}

View file

@ -80,9 +80,20 @@ func generateGiteaContextForRun(run *actions_model.ActionRun) *model.GithubConte
// GenerateGiteaContext generate the gitea context without token and gitea_runtime_token
// job can be nil when generating a context for parsing workflow-level expressions
func GenerateGiteaContext(run *actions_model.ActionRun, job *actions_model.ActionRunJob) map[string]any {
func GenerateGiteaContext(run *actions_model.ActionRun, job *actions_model.ActionRunJob) (map[string]any, error) {
gitContextObj := generateGiteaContextForRun(run)
if job != nil {
// Setting the `github.event_name` value to `workflow_call` while executing a reusable workflow's inner job
// causes forgejo-runner to read `on.workflow_call.inputs` and populate its values into the `inputs` context.
workflowCall, err := job.IsWorkflowCallInnerJob()
if err != nil {
return nil, fmt.Errorf("failed to inspect workflow call state: %w", err)
} else if workflowCall {
gitContextObj.EventName = "workflow_call"
}
}
gitContext, _ := githubContextToMap(gitContextObj)
// standard contexts, see https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
@ -108,7 +119,7 @@ func GenerateGiteaContext(run *actions_model.ActionRun, job *actions_model.Actio
gitContext["run_attempt"] = fmt.Sprint(job.Attempt)
}
return gitContext
return gitContext, nil
}
func githubContextToMap(gitContext *model.GithubContext) (map[string]any, error) {

View file

@ -66,7 +66,8 @@ func TestGenerateGiteaContext(t *testing.T) {
EventPayload: `{"repository": {"name": "testrepo"}}`,
}
context := GenerateGiteaContext(run, nil)
context, err := GenerateGiteaContext(run, nil)
require.NoError(t, err)
assert.Equal(t, "testuser", context["actor"])
assert.Equal(t, setting.AppURL+"api/v1", context["api_url"])
@ -121,13 +122,15 @@ func TestGenerateGiteaContext(t *testing.T) {
}
job := &actions_model.ActionRunJob{
ID: 100,
RunID: 1,
JobID: "test-job",
Attempt: 1,
ID: 100,
RunID: 1,
JobID: "test-job",
Attempt: 1,
WorkflowPayload: []byte("on: [push]"),
}
context := GenerateGiteaContext(run, job)
context, err := GenerateGiteaContext(run, job)
require.NoError(t, err)
assert.Equal(t, "test-job", context["job"])
assert.Equal(t, "1", context["run_id"])
@ -166,7 +169,8 @@ func TestGenerateGiteaContext(t *testing.T) {
EventPayload: string(payloadBytes),
}
context := GenerateGiteaContext(run, nil)
context, err := GenerateGiteaContext(run, nil)
require.NoError(t, err)
assert.Equal(t, "main", context["base_ref"])
assert.Equal(t, "feature-branch", context["head_ref"])
@ -207,7 +211,8 @@ func TestGenerateGiteaContext(t *testing.T) {
EventPayload: string(payloadBytes),
}
context := GenerateGiteaContext(run, nil)
context, err := GenerateGiteaContext(run, nil)
require.NoError(t, err)
assert.Equal(t, "main", context["base_ref"])
assert.Equal(t, "feature-branch", context["head_ref"])
@ -218,6 +223,33 @@ func TestGenerateGiteaContext(t *testing.T) {
assert.Equal(t, "branch", context["ref_type"])
assert.Equal(t, "testowner/testrepo/.github/workflows/test-workflow.yml@refs/heads/main", context["workflow_ref"])
})
t.Run("workflow_call job", func(t *testing.T) {
run := &actions_model.ActionRun{
ID: 1,
Index: 42,
TriggerUser: testUser,
Repo: testRepo,
TriggerEvent: "push",
Ref: "refs/heads/main",
CommitSHA: "abc123def456",
WorkflowID: "test-workflow",
EventPayload: `{}`,
}
job := &actions_model.ActionRunJob{
ID: 100,
RunID: 1,
JobID: "test-job",
Attempt: 1,
WorkflowPayload: []byte("on: { workflow_call: { inputs: {} } }\n__metadata:\n workflow_call_parent: b5a9f46f1f2513d7777fde50b169d323a6519e349cc175484c947ac315a209ed\n"),
}
context, err := GenerateGiteaContext(run, job)
require.NoError(t, err)
assert.Equal(t, "workflow_call", context["event_name"])
})
}
func TestGenerateGiteaContextForRun(t *testing.T) {

View file

@ -88,7 +88,10 @@ func generateTaskContext(t *actions_model.ActionTask) (*structpb.Struct, error)
return nil, err
}
gitCtx := GenerateGiteaContext(t.Job.Run, t.Job)
gitCtx, err := GenerateGiteaContext(t.Job.Run, t.Job)
if err != nil {
return nil, err
}
gitCtx["token"] = t.Token
gitCtx["gitea_runtime_token"] = giteaRuntimeToken