Repository: nektos/act (language: go)

Issue:
Issue: support for reusable workflows 
ref: https://github.com/github/roadmap/issues/98

GitHub is going to support reusable workflows (similar to Azure DevOps Pipelines)

> How will it work?
> To reuse a workflow, ensure it listens to a new `workflow_call` event trigger, and then reference it with the familiar `uses` syntax. Optionally, use `inputs` and `outputs` to pass data between them, just like with actions.
> 
> For example:
> 
> ```yaml
> name: Build & Release App
> 
> on:
>   push:
> 
> jobs:
>   build:
>     uses: example-org/automation/workflows/build.yml@1
> 
>   release:
>     needs: build
>     uses: example-org/automation/workflows/release.yml@1
>     with:
>       artifact-url: jobs.build.outputs.artifact-url
> ```


Discussion / hints:
I want to add someone found the beta documentation ([ source ](https://github.community/t/ref-head-in-reusable-workflows/203690))
https://docs.github.com/en/actions/learn-github-actions/reusing-workflows

I'm going to implement it in my own actions-service (runner.server)

It seems to be even deployed as beta. Far more information than in the roadmap.

**EDIT** 2021/10/28:
_For reusable workflows it might be useful to backport the following commit https://github.com/ChristopherHX/act/commit/efdc2fa17528a643addc00009a2299d2f514da6d after my PR `uses in composite` is merged_

**EDIT** 2022/03/28
The commit mentioned in the previous edit has been merged via the `uses in composite` and is already in `v0.2.26`.
This issue has a low priority for me.
DONE:
- make inputs context available in the runcontext

TODO:
- obtain the reusable workflows in the planner
- evaluate the inputs and secrets
- only make evaluated secrets available in the reusable workflow
- assign the evaluated inputs to the runcontext of the reusable workflow
- evaluate workflow outputs and add the `jobs` context, the same as `needs` of a job which depends on all other jobs in the reusable workflow
We just started implementing reusable workflows. As I was going through the pains of several commits and merges leading to broken workflows I came across this repo! It was linked in the recent Thoughtworks Technology Radar. Anyways just here to say I'm looking forward to using it !! 
https://github.blog/changelog/2021-11-24-github-actions-reusable-workflows-are-generally-available/
News?

I received an error when trying to use a reusable workflow by act:

```
ERRO[0000] 'runs-on' key not defined in Terraform Plan - Github Actions Runner/production 
ERRO[0000] 'runs-on' key not defined in Terraform Plan - Github Actions Runner/production 
ERRO[0000] 'runs-on' key not defined in Terraform Plan - Github Actions Runner/production 
```
That's because they are not supported
Hi, 
I  also get the following when trying to run a job that uses a reusable workflow:
```
time="2022-01-19T14:19:37+01:00" level=error msg="'runs-on' key not defined in <workflow_name to test>/<job_name>"
time="2022-01-19T14:19:37+01:00" level=error msg="'runs-on' key not defined in <workflow_name to test>/<job_name>"
time="2022-01-19T14:19:37+01:00" level=error msg="'runs-on' key not defined in <workflow_name to test>/<job_name>"
```

Any plans on implementing support for reusable workflows?
Hi, just wondering if support for reusable workflows is on the backlog for act?  As I've started refactoring my workflows to support them, I found that unfortunately it broke my ability to use act for unit testing.
> Hi, just wondering if support for reusable workflows is on the backlog for act?

Yes, that's why this issue is opened

--- EDIT ---

Issue is locked because none of recent responses are helpful at all. Please familiarise yourself with https://github.com/golang/go/wiki/NoPlusOne/
In case of this, we are aware of the reusable workflows (I raised the issue after all). If there will be any news regarding this issue, it will be updated and/or unlocked.
If there will be an attempt at implementing it, its pull request will be linked. (Please feel free to contribute it)
Please do not ask when is this going to be implemented. It's annoying and doesn't contribute at all.
Any Update?


Fix patch:
diff --git a/pkg/runner/reusable_workflow.go b/pkg/runner/reusable_workflow.go
index 87b7bde944b..b080b4dbb1d 100644
--- a/pkg/runner/reusable_workflow.go
+++ b/pkg/runner/reusable_workflow.go
@@ -1,35 +1,88 @@
 package runner
 
 import (
+	"context"
+	"errors"
 	"fmt"
+	"io/fs"
+	"os"
 	"path"
+	"regexp"
+	"strings"
+	"sync"
 
 	"github.com/nektos/act/pkg/common"
+	"github.com/nektos/act/pkg/common/git"
 	"github.com/nektos/act/pkg/model"
 )
 
 func newLocalReusableWorkflowExecutor(rc *RunContext) common.Executor {
-	return newReusableWorkflowExecutor(rc, rc.Config.Workdir)
+	return newReusableWorkflowExecutor(rc, rc.Config.Workdir, rc.Run.Job().Uses)
 }
 
 func newRemoteReusableWorkflowExecutor(rc *RunContext) common.Executor {
-	return common.NewErrorExecutor(fmt.Errorf("remote reusable workflows are currently not supported (see https://github.com/nektos/act/issues/826 for updates)"))
-}
+	uses := rc.Run.Job().Uses
 
-func newReusableWorkflowExecutor(rc *RunContext, directory string) common.Executor {
-	planner, err := model.NewWorkflowPlanner(path.Join(directory, rc.Run.Job().Uses), true)
-	if err != nil {
-		return common.NewErrorExecutor(err)
+	remoteReusableWorkflow := newRemoteReusableWorkflow(uses)
+	if remoteReusableWorkflow == nil {
+		return common.NewErrorExecutor(fmt.Errorf("expected format {owner}/{repo}/.github/workflows/{filename}@{ref}. Actual '%s' Input string was not in a correct format", uses))
 	}
+	remoteReusableWorkflow.URL = rc.Config.GitHubInstance
+
+	workflowDir := fmt.Sprintf("%s/%s", rc.ActionCacheDir(), strings.ReplaceAll(uses, "/", "-"))
+
+	return common.NewPipelineExecutor(
+		newMutexExecutor(cloneIfRequired(rc, *remoteReusableWorkflow, workflowDir)),
+		newReusableWorkflowExecutor(rc, workflowDir, fmt.Sprintf("./.github/workflows/%s", remoteReusableWorkflow.Filename)),
+	)
+}
+
+var (
+	executorLock sync.Mutex
+)
 
-	plan := planner.PlanEvent("workflow_call")
+func newMutexExecutor(executor common.Executor) common.Executor {
+	return func(ctx context.Context) error {
+		executorLock.Lock()
+		defer executorLock.Unlock()
 
-	runner, err := NewReusableWorkflowRunner(rc)
-	if err != nil {
-		return common.NewErrorExecutor(err)
+		return executor(ctx)
 	}
+}
+
+func cloneIfRequired(rc *RunContext, remoteReusableWorkflow remoteReusableWorkflow, targetDirectory string) common.Executor {
+	return common.NewConditionalExecutor(
+		func(ctx context.Context) bool {
+			_, err := os.Stat(targetDirectory)
+			notExists := errors.Is(err, fs.ErrNotExist)
+			return notExists
+		},
+		git.NewGitCloneExecutor(git.NewGitCloneExecutorInput{
+			URL:   remoteReusableWorkflow.CloneURL(),
+			Ref:   remoteReusableWorkflow.Ref,
+			Dir:   targetDirectory,
+			Token: rc.Config.Token,
+		}),
+		nil,
+	)
+}
+
+func newReusableWorkflowExecutor(rc *RunContext, directory string, workflow string) common.Executor {
+	return func(ctx context.Context) error {
+		planner, err := model.NewWorkflowPlanner(path.Join(directory, workflow), true)
+		if err != nil {
+			return err
+		}
 
-	return runner.NewPlanExecutor(plan)
+		plan := planner.PlanEvent("workflow_call")
+
+		runner, err := NewReusableWorkflowRunner(rc)
+		if err != nil {
+			return err
+		}
+
+		return runner.NewPlanExecutor(plan)(ctx)
+	}
 }
 
 func NewReusableWorkflowRunner(rc *RunContext) (Runner, error) {
@@ -43,3 +96,32 @@ func NewReusableWorkflowRunner(rc *RunContext) (Runner, error) {
 
 	return runner.configure()
 }
+
+type remoteReusableWorkflow struct {
+	URL      string
+	Org      string
+	Repo     string
+	Filename string
+	Ref      string
+}
+
+func (r *remoteReusableWorkflow) CloneURL() string {
+	return fmt.Sprintf("https://%s/%s/%s", r.URL, r.Org, r.Repo)
+}
+
+func newRemoteReusableWorkflow(uses string) *remoteReusableWorkflow {
+	// GitHub docs:
+	// https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_iduses
+	r := regexp.MustCompile(`^([^/]+)/([^/]+)/.github/workflows/([^@]+)@(.*)$`)
+	matches := r.FindStringSubmatch(uses)
+	if len(matches) != 5 {
+		return nil
+	}
+	return &remoteReusableWorkflow{
+		Org:      matches[1],
+		Repo:     matches[2],
+		Filename: matches[3],
+		Ref:      matches[4],
+		URL:      "github.com",
+	}
+}


Test patch:
diff --git a/pkg/runner/runner_test.go b/pkg/runner/runner_test.go
index 6096abe1adc..9298afcc452 100644
--- a/pkg/runner/runner_test.go
+++ b/pkg/runner/runner_test.go
@@ -145,7 +145,7 @@ func TestRunEvent(t *testing.T) {
 		{workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms, secrets},
 		{workdir, "uses-nested-composite", "push", "", platforms, secrets},
 		{workdir, "remote-action-composite-js-pre-with-defaults", "push", "", platforms, secrets},
-		{workdir, "uses-workflow", "push", "reusable workflows are currently not supported (see https://github.com/nektos/act/issues/826 for updates)", platforms, secrets},
+		{workdir, "uses-workflow", "push", "", platforms, map[string]string{"secret": "keep_it_private"}},
 		{workdir, "uses-workflow", "pull_request", "", platforms, map[string]string{"secret": "keep_it_private"}},
 		{workdir, "uses-docker-url", "push", "", platforms, secrets},
 		{workdir, "act-composite-env-test", "push", "", platforms, secrets},
diff --git a/pkg/runner/testdata/uses-workflow/push.yml b/pkg/runner/testdata/uses-workflow/push.yml
index 855dacfe187..ddc37b86153 100644
--- a/pkg/runner/testdata/uses-workflow/push.yml
+++ b/pkg/runner/testdata/uses-workflow/push.yml
@@ -2,8 +2,34 @@ on: push
 
 jobs:
   reusable-workflow:
-    uses: nektos/act-tests/.github/workflows/reusable-workflow.yml@master
+    uses: nektos/act-test-actions/.github/workflows/reusable-workflow.yml@main
     with:
-      username: mona
+      string_required: string
+      bool_required: ${{ true }}
+      number_required: 1
     secrets:
-      envPATH: ${{ secrets.envPAT }}
+      secret: keep_it_private
+
+  reusable-workflow-with-inherited-secrets:
+    uses: nektos/act-test-actions/.github/workflows/reusable-workflow.yml@main
+    with:
+      string_required: string
+      bool_required: ${{ true }}
+      number_required: 1
+    secrets: inherit
+
+  output-test:
+    runs-on: ubuntu-latest
+    needs:
+      - reusable-workflow
+      - reusable-workflow-with-inherited-secrets
+    steps:
+      - name: output with secrets map
+        run: |
+          echo reusable-workflow.output=${{ needs.reusable-workflow.outputs.output }}
+          [[ "${{ needs.reusable-workflow.outputs.output == 'string' }}" = "true" ]] || exit 1
+
+      - name: output with inherited secrets
+        run: |
+          echo reusable-workflow-with-inherited-secrets.output=${{ needs.reusable-workflow-with-inherited-secrets.outputs.output }}
+          [[ "${{ needs.reusable-workflow-with-inherited-secrets.outputs.output == 'string' }}" = "true" ]] || exit 1