1// Copyright 2023 The Gitea Authors. All rights reserved.2// Copyright 2019 nektos3// SPDX-License-Identifier: MIT45package cmd67import (8 "context"9 "fmt"10 "os"11 "path/filepath"12 "strconv"13 "strings"14 "time"1516 "github.com/docker/docker/api/types/container"17 "github.com/joho/godotenv"18 "github.com/nektos/act/pkg/artifactcache"19 "github.com/nektos/act/pkg/artifacts"20 "github.com/nektos/act/pkg/common"21 "github.com/nektos/act/pkg/model"22 "github.com/nektos/act/pkg/runner"23 log "github.com/sirupsen/logrus"24 "github.com/spf13/cobra"25 "golang.org/x/term"26)2728type executeArgs struct {29 runList bool30 job string31 event string32 workdir string33 workflowsPath string34 noWorkflowRecurse bool35 autodetectEvent bool36 forcePull bool37 forceRebuild bool38 jsonLogger bool39 envs []string40 envfile string41 secrets []string42 defaultActionsURL string43 insecureSecrets bool44 privileged bool45 usernsMode string46 containerArchitecture string47 containerDaemonSocket string48 useGitIgnore bool49 containerCapAdd []string50 containerCapDrop []string51 containerOptions string52 artifactServerPath string53 artifactServerAddr string54 artifactServerPort string55 noSkipCheckout bool56 debug bool57 dryrun bool58 image string59 cacheHandler *artifactcache.Handler60 network string61 enableIPv6 bool62 githubInstance string63}6465// WorkflowsPath returns path to workflow file(s)66func (i *executeArgs) WorkflowsPath() string {67 return i.resolve(i.workflowsPath)68}6970// Envfile returns path to .env71func (i *executeArgs) Envfile() string {72 return i.resolve(i.envfile)73}7475func (i *executeArgs) LoadSecrets() map[string]string {76 s := make(map[string]string)77 for _, secretPair := range i.secrets {78 secretPairParts := strings.SplitN(secretPair, "=", 2)79 secretPairParts[0] = strings.ToUpper(secretPairParts[0])80 if strings.ToUpper(s[secretPairParts[0]]) == secretPairParts[0] {81 log.Errorf("Secret %s is already defined (secrets are case insensitive)", secretPairParts[0])82 }83 if len(secretPairParts) == 2 {84 s[secretPairParts[0]] = secretPairParts[1]85 } else if env, ok := os.LookupEnv(secretPairParts[0]); ok && env != "" {86 s[secretPairParts[0]] = env87 } else {88 fmt.Printf("Provide value for '%s': ", secretPairParts[0])89 val, err := term.ReadPassword(int(os.Stdin.Fd()))90 fmt.Println()91 if err != nil {92 log.Errorf("failed to read input: %v", err)93 os.Exit(1)94 }95 s[secretPairParts[0]] = string(val)96 }97 }98 return s99}100101func readEnvs(path string, envs map[string]string) bool {102 if _, err := os.Stat(path); err == nil {103 env, err := godotenv.Read(path)104 if err != nil {105 log.Fatalf("Error loading from %s: %v", path, err)106 }107 for k, v := range env {108 envs[k] = v109 }110 return true111 }112 return false113}114115func (i *executeArgs) LoadEnvs() map[string]string {116 envs := make(map[string]string)117 if i.envs != nil {118 for _, envVar := range i.envs {119 e := strings.SplitN(envVar, `=`, 2)120 if len(e) == 2 {121 envs[e[0]] = e[1]122 } else {123 envs[e[0]] = ""124 }125 }126 }127 _ = readEnvs(i.Envfile(), envs)128129 envs["ACTIONS_CACHE_URL"] = i.cacheHandler.ExternalURL() + "/"130131 return envs132}133134// Workdir returns path to workdir135func (i *executeArgs) Workdir() string {136 return i.resolve(".")137}138139func (i *executeArgs) resolve(path string) string {140 basedir, err := filepath.Abs(i.workdir)141 if err != nil {142 log.Fatal(err)143 }144 if path == "" {145 return path146 }147 if !filepath.IsAbs(path) {148 path = filepath.Join(basedir, path)149 }150 return path151}152153func printList(plan *model.Plan) error {154 type lineInfoDef struct {155 jobID string156 jobName string157 stage string158 wfName string159 wfFile string160 events string161 }162 lineInfos := []lineInfoDef{}163164 header := lineInfoDef{165 jobID: "Job ID",166 jobName: "Job name",167 stage: "Stage",168 wfName: "Workflow name",169 wfFile: "Workflow file",170 events: "Events",171 }172173 jobs := map[string]bool{}174 duplicateJobIDs := false175176 jobIDMaxWidth := len(header.jobID)177 jobNameMaxWidth := len(header.jobName)178 stageMaxWidth := len(header.stage)179 wfNameMaxWidth := len(header.wfName)180 wfFileMaxWidth := len(header.wfFile)181 eventsMaxWidth := len(header.events)182183 for i, stage := range plan.Stages {184 for _, r := range stage.Runs {185 jobID := r.JobID186 line := lineInfoDef{187 jobID: jobID,188 jobName: r.String(),189 stage: strconv.Itoa(i),190 wfName: r.Workflow.Name,191 wfFile: r.Workflow.File,192 events: strings.Join(r.Workflow.On(), `,`),193 }194 if _, ok := jobs[jobID]; ok {195 duplicateJobIDs = true196 } else {197 jobs[jobID] = true198 }199 lineInfos = append(lineInfos, line)200 if jobIDMaxWidth < len(line.jobID) {201 jobIDMaxWidth = len(line.jobID)202 }203 if jobNameMaxWidth < len(line.jobName) {204 jobNameMaxWidth = len(line.jobName)205 }206 if stageMaxWidth < len(line.stage) {207 stageMaxWidth = len(line.stage)208 }209 if wfNameMaxWidth < len(line.wfName) {210 wfNameMaxWidth = len(line.wfName)211 }212 if wfFileMaxWidth < len(line.wfFile) {213 wfFileMaxWidth = len(line.wfFile)214 }215 if eventsMaxWidth < len(line.events) {216 eventsMaxWidth = len(line.events)217 }218 }219 }220221 jobIDMaxWidth += 2222 jobNameMaxWidth += 2223 stageMaxWidth += 2224 wfNameMaxWidth += 2225 wfFileMaxWidth += 2226227 fmt.Printf("%*s%*s%*s%*s%*s%*s\n",228 -stageMaxWidth, header.stage,229 -jobIDMaxWidth, header.jobID,230 -jobNameMaxWidth, header.jobName,231 -wfNameMaxWidth, header.wfName,232 -wfFileMaxWidth, header.wfFile,233 -eventsMaxWidth, header.events,234 )235 for _, line := range lineInfos {236 fmt.Printf("%*s%*s%*s%*s%*s%*s\n",237 -stageMaxWidth, line.stage,238 -jobIDMaxWidth, line.jobID,239 -jobNameMaxWidth, line.jobName,240 -wfNameMaxWidth, line.wfName,241 -wfFileMaxWidth, line.wfFile,242 -eventsMaxWidth, line.events,243 )244 }245 if duplicateJobIDs {246 fmt.Print("\nDetected multiple jobs with the same job name, use `-W` to specify the path to the specific workflow.\n")247 }248 return nil249}250251func runExecList(ctx context.Context, planner model.WorkflowPlanner, execArgs *executeArgs) error {252 // plan with filtered jobs - to be used for filtering only253 var filterPlan *model.Plan254255 // Determine the event name to be filtered256 var filterEventName string257258 if len(execArgs.event) > 0 {259 log.Infof("Using chosed event for filtering: %s", execArgs.event)260 filterEventName = execArgs.event261 } else if execArgs.autodetectEvent {262 // collect all events from loaded workflows263 events := planner.GetEvents()264265 // set default event type to first event from many available266 // this way user dont have to specify the event.267 log.Infof("Using first detected workflow event for filtering: %s", events[0])268269 filterEventName = events[0]270 }271272 var err error273 if execArgs.job != "" {274 log.Infof("Preparing plan with a job: %s", execArgs.job)275 filterPlan, err = planner.PlanJob(execArgs.job)276 if err != nil {277 return err278 }279 } else if filterEventName != "" {280 log.Infof("Preparing plan for a event: %s", filterEventName)281 filterPlan, err = planner.PlanEvent(filterEventName)282 if err != nil {283 return err284 }285 } else {286 log.Infof("Preparing plan with all jobs")287 filterPlan, err = planner.PlanAll()288 if err != nil {289 return err290 }291 }292293 _ = printList(filterPlan)294295 return nil296}297298func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command, args []string) error {299 return func(cmd *cobra.Command, args []string) error {300 planner, err := model.NewWorkflowPlanner(execArgs.WorkflowsPath(), execArgs.noWorkflowRecurse)301 if err != nil {302 return err303 }304305 if execArgs.runList {306 return runExecList(ctx, planner, execArgs)307 }308309 // plan with triggered jobs310 var plan *model.Plan311312 // Determine the event name to be triggered313 var eventName string314315 // collect all events from loaded workflows316 events := planner.GetEvents()317318 if len(execArgs.event) > 0 {319 log.Infof("Using chosed event for filtering: %s", execArgs.event)320 eventName = execArgs.event321 } else if len(events) == 1 && len(events[0]) > 0 {322 log.Infof("Using the only detected workflow event: %s", events[0])323 eventName = events[0]324 } else if execArgs.autodetectEvent && len(events) > 0 && len(events[0]) > 0 {325 // set default event type to first event from many available326 // this way user dont have to specify the event.327 log.Infof("Using first detected workflow event: %s", events[0])328 eventName = events[0]329 } else {330 log.Infof("Using default workflow event: push")331 eventName = "push"332 }333334 // build the plan for this run335 if execArgs.job != "" {336 log.Infof("Planning job: %s", execArgs.job)337 plan, err = planner.PlanJob(execArgs.job)338 if err != nil {339 return err340 }341 } else {342 log.Infof("Planning jobs for event: %s", eventName)343 plan, err = planner.PlanEvent(eventName)344 if err != nil {345 return err346 }347 }348349 maxLifetime := 3 * time.Hour350 if deadline, ok := ctx.Deadline(); ok {351 maxLifetime = time.Until(deadline)352 }353354 // init a cache server355 handler, err := artifactcache.StartHandler("", "", 0, log.StandardLogger().WithField("module", "cache_request"))356 if err != nil {357 return err358 }359 log.Infof("cache handler listens on: %v", handler.ExternalURL())360 execArgs.cacheHandler = handler361362 if len(execArgs.artifactServerAddr) == 0 {363 ip := common.GetOutboundIP()364 if ip == nil {365 return fmt.Errorf("unable to determine outbound IP address")366 }367 execArgs.artifactServerAddr = ip.String()368 }369370 if len(execArgs.artifactServerPath) == 0 {371 tempDir, err := os.MkdirTemp("", "gitea-act-")372 if err != nil {373 fmt.Println(err)374 }375 defer os.RemoveAll(tempDir)376377 execArgs.artifactServerPath = tempDir378 }379380 // run the plan381 config := &runner.Config{382 Workdir: execArgs.Workdir(),383 BindWorkdir: false,384 ReuseContainers: false,385 ForcePull: execArgs.forcePull,386 ForceRebuild: execArgs.forceRebuild,387 LogOutput: true,388 JSONLogger: execArgs.jsonLogger,389 Env: execArgs.LoadEnvs(),390 Secrets: execArgs.LoadSecrets(),391 InsecureSecrets: execArgs.insecureSecrets,392 Privileged: execArgs.privileged,393 UsernsMode: execArgs.usernsMode,394 ContainerArchitecture: execArgs.containerArchitecture,395 ContainerDaemonSocket: execArgs.containerDaemonSocket,396 UseGitIgnore: execArgs.useGitIgnore,397 GitHubInstance: execArgs.githubInstance,398 ContainerCapAdd: execArgs.containerCapAdd,399 ContainerCapDrop: execArgs.containerCapDrop,400 ContainerOptions: execArgs.containerOptions,401 AutoRemove: true,402 ArtifactServerPath: execArgs.artifactServerPath,403 ArtifactServerPort: execArgs.artifactServerPort,404 ArtifactServerAddr: execArgs.artifactServerAddr,405 NoSkipCheckout: execArgs.noSkipCheckout,406 // PresetGitHubContext: preset,407 // EventJSON: string(eventJSON),408 ContainerNamePrefix: fmt.Sprintf("FORGEJO-ACTIONS-TASK-%s", eventName),409 ContainerMaxLifetime: maxLifetime,410 ContainerNetworkMode: container.NetworkMode(execArgs.network),411 ContainerNetworkEnableIPv6: execArgs.enableIPv6,412 DefaultActionInstance: execArgs.defaultActionsURL,413 PlatformPicker: func(_ []string) string {414 return execArgs.image415 },416 ValidVolumes: []string{"**"}, // All volumes are allowed for `exec` command417 }418419 config.Env["ACT_EXEC"] = "true"420421 if t := config.Secrets["GITEA_TOKEN"]; t != "" {422 config.Token = t423 } else if t := config.Secrets["GITHUB_TOKEN"]; t != "" {424 config.Token = t425 }426427 if !execArgs.debug {428 logLevel := log.InfoLevel429 config.JobLoggerLevel = &logLevel430 }431432 r, err := runner.New(config)433 if err != nil {434 return err435 }436437 artifactCancel := artifacts.Serve(ctx, execArgs.artifactServerPath, execArgs.artifactServerAddr, execArgs.artifactServerPort)438 log.Debugf("artifacts server started at %s:%s", execArgs.artifactServerPath, execArgs.artifactServerPort)439440 ctx = common.WithDryrun(ctx, execArgs.dryrun)441 executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error {442 artifactCancel()443 return nil444 })445446 return executor(ctx)447 }448}449450func loadExecCmd(ctx context.Context) *cobra.Command {451 execArg := executeArgs{}452453 execCmd := &cobra.Command{454 Use: "exec",455 Short: "Run workflow locally.",456 Args: cobra.MaximumNArgs(20),457 RunE: runExec(ctx, &execArg),458 }459460 execCmd.Flags().BoolVarP(&execArg.runList, "list", "l", false, "list workflows")461 execCmd.Flags().StringVarP(&execArg.job, "job", "j", "", "run a specific job ID")462 execCmd.Flags().StringVarP(&execArg.event, "event", "E", "", "run a event name")463 execCmd.PersistentFlags().StringVarP(&execArg.workflowsPath, "workflows", "W", "./.forgejo/workflows/", "path to workflow file(s)")464 execCmd.PersistentFlags().StringVarP(&execArg.workdir, "directory", "C", ".", "working directory")465 execCmd.PersistentFlags().BoolVarP(&execArg.noWorkflowRecurse, "no-recurse", "", false, "Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag")466 execCmd.Flags().BoolVarP(&execArg.autodetectEvent, "detect-event", "", false, "Use first event type from workflow as event that triggered the workflow")467 execCmd.Flags().BoolVarP(&execArg.forcePull, "pull", "p", false, "pull docker image(s) even if already present")468 execCmd.Flags().BoolVarP(&execArg.forceRebuild, "rebuild", "", false, "rebuild local action docker image(s) even if already present")469 execCmd.PersistentFlags().BoolVar(&execArg.jsonLogger, "json", false, "Output logs in json format")470 execCmd.Flags().StringArrayVarP(&execArg.envs, "env", "", []string{}, "env to make available to actions with optional value (e.g. --env myenv=foo or --env myenv)")471 execCmd.PersistentFlags().StringVarP(&execArg.envfile, "env-file", "", ".env", "environment file to read and use as env in the containers")472 execCmd.Flags().StringArrayVarP(&execArg.secrets, "secret", "s", []string{}, "secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret)")473 execCmd.PersistentFlags().BoolVarP(&execArg.insecureSecrets, "insecure-secrets", "", false, "NOT RECOMMENDED! Doesn't hide secrets while printing logs.")474 execCmd.Flags().BoolVar(&execArg.privileged, "privileged", false, "use privileged mode")475 execCmd.Flags().StringVar(&execArg.usernsMode, "userns", "", "user namespace to use")476 execCmd.PersistentFlags().StringVarP(&execArg.containerArchitecture, "container-architecture", "", "", "Architecture which should be used to run containers, e.g.: linux/amd64. If not specified, will use host default architecture. Requires Docker server API Version 1.41+. Ignored on earlier Docker server platforms.")477 execCmd.PersistentFlags().StringVarP(&execArg.containerDaemonSocket, "container-daemon-socket", "", "/var/run/docker.sock", "Path to Docker daemon socket which will be mounted to containers")478 execCmd.Flags().BoolVar(&execArg.useGitIgnore, "use-gitignore", true, "Controls whether paths specified in .gitignore should be copied into container")479 execCmd.Flags().StringArrayVarP(&execArg.containerCapAdd, "container-cap-add", "", []string{}, "kernel capabilities to add to the workflow containers (e.g. --container-cap-add SYS_PTRACE)")480 execCmd.Flags().StringArrayVarP(&execArg.containerCapDrop, "container-cap-drop", "", []string{}, "kernel capabilities to remove from the workflow containers (e.g. --container-cap-drop SYS_PTRACE)")481 execCmd.Flags().StringVarP(&execArg.containerOptions, "container-opts", "", "", "container options")482 execCmd.PersistentFlags().StringVarP(&execArg.artifactServerPath, "artifact-server-path", "", ".", "Defines the path where the artifact server stores uploads and retrieves downloads from. If not specified the artifact server will not start.")483 execCmd.PersistentFlags().StringVarP(&execArg.artifactServerAddr, "artifact-server-addr", "", "", "Defines the address where the artifact server listens")484 execCmd.PersistentFlags().StringVarP(&execArg.artifactServerPort, "artifact-server-port", "", "34567", "Defines the port where the artifact server listens (will only bind to localhost).")485 execCmd.PersistentFlags().StringVarP(&execArg.defaultActionsURL, "default-actions-url", "", "https://code.forgejo.org", "Defines the default base url of the action.")486 execCmd.PersistentFlags().BoolVarP(&execArg.noSkipCheckout, "no-skip-checkout", "", false, "Do not skip actions/checkout")487 execCmd.PersistentFlags().BoolVarP(&execArg.debug, "debug", "d", false, "enable debug log")488 execCmd.PersistentFlags().BoolVarP(&execArg.dryrun, "dryrun", "n", false, "dryrun mode")489 execCmd.PersistentFlags().StringVarP(&execArg.image, "image", "i", "node:20-bullseye", "Docker image to use. Use \"-self-hosted\" to run directly on the host.")490 execCmd.PersistentFlags().StringVarP(&execArg.network, "network", "", "", "Specify the network to which the container will connect")491 execCmd.PersistentFlags().BoolVarP(&execArg.enableIPv6, "enable-ipv6", "6", false, "Create network with IPv6 enabled.")492 execCmd.PersistentFlags().StringVarP(&execArg.githubInstance, "gitea-instance", "", "", "Gitea instance to use.")493494 return execCmd495}