feat: add remote reusable workflows (#1525)
* feat: add remote reusable workflows This changes adds cloning of a remote repository to run a workflow included in it. Closes #826 * fix: defer plan creation until clone is done We need wait for the full clone (and only clone once) before we start to plan the execution for a remote workflow Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
parent
566b9d843e
commit
82a8c1e80d
3 changed files with 124 additions and 16 deletions
|
@ -1,35 +1,88 @@
|
||||||
package runner
|
package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/common"
|
"github.com/nektos/act/pkg/common"
|
||||||
|
"github.com/nektos/act/pkg/common/git"
|
||||||
"github.com/nektos/act/pkg/model"
|
"github.com/nektos/act/pkg/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newLocalReusableWorkflowExecutor(rc *RunContext) common.Executor {
|
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 {
|
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
|
||||||
|
|
||||||
|
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)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newReusableWorkflowExecutor(rc *RunContext, directory string) common.Executor {
|
var (
|
||||||
planner, err := model.NewWorkflowPlanner(path.Join(directory, rc.Run.Job().Uses), true)
|
executorLock sync.Mutex
|
||||||
if err != nil {
|
)
|
||||||
return common.NewErrorExecutor(err)
|
|
||||||
|
func newMutexExecutor(executor common.Executor) common.Executor {
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
executorLock.Lock()
|
||||||
|
defer executorLock.Unlock()
|
||||||
|
|
||||||
|
return executor(ctx)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
plan := planner.PlanEvent("workflow_call")
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
runner, err := NewReusableWorkflowRunner(rc)
|
func newReusableWorkflowExecutor(rc *RunContext, directory string, workflow string) common.Executor {
|
||||||
if err != nil {
|
return func(ctx context.Context) error {
|
||||||
return common.NewErrorExecutor(err)
|
planner, err := model.NewWorkflowPlanner(path.Join(directory, workflow), true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
plan := planner.PlanEvent("workflow_call")
|
||||||
|
|
||||||
|
runner, err := NewReusableWorkflowRunner(rc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return runner.NewPlanExecutor(plan)(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
return runner.NewPlanExecutor(plan)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReusableWorkflowRunner(rc *RunContext) (Runner, error) {
|
func NewReusableWorkflowRunner(rc *RunContext) (Runner, error) {
|
||||||
|
@ -43,3 +96,32 @@ func NewReusableWorkflowRunner(rc *RunContext) (Runner, error) {
|
||||||
|
|
||||||
return runner.configure()
|
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",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -145,7 +145,7 @@ func TestRunEvent(t *testing.T) {
|
||||||
{workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms, secrets},
|
{workdir, "uses-composite-with-error", "push", "Job 'failing-composite-action' failed", platforms, secrets},
|
||||||
{workdir, "uses-nested-composite", "push", "", platforms, secrets},
|
{workdir, "uses-nested-composite", "push", "", platforms, secrets},
|
||||||
{workdir, "remote-action-composite-js-pre-with-defaults", "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-workflow", "pull_request", "", platforms, map[string]string{"secret": "keep_it_private"}},
|
||||||
{workdir, "uses-docker-url", "push", "", platforms, secrets},
|
{workdir, "uses-docker-url", "push", "", platforms, secrets},
|
||||||
{workdir, "act-composite-env-test", "push", "", platforms, secrets},
|
{workdir, "act-composite-env-test", "push", "", platforms, secrets},
|
||||||
|
|
32
pkg/runner/testdata/uses-workflow/push.yml
vendored
32
pkg/runner/testdata/uses-workflow/push.yml
vendored
|
@ -2,8 +2,34 @@ on: push
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
reusable-workflow:
|
reusable-workflow:
|
||||||
uses: nektos/act-tests/.github/workflows/reusable-workflow.yml@master
|
uses: nektos/act-test-actions/.github/workflows/reusable-workflow.yml@main
|
||||||
with:
|
with:
|
||||||
username: mona
|
string_required: string
|
||||||
|
bool_required: ${{ true }}
|
||||||
|
number_required: 1
|
||||||
secrets:
|
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
|
||||||
|
|
Loading…
Reference in a new issue