Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package forgejo-runner for openSUSE:Factory checked in at 2025-12-25 19:57:45 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/forgejo-runner (Old) and /work/SRC/openSUSE:Factory/.forgejo-runner.new.1928 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "forgejo-runner" Thu Dec 25 19:57:45 2025 rev:37 rq:1324386 version:12.3.1 Changes: -------- --- /work/SRC/openSUSE:Factory/forgejo-runner/forgejo-runner.changes 2025-12-22 22:52:47.209793275 +0100 +++ /work/SRC/openSUSE:Factory/.forgejo-runner.new.1928/forgejo-runner.changes 2025-12-25 19:58:01.112151935 +0100 @@ -1,0 +2,10 @@ +Thu Dec 25 01:23:30 UTC 2025 - Richard Rahl <[email protected]> + +- Update to version 12.3.1: + * feat(jobparser): separate the concept of an instance & external reusable + workflow + * fix: second-layer reusable workflows losing their inputs when expanded +- remove basic version check, as upstream most of the time forgets updating + that + +------------------------------------------------------------------- Old: ---- forgejo-runner-12.3.0.obscpio New: ---- forgejo-runner-12.3.1.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ forgejo-runner.spec ++++++ --- /var/tmp/diff_new_pack.QnEcko/_old 2025-12-25 19:58:02.144193811 +0100 +++ /var/tmp/diff_new_pack.QnEcko/_new 2025-12-25 19:58:02.148193973 +0100 @@ -17,9 +17,8 @@ %define services %{name}.service - Name: forgejo-runner -Version: 12.3.0 +Version: 12.3.1 Release: 0 Summary: Daemon that connects to a Forgejo instance and runs CI jobs License: GPL-3.0-or-later @@ -112,10 +111,6 @@ install -m 0640 /dev/null %{buildroot}%{_sysconfdir}/%{name}/runners install -D -m 0750 -d %{buildroot}%{_localstatedir}/lib/%{name} -# this update forgot to change the version, so disable for >12.1.1, enable when update -#%%check -#bin/%{name} --version | grep %{version} - %pre %service_add_pre %{services} ++++++ _service ++++++ --- /var/tmp/diff_new_pack.QnEcko/_old 2025-12-25 19:58:02.216196733 +0100 +++ /var/tmp/diff_new_pack.QnEcko/_new 2025-12-25 19:58:02.228197220 +0100 @@ -2,7 +2,7 @@ <service name="obs_scm" mode="manual"> <param name="url">https://code.forgejo.org/forgejo/runner</param> <param name="scm">git</param> - <param name="revision">refs/tags/v12.3.0</param> + <param name="revision">refs/tags/v12.3.1</param> <param name="versionformat">@PARENT_TAG@</param> <param name="changesgenerate">disable</param> <param name="versionrewrite-pattern">v(.*)</param> ++++++ forgejo-runner-12.3.0.obscpio -> forgejo-runner-12.3.1.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/forgejo-runner-12.3.0/act/jobparser/jobparser.go new/forgejo-runner-12.3.1/act/jobparser/jobparser.go --- old/forgejo-runner-12.3.0/act/jobparser/jobparser.go 2025-12-21 17:32:28.000000000 +0100 +++ new/forgejo-runner-12.3.1/act/jobparser/jobparser.go 2025-12-24 17:06:29.000000000 +0100 @@ -120,7 +120,7 @@ } // Expand reusable workflows `uses:...` into inner jobs: - if pc.localWorkflowFetcher != nil || pc.remoteWorkflowFetcher != nil { + if pc.localWorkflowFetcher != nil || pc.instanceWorkflowFetcher != nil || pc.externalWorkflowFetcher != nil { newJobs, err := expandReusableWorkflows(postMatrixJobs, validate, incompleteMatrix, options, pc, results) if err != nil { return nil, err @@ -340,84 +340,111 @@ continue } - workflowJob := bothJobs.workflowJob - - jobType, err := workflowJob.Type() + reusableWorkflow, err := tryFetchReusableWorkflow(bothJobs, pc) if err != nil { return nil, err + } else if reusableWorkflow == nil { + // Not a reusable workflow to expand. + continue } - var reusableWorkflow []byte - if jobType == model.JobTypeReusableWorkflowLocal && pc.localWorkflowFetcher != nil { - contents, err := pc.localWorkflowFetcher(bothJobs.jobParserJob, workflowJob.Uses) - if err != nil { - if errors.Is(err, ErrUnsupportedReusableWorkflowFetch) { - // Skip workflow expansion. - continue - } - return nil, fmt.Errorf("unable to read local workflow %q: %w", workflowJob.Uses, err) + + // If we encounter an InvalidJobOutputReferencedError error, we'll know that this is caused by a `with:` + // clause referencing a job output that isn't present yet. In this case, don't expand the job, but provide + // the error back in `bothJobTypes` so that it can be returned in the `SingleWorkflow`. + var withInvalidJobReference *exprparser.InvalidJobOutputReferencedError + // Same type of error, but for accessing an invalid matrix during `with:`. + var withInvalidMatrixReference *exprparser.InvalidMatrixDimensionReferencedError + + workflowParent := generateWorkflowCallID(pc.parentUniqueID, bothJobs.id, bothJobs.matrix) + bothJobs.workflowCallID = workflowParent + + newJobs, err := expandReusableWorkflow(reusableWorkflow, validate, options, pc, jobResults, bothJobs.matrix, bothJobs) + if err != nil { + errors.As(err, &withInvalidJobReference) + errors.As(err, &withInvalidMatrixReference) + if withInvalidJobReference == nil && withInvalidMatrixReference == nil { + return nil, fmt.Errorf("error expanding reusable workflow %q: %v", bothJobs.workflowJob.Uses, err) } - reusableWorkflow = contents } - if jobType == model.JobTypeReusableWorkflowRemote && pc.remoteWorkflowFetcher != nil { - parsed, err := model.ParseRemoteReusableWorkflow(workflowJob.Uses) - if err != nil { - return nil, fmt.Errorf("unable to parse `uses: %q` as a valid reusable workflow: %w", workflowJob.Uses, err) - } - contents, err := pc.remoteWorkflowFetcher(bothJobs.jobParserJob, parsed) - if err != nil { - if errors.Is(err, ErrUnsupportedReusableWorkflowFetch) { - // Skip workflow expansion. - continue - } - return nil, fmt.Errorf("unable to read remote workflow %q: %w", workflowJob.Uses, err) + + if withInvalidJobReference == nil && withInvalidMatrixReference == nil { + // Append the inner jobs' IDs to the `needs` of the parent job. + additionalNeeds := make([]string, len(newJobs)) + for i, b := range newJobs { + additionalNeeds[i] = b.id } - reusableWorkflow = contents + callerNeeds := bothJobs.jobParserJob.Needs() + callerNeeds = append(callerNeeds, additionalNeeds...) + _ = bothJobs.jobParserJob.RawNeeds.Encode(callerNeeds) + + // The calling job will still exist in order to act as a `sentinel` for `needs` job ordering & output + // access. We'll take away the job's content to ensure nothing is actually executed. + _ = bothJobs.jobParserJob.If.Encode(false) + bothJobs.jobParserJob.Uses = "" + bothJobs.jobParserJob.With = nil + } else { + // Retain all the original data of the job until it's really expanded, with its inputs, later + bothJobs.withInvalidJobReference = withInvalidJobReference + bothJobs.withInvalidMatrixReference = withInvalidMatrixReference } - if reusableWorkflow != nil { - // If we encounter an InvalidJobOutputReferencedError error, we'll know that this is caused by a `with:` - // clause referencing a job output that isn't present yet. In this case, don't expand the job, but provide - // the error back in `bothJobTypes` so that it can be returned in the `SingleWorkflow`. - var withInvalidJobReference *exprparser.InvalidJobOutputReferencedError - // Same type of error, but for accessing an invalid matrix during `with:`. - var withInvalidMatrixReference *exprparser.InvalidMatrixDimensionReferencedError - workflowParent := generateWorkflowCallID(pc.parentUniqueID, bothJobs.id, bothJobs.matrix) - bothJobs.workflowCallID = workflowParent + retval = append(retval, newJobs...) + } + return retval, nil +} - newJobs, err := expandReusableWorkflow(reusableWorkflow, validate, options, pc, jobResults, bothJobs.matrix, bothJobs) +func tryFetchReusableWorkflow(bothJobs *bothJobTypes, pc *parseContext) ([]byte, error) { + workflowJob := bothJobs.workflowJob + + jobType, err := workflowJob.Type() + if err != nil { + return nil, err + } + + if jobType == model.JobTypeReusableWorkflowLocal && pc.localWorkflowFetcher != nil { + contents, err := pc.localWorkflowFetcher(bothJobs.jobParserJob, workflowJob.Uses) + if err != nil { + if errors.Is(err, ErrUnsupportedReusableWorkflowFetch) { + // Skip workflow expansion. + return nil, nil + } + return nil, fmt.Errorf("unable to read local workflow %q: %w", workflowJob.Uses, err) + } + return contents, nil + } else if jobType == model.JobTypeReusableWorkflowRemote { + parsed, err := model.ParseRemoteReusableWorkflow(workflowJob.Uses) + if err != nil { + return nil, fmt.Errorf("unable to parse `uses: %q` as a valid reusable workflow: %w", workflowJob.Uses, err) + } + external, isExternal := parsed.(*model.ExternalReusableWorkflowReference) + if !isExternal && pc.instanceWorkflowFetcher != nil { + contents, err := pc.instanceWorkflowFetcher(bothJobs.jobParserJob, parsed.Reference()) if err != nil { - errors.As(err, &withInvalidJobReference) - errors.As(err, &withInvalidMatrixReference) - if withInvalidJobReference == nil && withInvalidMatrixReference == nil { - return nil, fmt.Errorf("error expanding reusable workflow %q: %v", workflowJob.Uses, err) + if errors.Is(err, ErrUnsupportedReusableWorkflowFetch) { + // Skip workflow expansion. + return nil, nil } + return nil, fmt.Errorf("unable to read instance workflow %q: %w", workflowJob.Uses, err) } - - if withInvalidJobReference == nil && withInvalidMatrixReference == nil { - // Append the inner jobs' IDs to the `needs` of the parent job. - additionalNeeds := make([]string, len(newJobs)) - for i, b := range newJobs { - additionalNeeds[i] = b.id + return contents, nil + } else if isExternal && pc.externalWorkflowFetcher != nil { + contents, err := pc.externalWorkflowFetcher(bothJobs.jobParserJob, external) + if err != nil { + if errors.Is(err, ErrUnsupportedReusableWorkflowFetch) { + // Skip workflow expansion. + return nil, nil } - callerNeeds := bothJobs.jobParserJob.Needs() - callerNeeds = append(callerNeeds, additionalNeeds...) - _ = bothJobs.jobParserJob.RawNeeds.Encode(callerNeeds) - - // The calling job will still exist in order to act as a `sentinel` for `needs` job ordering & output - // access. We'll take away the job's content to ensure nothing is actually executed. - _ = bothJobs.jobParserJob.If.Encode(false) - bothJobs.jobParserJob.Uses = "" - bothJobs.jobParserJob.With = nil - } else { - // Retain all the original data of the job until it's really expanded, with its inputs, later - bothJobs.withInvalidJobReference = withInvalidJobReference - bothJobs.withInvalidMatrixReference = withInvalidMatrixReference + return nil, fmt.Errorf("unable to read external workflow %q: %w", workflowJob.Uses, err) } - - retval = append(retval, newJobs...) + return contents, nil } + + // Fallthrough intentional -- `isExternal` and the relevant available fetcher didn't combine to have a relevant + // fetcher for the type of this reusable workflow. } - return retval, nil + + // Either not a reusable workflow, or not a reusable workflow type that we have a fetcher for. + return nil, nil } func expandReusableWorkflow(contents []byte, validate bool, options []ParseOption, pc *parseContext, jobResults map[string]*JobResult, matrix map[string]any, callerJob *bothJobTypes) ([]*bothJobTypes, error) { @@ -493,17 +520,25 @@ id: fmt.Sprintf("%s.%s", callerJob.id, id), jobParserJob: job, workflowJob: workflow.GetJob(id), - overrideOnClause: rebuiltOn, + overrideOnClause: &swf.RawOn, - workflowCallParent: callerJob.workflowCallID, - } - // Maintain existing ID / Parent if populated in a lower-level recursive workflow call - if swf.Metadata.WorkflowCallID != "" { - newEntry.workflowCallID = swf.Metadata.WorkflowCallID - } - if swf.Metadata.WorkflowCallParent != "" { - newEntry.workflowCallParent = swf.Metadata.WorkflowCallParent + // Preserve metadata: + workflowCallParent: swf.Metadata.WorkflowCallParent, + workflowCallInputs: swf.Metadata.WorkflowCallInputs, + workflowCallID: swf.Metadata.WorkflowCallID, + } + + // If the new job doesn't have a parent, that means it's a direct-child of the job that we're currently + // expanding `callerJob`: + if newEntry.workflowCallParent == "" { + // Store it's relationship to its parent. + newEntry.workflowCallParent = callerJob.workflowCallID + + // Store the inputs that were calculated for `callerJob` in the `on` clause so that they're used when the + // child job is executed: + newEntry.overrideOnClause = rebuiltOn } + if swf.IncompleteMatrix || swf.IncompleteRunsOn || swf.IncompleteWith { newEntry.internalIncompleteState = swf // if we have a reference to a job stored in the incomplete state, then qualify that job name: @@ -716,7 +751,7 @@ // ./.forgejo/workflows/reusable.yml`) into one-or-more jobs contained within the local workflow. The // `localWorkflowFetcher` function allows jobparser to read the target workflow file. // -// The `localWorkflowFetcher` can return the error ErrUnsupportedReusableWorkflowFetch if the fetcher doesn't support +// The `localWorkflowFetcher` can return the error [ErrUnsupportedReusableWorkflowFetch] if the fetcher doesn't support // the target workflow for job parsing. The job will go to the "fallback" mode of operation where its internal jobs are // not expanded into the parsed workflow, and it can still be executed as a single monolithic job. All other errors are // considered fatal for job parsing. @@ -726,20 +761,31 @@ } } -// Allows the job parser to convert a workflow job that references a remote (eg. not part of the current workflow's -// repository) reusable workflow (eg. `uses: some-org/some-repo/.forgejo/workflows/reusable.yml`) into one-or-more jobs -// contained within the remote workflow. The `remoteWorkflowFetcher` function allows jobparser to read the target -// workflow file. +// Allows the job parser to read a workflow job that references a reusable workflow on the same Forgejo instance, but +// not in the same repository (eg. `uses: some-org/some-repo/.forgejo/workflows/reusable.yml`). The workflow is +// converted into one-or-more jobs contained within the workflow. // -// `ref.Host` will be `nil` if the remote reference was not a fully-qualified URL. No default value is provided. +// The `instanceWorkflowFetcher` can return the error [ErrUnsupportedReusableWorkflowFetch] if the fetcher doesn't +// support the target workflow for job parsing. The job will go to the "fallback" mode of operation where its internal +// jobs are not expanded into the parsed workflow, and it can still be executed as a single monolithic job. All other +// errors are considered fatal for job parsing. +func ExpandInstanceReusableWorkflows(instanceWorkflowFetcher InstanceWorkflowFetcher) ParseOption { + return func(c *parseContext) { + c.instanceWorkflowFetcher = instanceWorkflowFetcher + } +} + +// Allows the job parser to read a workflow job that references an external reusable workflow with a fully-qualified URL +// (eg. `uses: https://example.com/some-org/some-repo/.forgejo/workflows/reusable.yml`). The workflow is converted into +// one-or-more jobs contained within the external workflow file. // -// The `remoteWorkflowFetcher` can return the error ErrUnsupportedReusableWorkflowFetch if the fetcher doesn't support -// the target workflow for job parsing. The job will go to the "fallback" mode of operation where its internal jobs are -// not expanded into the parsed workflow, and it can still be executed as a single monolithic job. All other errors are -// considered fatal for job parsing. -func ExpandRemoteReusableWorkflows(remoteWorkflowFetcher RemoteWorkflowFetcher) ParseOption { +// The `externalWorkflowFetcher` can return the error [ErrUnsupportedReusableWorkflowFetch] if the fetcher doesn't +// support the target workflow for job parsing. The job will go to the "fallback" mode of operation where its internal +// jobs are not expanded into the parsed workflow, and it can still be executed as a single monolithic job. All other +// errors are considered fatal for job parsing. +func ExpandExternalReusableWorkflows(externalWorkflowFetcher ExternalWorkflowFetcher) ParseOption { return func(c *parseContext) { - c.remoteWorkflowFetcher = remoteWorkflowFetcher + c.externalWorkflowFetcher = externalWorkflowFetcher } } @@ -756,8 +802,9 @@ } type ( - LocalWorkflowFetcher func(job *Job, path string) ([]byte, error) - RemoteWorkflowFetcher func(job *Job, ref *model.RemoteReusableWorkflowWithBaseURL) ([]byte, error) + LocalWorkflowFetcher func(job *Job, path string) ([]byte, error) + InstanceWorkflowFetcher func(job *Job, ref *model.NonLocalReusableWorkflowReference) ([]byte, error) + ExternalWorkflowFetcher func(job *Job, ref *model.ExternalReusableWorkflowReference) ([]byte, error) ) type parseContext struct { @@ -769,7 +816,8 @@ workflowNeeds []string supportIncompleteRunsOn bool localWorkflowFetcher LocalWorkflowFetcher - remoteWorkflowFetcher RemoteWorkflowFetcher + instanceWorkflowFetcher InstanceWorkflowFetcher + externalWorkflowFetcher ExternalWorkflowFetcher recursionDepth int parentUniqueID string } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/forgejo-runner-12.3.0/act/jobparser/jobparser_test.go new/forgejo-runner-12.3.1/act/jobparser/jobparser_test.go --- old/forgejo-runner-12.3.0/act/jobparser/jobparser_test.go 2025-12-21 17:32:28.000000000 +0100 +++ new/forgejo-runner-12.3.1/act/jobparser/jobparser_test.go 2025-12-24 17:06:29.000000000 +0100 @@ -173,7 +173,7 @@ name: "expand_remote_workflow", expectingInvalidWorkflowOutput: true, options: []ParseOption{ - ExpandRemoteReusableWorkflows(func(job *Job, ref *model.RemoteReusableWorkflowWithBaseURL) ([]byte, error) { + ExpandInstanceReusableWorkflows(func(job *Job, ref *model.NonLocalReusableWorkflowReference) ([]byte, error) { if ref.Org != "some-org" { return nil, fmt.Errorf("unexpected remote Org: %q", ref.Org) } @@ -183,19 +183,9 @@ if ref.GitPlatform != "forgejo" { return nil, fmt.Errorf("unexpected remote GitPlatform: %q", ref.GitPlatform) } - if ref.BaseURL == nil { - // relative reference in expand_remote_workflow.in.yaml - if ref.Filename != "expand_remote_workflow_reusable-2.yml" { - return nil, fmt.Errorf("unexpected remote Filename: %q", ref.Filename) - } - } else { - // absolute reference in expand_remote_workflow.in.yaml - if *ref.BaseURL != "https://example.com" { - return nil, fmt.Errorf("unexpected remote Host: %v", ref.BaseURL) - } - if ref.Filename != "expand_remote_workflow_reusable-1.yml" { - return nil, fmt.Errorf("unexpected remote Filename: %q", ref.Filename) - } + // remote reference in expand_remote_workflow.in.yaml + if ref.Filename != "expand_remote_workflow_reusable-2.yml" { + return nil, fmt.Errorf("unexpected remote Filename: %q", ref.Filename) } if ref.Ref != "v1" { return nil, fmt.Errorf("unexpected remote Ref: %q", ref.Ref) @@ -203,6 +193,32 @@ content := ReadTestdata(t, "expand_remote_workflow_reusable-1.yaml", true) return content, nil }), + ExpandExternalReusableWorkflows(func(job *Job, ref *model.ExternalReusableWorkflowReference) ([]byte, error) { + if ref.Org != "some-org" { + return nil, fmt.Errorf("unexpected external Org: %q", ref.Org) + } + if ref.Repo != "some-repo" { + return nil, fmt.Errorf("unexpected external Repo: %q", ref.Repo) + } + if ref.GitPlatform != "forgejo" { + return nil, fmt.Errorf("unexpected external GitPlatform: %q", ref.GitPlatform) + } + // external reference in expand_remote_workflow.in.yaml + if ref.Filename != "expand_remote_workflow_reusable-1.yml" { + return nil, fmt.Errorf("unexpected external Filename: %q", ref.Filename) + } + if ref.BaseURL != "https://example.com" { + return nil, fmt.Errorf("unexpected external Host: %v", ref.BaseURL) + } + if ref.Filename != "expand_remote_workflow_reusable-1.yml" { + return nil, fmt.Errorf("unexpected external Filename: %q", ref.Filename) + } + if ref.Ref != "v1" { + return nil, fmt.Errorf("unexpected external Ref: %q", ref.Ref) + } + content := ReadTestdata(t, "expand_remote_workflow_reusable-1.yaml", true) + return content, nil + }), }, }, { @@ -604,7 +620,7 @@ swf, err := Parse( []byte(testWorkflow), false, - ExpandRemoteReusableWorkflows(func(job *Job, ref *model.RemoteReusableWorkflowWithBaseURL) ([]byte, error) { + ExpandInstanceReusableWorkflows(func(job *Job, ref *model.NonLocalReusableWorkflowReference) ([]byte, error) { return []byte(testWorkflow), nil }), ) @@ -643,7 +659,7 @@ []byte(outerWorkflow), false, WithJobOutputs(jobOutputs), - ExpandRemoteReusableWorkflows(func(job *Job, ref *model.RemoteReusableWorkflowWithBaseURL) ([]byte, error) { + ExpandInstanceReusableWorkflows(func(job *Job, ref *model.NonLocalReusableWorkflowReference) ([]byte, error) { return []byte(innerWorkflow), nil }), ) @@ -672,7 +688,7 @@ }, }), WithWorkflowNeeds([]string{"some-other-job"}), - ExpandRemoteReusableWorkflows(func(job *Job, ref *model.RemoteReusableWorkflowWithBaseURL) ([]byte, error) { + ExpandInstanceReusableWorkflows(func(job *Job, ref *model.NonLocalReusableWorkflowReference) ([]byte, error) { callCount++ return []byte(outerWorkflow), nil }), @@ -683,7 +699,7 @@ } func TestReusableWorkflowFetcherArgs(t *testing.T) { - t.Run("ExpandRemoteReusableWorkflows", func(t *testing.T) { + t.Run("ExpandInstanceReusableWorkflows", func(t *testing.T) { testWorkflow := ` on: pull_request: @@ -700,7 +716,7 @@ swf, err := Parse( []byte(testWorkflow), false, - ExpandRemoteReusableWorkflows(func(job *Job, ref *model.RemoteReusableWorkflowWithBaseURL) ([]byte, error) { + ExpandInstanceReusableWorkflows(func(job *Job, ref *model.NonLocalReusableWorkflowReference) ([]byte, error) { executed = true // validate `job` passed in is correct/expected object assert.Equal(t, []string{"ubuntu-latest"}, job.RunsOn()) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/forgejo-runner-12.3.0/act/jobparser/testdata/expand_reusable_needs_recursive-1.yaml new/forgejo-runner-12.3.1/act/jobparser/testdata/expand_reusable_needs_recursive-1.yaml --- old/forgejo-runner-12.3.0/act/jobparser/testdata/expand_reusable_needs_recursive-1.yaml 2025-12-21 17:32:28.000000000 +0100 +++ new/forgejo-runner-12.3.1/act/jobparser/testdata/expand_reusable_needs_recursive-1.yaml 2025-12-24 17:06:29.000000000 +0100 @@ -1,3 +1,8 @@ +on: + workflow_call: + inputs: + input1: + type: string name: test jobs: job1: @@ -7,3 +12,6 @@ reusable-2: uses: ./.forgejo/workflows/expand_reusable_needs_recursive-2.yml needs: job1 + with: + input1: ${{ inputs.input1 }} + input2: this input comes from the first layer inner job diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/forgejo-runner-12.3.0/act/jobparser/testdata/expand_reusable_needs_recursive-2.yaml new/forgejo-runner-12.3.1/act/jobparser/testdata/expand_reusable_needs_recursive-2.yaml --- old/forgejo-runner-12.3.0/act/jobparser/testdata/expand_reusable_needs_recursive-2.yaml 2025-12-21 17:32:28.000000000 +0100 +++ new/forgejo-runner-12.3.1/act/jobparser/testdata/expand_reusable_needs_recursive-2.yaml 2025-12-24 17:06:29.000000000 +0100 @@ -1,3 +1,10 @@ +on: + workflow_call: + inputs: + input1: + type: string + input2: + type: string name: test jobs: job1: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/forgejo-runner-12.3.0/act/jobparser/testdata/expand_reusable_needs_recursive.in.yaml new/forgejo-runner-12.3.1/act/jobparser/testdata/expand_reusable_needs_recursive.in.yaml --- old/forgejo-runner-12.3.0/act/jobparser/testdata/expand_reusable_needs_recursive.in.yaml 2025-12-21 17:32:28.000000000 +0100 +++ new/forgejo-runner-12.3.1/act/jobparser/testdata/expand_reusable_needs_recursive.in.yaml 2025-12-24 17:06:29.000000000 +0100 @@ -2,3 +2,5 @@ jobs: reusable-1: uses: ./.forgejo/workflows/expand_reusable_needs_recursive-1.yml + with: + input1: this input comes from the outer job diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/forgejo-runner-12.3.0/act/jobparser/testdata/expand_reusable_needs_recursive.out.yaml new/forgejo-runner-12.3.1/act/jobparser/testdata/expand_reusable_needs_recursive.out.yaml --- old/forgejo-runner-12.3.0/act/jobparser/testdata/expand_reusable_needs_recursive.out.yaml 2025-12-21 17:32:28.000000000 +0100 +++ new/forgejo-runner-12.3.1/act/jobparser/testdata/expand_reusable_needs_recursive.out.yaml 2025-12-24 17:06:29.000000000 +0100 @@ -9,9 +9,17 @@ runs-on: [] if: false __metadata: + workflow_call_inputs: + input1: this input comes from the outer job workflow_call_id: b5a9f46f1f2513d7777fde50b169d323a6519e349cc175484c947ac315a209ed --- name: test +"on": + workflow_call: + inputs: + input1: + default: this input comes from the outer job + type: string jobs: reusable-1.job1: name: job1 @@ -22,6 +30,12 @@ workflow_call_parent: b5a9f46f1f2513d7777fde50b169d323a6519e349cc175484c947ac315a209ed --- name: test +"on": + workflow_call: + inputs: + input1: + default: this input comes from the outer job + type: string jobs: reusable-1.reusable-2: name: reusable-2 @@ -31,10 +45,22 @@ runs-on: [] if: false __metadata: + workflow_call_inputs: + input1: this input comes from the outer job + input2: this input comes from the first layer inner job workflow_call_id: ea7d9f7b51d5cee1511dbe76b9763615f58ecbcb7832e144a8eec2535b380f1e workflow_call_parent: b5a9f46f1f2513d7777fde50b169d323a6519e349cc175484c947ac315a209ed --- name: test +"on": + workflow_call: + inputs: + input1: + default: this input comes from the outer job + type: string + input2: + default: this input comes from the first layer inner job + type: string jobs: reusable-1.reusable-2.job1: name: job1 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/forgejo-runner-12.3.0/act/model/reusable.go new/forgejo-runner-12.3.1/act/model/reusable.go --- old/forgejo-runner-12.3.0/act/model/reusable.go 2025-12-21 17:32:28.000000000 +0100 +++ new/forgejo-runner-12.3.1/act/model/reusable.go 2025-12-24 17:06:29.000000000 +0100 @@ -7,9 +7,19 @@ "strings" ) -// Parsed version of a remote `job.<job-id>.uses:` reference, such as `uses: -// org/repo/.forgejo/workflows/reusable-workflow.yml`. -type RemoteReusableWorkflow struct { +// NonLocalReusableWorkflow is either a [NonLocalReusableWorkflowReference] or an [ExternalReusableWorkflowReference]. +type NonLocalReusableWorkflow interface { + // Access the shared details that are present on all non-local reusable workflows. + Reference() *NonLocalReusableWorkflowReference + + // ConvertExternalWithDefaultBaseURL converts this reference from a remote into an external reference with the given + // base URL. If it is already an external reference, it is returned unchanged. + ConvertExternalWithDefaultBaseURL(baseURL string) *ExternalReusableWorkflowReference +} + +// Parsed version of a `job.<job-id>.uses:` reference, such as `uses: +// org/repo/.forgejo/workflows/reusable-workflow.yml@v1` +type NonLocalReusableWorkflowReference struct { Org string Repo string Filename string @@ -18,9 +28,8 @@ GitPlatform string } -// newRemoteReusableWorkflowWithPlat create a `remoteReusableWorkflow` -// workflows from `.gitea/workflows` and `.github/workflows` are supported -func NewRemoteReusableWorkflowWithPlat(uses string) *RemoteReusableWorkflow { +// parseReusableWorkflowReference parses a workflow reference like `.forgejo/workflows/reusable-workflow.yml@v1` +func parseReusableWorkflowReference(uses string) *NonLocalReusableWorkflowReference { // GitHub docs: // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_iduses r := regexp.MustCompile(`^([^/]+)/([^/]+)/\.([^/]+)/workflows/([^@]+)@(.*)$`) @@ -28,7 +37,7 @@ if len(matches) != 6 { return nil } - return &RemoteReusableWorkflow{ + return &NonLocalReusableWorkflowReference{ Org: matches[1], Repo: matches[2], GitPlatform: matches[3], @@ -37,27 +46,45 @@ } } -func (r *RemoteReusableWorkflow) FilePath() string { +func (r *NonLocalReusableWorkflowReference) FilePath() string { return fmt.Sprintf("./.%s/workflows/%s", r.GitPlatform, r.Filename) } -type RemoteReusableWorkflowWithBaseURL struct { - RemoteReusableWorkflow - BaseURL *string +func (r *NonLocalReusableWorkflowReference) Reference() *NonLocalReusableWorkflowReference { + return r } -func (r *RemoteReusableWorkflowWithBaseURL) CloneURL() string { - if r.BaseURL == nil { - return "" +func (r *NonLocalReusableWorkflowReference) ConvertExternalWithDefaultBaseURL(defaultBaseURL string) *ExternalReusableWorkflowReference { + return &ExternalReusableWorkflowReference{ + NonLocalReusableWorkflowReference: *r, + BaseURL: defaultBaseURL, } - return fmt.Sprintf("%s/%s/%s", *r.BaseURL, r.Org, r.Repo) } -// Parses a `uses` declaration for a "remote" (not this repo) reusable workflow. Typically something like -// "some-org/some-repo/.forgejo/workflows/called-workflow.yml@v1". Can also be domain-qualified, in which case the -// `BaseURL` field will be populated -- otherwise it should be assumed to be an org/repo on the same Forgejo instance as -// the `uses: ...` was declared. -func ParseRemoteReusableWorkflow(uses string) (*RemoteReusableWorkflowWithBaseURL, error) { +// Parsed version of `job.<job-id>.uses:` which is fully qualified with a URL, such as `uses: +// https://example.com/org/repo/.forgejo/workflows/reusable-workflow.yml@v1`. An external workflow reference has a +// BaseURL that represents the `https://example.com` base. +type ExternalReusableWorkflowReference struct { + NonLocalReusableWorkflowReference + BaseURL string +} + +func (r *ExternalReusableWorkflowReference) CloneURL() string { + return fmt.Sprintf("%s/%s/%s", r.BaseURL, r.Org, r.Repo) +} + +func (r *ExternalReusableWorkflowReference) Reference() *NonLocalReusableWorkflowReference { + return &r.NonLocalReusableWorkflowReference +} + +func (r *ExternalReusableWorkflowReference) ConvertExternalWithDefaultBaseURL(defaultBaseURL string) *ExternalReusableWorkflowReference { + return r +} + +// Parses a `uses` declaration such as `uses: some-org/some-repo/.forgejo/workflows/called-workflow.yml@v1`. The +// returned object may be a [NonLocalReusableWorkflowReference] or an [ExternalReusableWorkflowReference] if it was a fully +// qualified URL. +func ParseRemoteReusableWorkflow(uses string) (NonLocalReusableWorkflow, error) { url, err := url.Parse(uses) if err != nil { return nil, fmt.Errorf("'%s' cannot be parsed as a URL: %v", uses, err) @@ -71,12 +98,16 @@ baseURL = &innerBaseURL } - remoteReusableWorkflow := NewRemoteReusableWorkflowWithPlat(strings.TrimPrefix(url.Path, "/")) - if remoteReusableWorkflow == nil { + reusableWorkflowReference := parseReusableWorkflowReference(strings.TrimPrefix(url.Path, "/")) + if reusableWorkflowReference == nil { return nil, fmt.Errorf("expected format {owner}/{repo}/.{git_platform}/workflows/{filename}@{ref}. Actual '%s' Input string was not in a correct format", url.Path) } - return &RemoteReusableWorkflowWithBaseURL{ - RemoteReusableWorkflow: *remoteReusableWorkflow, - BaseURL: baseURL, + if baseURL == nil { + return reusableWorkflowReference, nil + } + + return &ExternalReusableWorkflowReference{ + NonLocalReusableWorkflowReference: *reusableWorkflowReference, + BaseURL: *baseURL, }, nil } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/forgejo-runner-12.3.0/act/model/reusable_test.go new/forgejo-runner-12.3.1/act/model/reusable_test.go --- old/forgejo-runner-12.3.0/act/model/reusable_test.go 2025-12-21 17:32:28.000000000 +0100 +++ new/forgejo-runner-12.3.1/act/model/reusable_test.go 2025-12-24 17:06:29.000000000 +0100 @@ -68,17 +68,18 @@ } else { require.NoError(t, err) assert.NotNil(t, result) + external, ok := result.(*ExternalReusableWorkflowReference) if tt.expectedBaseURL != "" { - require.NotNil(t, result.BaseURL) - assert.Equal(t, tt.expectedBaseURL, *result.BaseURL) + require.True(t, ok) + assert.Equal(t, tt.expectedBaseURL, external.BaseURL) } else { - assert.Nil(t, result.BaseURL) + assert.False(t, ok) } - assert.Equal(t, tt.expectedOrg, result.Org) - assert.Equal(t, tt.expectedRepo, result.Repo) - assert.Equal(t, tt.expectedPlatform, result.GitPlatform) - assert.Equal(t, tt.expectedFilename, result.Filename) - assert.Equal(t, tt.expectedRef, result.Ref) + assert.Equal(t, tt.expectedOrg, result.Reference().Org) + assert.Equal(t, tt.expectedRepo, result.Reference().Repo) + assert.Equal(t, tt.expectedPlatform, result.Reference().GitPlatform) + assert.Equal(t, tt.expectedFilename, result.Reference().Filename) + assert.Equal(t, tt.expectedRef, result.Reference().Ref) } }) } @@ -130,7 +131,7 @@ for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := NewRemoteReusableWorkflowWithPlat(tt.uses) + result := parseReusableWorkflowReference(tt.uses) if tt.shouldFail { assert.Nil(t, result) @@ -147,13 +148,12 @@ } func TestRemoteReusableWorkflow_CloneURL(t *testing.T) { - baseURL := "https://code.forgejo.org" - rw := &RemoteReusableWorkflowWithBaseURL{ - RemoteReusableWorkflow: RemoteReusableWorkflow{ + rw := &ExternalReusableWorkflowReference{ + NonLocalReusableWorkflowReference: NonLocalReusableWorkflowReference{ Org: "owner", Repo: "repo", }, - BaseURL: &baseURL, + BaseURL: "https://code.forgejo.org", } assert.Equal(t, "https://code.forgejo.org/owner/repo", rw.CloneURL()) } @@ -187,7 +187,7 @@ for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - rw := &RemoteReusableWorkflow{ + rw := &NonLocalReusableWorkflowReference{ GitPlatform: tt.gitPlatform, Filename: tt.filename, } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/forgejo-runner-12.3.0/act/runner/reusable_workflow.go new/forgejo-runner-12.3.1/act/runner/reusable_workflow.go --- old/forgejo-runner-12.3.0/act/runner/reusable_workflow.go 2025-12-21 17:32:28.000000000 +0100 +++ new/forgejo-runner-12.3.1/act/runner/reusable_workflow.go 2025-12-24 17:06:29.000000000 +0100 @@ -41,18 +41,21 @@ if err != nil { return common.NewErrorExecutor(err) } - if reusable.BaseURL == nil { - reusable.BaseURL = getBaseURL(rc.Config.GitHubInstance) + var externalReference *model.ExternalReusableWorkflowReference + baseURL := getBaseURL(rc.Config.GitHubInstance) + if baseURL == nil { + return common.NewErrorExecutor(fmt.Errorf("unable to determine base URL for reusable workflow reference, configured base is %q", rc.Config.GitHubInstance)) } + externalReference = reusable.ConvertExternalWithDefaultBaseURL(*baseURL) // If the repository is private, we need a token to clone it token := rc.Config.GetToken() makeWorkflowExecutorForWorkTree := func(workflowDir string) common.Executor { - return newReusableWorkflowExecutor(rc, workflowDir, reusable.FilePath()) + return newReusableWorkflowExecutor(rc, workflowDir, reusable.Reference().FilePath()) } - return cloneIfRequired(rc, reusable, token, makeWorkflowExecutorForWorkTree) + return cloneIfRequired(rc, externalReference, token, makeWorkflowExecutorForWorkTree) } // See "Obsolete" note on newLocalReusableWorkflowExecutor -- applies to this as well. @@ -63,18 +66,28 @@ if err != nil { return common.NewErrorExecutor(err) } - if reusable.BaseURL == nil { - reusable.BaseURL = getBaseURL(rc.Config.GitHubInstance) + var externalReference *model.ExternalReusableWorkflowReference + baseURL := getBaseURL(rc.Config.GitHubInstance) + if baseURL == nil { + // reusable can be in two states, it can either be a external reference already in which case we don't need + // `baseURL`, or it can be a reference where we do need baseURL. + f, isExternal := reusable.(*model.ExternalReusableWorkflowReference) + if !isExternal { + return common.NewErrorExecutor(fmt.Errorf("unable to determine base URL for reusable workflow reference %q, configured base is %q", uses, rc.Config.GitHubInstance)) + } + externalReference = f + } else { + externalReference = reusable.ConvertExternalWithDefaultBaseURL(*baseURL) } // FIXME: if the reusable workflow is from a private repository, we need to provide a token to access the repository. token := "" makeWorkflowExecutorForWorkTree := func(workflowDir string) common.Executor { - return newReusableWorkflowExecutor(rc, workflowDir, reusable.FilePath()) + return newReusableWorkflowExecutor(rc, workflowDir, reusable.Reference().FilePath()) } - return cloneIfRequired(rc, reusable, token, makeWorkflowExecutorForWorkTree) + return cloneIfRequired(rc, externalReference, token, makeWorkflowExecutorForWorkTree) } // gitHubInstance can be a URL or just a hostname -- return a URL for consistency @@ -89,7 +102,7 @@ return nil } -func cloneIfRequired(rc *RunContext, remoteReusableWorkflow *model.RemoteReusableWorkflowWithBaseURL, token string, makeWorkflowExecutorForWorkTree func(workflowDir string) common.Executor) common.Executor { +func cloneIfRequired(rc *RunContext, remoteReusableWorkflow *model.ExternalReusableWorkflowReference, token string, makeWorkflowExecutorForWorkTree func(workflowDir string) common.Executor) common.Executor { return func(ctx context.Context) error { // Do not change the remoteReusableWorkflow.URL, because: // 1. Gitea doesn't support specifying GithubContext.ServerURL by the GITHUB_SERVER_URL env ++++++ forgejo-runner.obsinfo ++++++ --- /var/tmp/diff_new_pack.QnEcko/_old 2025-12-25 19:58:04.272280160 +0100 +++ /var/tmp/diff_new_pack.QnEcko/_new 2025-12-25 19:58:04.284280648 +0100 @@ -1,5 +1,5 @@ name: forgejo-runner -version: 12.3.0 -mtime: 1766334748 -commit: 54bfaa785975eae868b6e5918d727f30c6d90445 +version: 12.3.1 +mtime: 1766592389 +commit: 4e1da369ef8c5e6ce963b0121393af6f78d0df0b ++++++ vendor.tar.gz ++++++
