From 935b00bd7cf45083bddaeae4ad2a08f6f59a144c Mon Sep 17 00:00:00 2001 From: Markus Wolf Date: Tue, 20 Dec 2022 14:22:37 +0100 Subject: [PATCH] feat: add remote reusable workflows This changes adds cloning of a remote repository to run a workflow included in it. Closes #826 --- pkg/runner/reusable_workflow.go | 62 ++++++++++++++++++++-- pkg/runner/runner_test.go | 2 +- pkg/runner/testdata/uses-workflow/push.yml | 32 +++++++++-- 3 files changed, 88 insertions(+), 8 deletions(-) diff --git a/pkg/runner/reusable_workflow.go b/pkg/runner/reusable_workflow.go index 87b7bde944b..5a4ef78337b 100644 --- a/pkg/runner/reusable_workflow.go +++ b/pkg/runner/reusable_workflow.go @@ -3,21 +3,46 @@ package runner import ( "fmt" "path" + "regexp" + "strings" "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 + + 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 + + // todo: really use rc.ActionCacheDir()? + workflowDir := fmt.Sprintf("%s/%s", rc.ActionCacheDir(), strings.ReplaceAll(uses, "/", "-")) + + gitClone := git.NewGitCloneExecutor(git.NewGitCloneExecutorInput{ + URL: remoteReusableWorkflow.CloneURL(), + Ref: remoteReusableWorkflow.Ref, + Dir: workflowDir, + Token: rc.Config.Token, + }) + + return common.NewPipelineExecutor( + gitClone, + newReusableWorkflowExecutor(rc, workflowDir, fmt.Sprintf("./.github/workflows/%s", remoteReusableWorkflow.Filename)), + ) } -func newReusableWorkflowExecutor(rc *RunContext, directory string) common.Executor { - planner, err := model.NewWorkflowPlanner(path.Join(directory, rc.Run.Job().Uses), true) +func newReusableWorkflowExecutor(rc *RunContext, directory string, workflow string) common.Executor { + planner, err := model.NewWorkflowPlanner(path.Join(directory, workflow), true) if err != nil { return common.NewErrorExecutor(err) } @@ -43,3 +68,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", + } +} 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