diff --git a/pkg/gale/env.go b/pkg/gale/env.go index e564c625..a0aadea4 100644 --- a/pkg/gale/env.go +++ b/pkg/gale/env.go @@ -65,5 +65,13 @@ func (e *Env) WithContainerFunc(container *dagger.Container) *dagger.Container { e.artifacts = artifactService // save for later access + // runner context + + rc := model.NewRunnerContextFromEnv() + + for k, v := range rc.ToEnv() { + container = container.WithEnvVariable(k, v) + } + return container } diff --git a/pkg/gale/gale.go b/pkg/gale/gale.go index 92b7a995..68dd1724 100644 --- a/pkg/gale/gale.go +++ b/pkg/gale/gale.go @@ -61,6 +61,8 @@ func NewFromContainer(cfg *config.Config, client *dagger.Client, base *dagger.Co return nil, fmt.Errorf("failed to get initial github context: %w", err) } + gale.github = github + // adds the default modifier functions to the gale instance gale.init() gale.loadCurrentRepository() diff --git a/pkg/gale/job.go b/pkg/gale/job.go index 78f0bf4c..692a0a79 100644 --- a/pkg/gale/job.go +++ b/pkg/gale/job.go @@ -65,11 +65,6 @@ func (j *Job) Load(ctx context.Context, workflow, job string) (*Job, error) { } func (j *Job) WithContainerFunc(container *dagger.Container) *dagger.Container { - data, err := json.Marshal(j.config) - if err != nil { - fail(container, err) - } - // TODO: look better way to generate run id, uuid is unique but not readable runID := uuid.New().String() @@ -77,10 +72,28 @@ func (j *Job) WithContainerFunc(container *dagger.Container) *dagger.Container { // unique path for the job path := filepath.Join(containerRunnerPath, "runs", runID) + // marshal event to json + event, err := json.Marshal(j.config.GithubContext.Event) + if err != nil { + fail(container, err) + } + + // update event path to point to the event.json file + j.config.GithubContext.EventPath = filepath.Join(path, "event.json") + + // marshal config to json + config, err := json.Marshal(j.config) + if err != nil { + fail(container, err) + } + // create initial directory for the job and mount it to the container - dir := j.client.Directory().WithNewFile("config.json", string(data)) + dir := j.client.Directory(). + WithNewFile("config.json", string(config)). + WithNewFile("event.json", string(event)) - container = container.WithMountedDirectory(path, dir) + container = container.WithMountedDirectory(path, dir) // mount job directory to the container + container = container.WithDirectory(j.config.GithubContext.Workspace, j.client.Host().Directory(".")) // mount workspace to the container container = container.WithExec([]string{"ghx", "run", runID}) // load final directory for the job diff --git a/test/main.go b/test/main.go new file mode 100644 index 00000000..55298c0c --- /dev/null +++ b/test/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "context" + "fmt" + "os" + + "dagger.io/dagger" + "github.com/aweris/gale/internal/dagger/images" + "github.com/aweris/gale/pkg/config" + "github.com/aweris/gale/pkg/gale" +) + +func main() { + ctx := context.Background() + + client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout)) + if err != nil { + fmt.Printf("Error connecting to dagger: %v\n", err) + os.Exit(1) + } + defer client.Close() + + container := images.RunnerBase(client) + + g, err := gale.New(&config.Config{}, client) + if err != nil { + fmt.Printf("Error creating gale: %v\n", err) + os.Exit(1) + } + + job, err := g.Job().Load(ctx, "example-golangci-lint", "golangci-lint") + if err != nil { + fmt.Printf("Error loading job: %v\n", err) + os.Exit(1) + } + + env := g.Env() + + _, err = container.With(gale.Load(env)).With(gale.Load(job)).Sync(ctx) + if err != nil { + fmt.Printf("Error syncing container: %v\n", err) + os.Exit(1) + } +} diff --git a/tools/ghx/runner/cmd_executor.go b/tools/ghx/runner/cmd_executor.go new file mode 100644 index 00000000..59d14a14 --- /dev/null +++ b/tools/ghx/runner/cmd_executor.go @@ -0,0 +1,119 @@ +package runner + +import ( + "context" + "fmt" + "github.com/aweris/gale/tools/ghx/actions" + "os" + "os/exec" + "path/filepath" + "strings" +) + +type CmdExecutor struct { + command string // command to execute + args []string // args to pass to the command + env map[string]string // env to pass to the command as environment variables + ac *actions.Context // ac is the action context + ef map[string]*actions.EnvironmentFile // ef is the list of environment files +} + +func NewCmdExecutorFromStepAction(sa *StepAction, entrypoint string) *CmdExecutor { + ce := &CmdExecutor{env: make(map[string]string), ef: make(map[string]*actions.EnvironmentFile)} + + ce.command = "node" + ce.args = append(ce.args, fmt.Sprintf("%s/%s", sa.Action.Path, entrypoint)) + ce.ac = sa.ac + + // env + for k, v := range sa.ac.Github.ToEnv() { + ce.env[k] = v + } + + for k, v := range sa.ac.Env.Env() { + ce.env[k] = v + } + + for k, v := range sa.Step.With { + ce.env[fmt.Sprintf("INPUT_%s", strings.ToUpper(k))] = v + } + + // add default values for inputs that are not defined in the step config + for k, v := range sa.Action.Meta.Inputs { + if _, ok := sa.Step.With[k]; ok { + continue + } + + if v.Default == "" { + continue + } + + ce.env[fmt.Sprintf("INPUT_%s", strings.ToUpper(k))] = v.Default + } + + // TODO add step state to the environment + + return ce +} + +func (c *CmdExecutor) Execute(_ context.Context) error { + + cmd := exec.Command(c.command, c.args...) + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + // load env files before setting the environment variables to the command. This is because the env files + // are loaded from the environment variables and loadEnvFile function will override the environment variable + // with the path of the file + + // TODO: replace this temp dir with a a step specific temp dir under runs/run-id/job-id/step-id + dir := os.TempDir() + + if err := c.loadEnvFile(actions.EnvFileNameGithubEnv, filepath.Join(dir, "env")); err != nil { + return err + } + + if err := c.loadEnvFile(actions.EnvFileNameGithubPath, filepath.Join(dir, "path")); err != nil { + return err + } + + if err := c.loadEnvFile(actions.EnvFileNameGithubStepSummary, filepath.Join(dir, "step_summary")); err != nil { + return err + } + + if err := c.loadEnvFile(actions.EnvFileNameGithubActionOutput, filepath.Join(dir, "github_action_output")); err != nil { + return err + } + + env := os.Environ() + + for k, v := range c.env { + // convert value to Evaluable String type + str := actions.NewString(v) + + // evaluate the expression + res, err := str.Eval(c.ac) + if err != nil { + return fmt.Errorf("failed to evaluate default value for input %s: %v", k, err) + } + + env = append(env, fmt.Sprintf("%s=%s", k, res)) + } + + cmd.Env = env + + return cmd.Run() +} + +func (c *CmdExecutor) loadEnvFile(env, path string) error { + ef, err := actions.NewEnvironmentFile(path) + if err != nil { + return err + } + + c.ef[env] = ef // add file to a map to process after the command execution + c.env[env] = ef.Path // set the environment variable to the path of the file + + return nil +} diff --git a/tools/ghx/runner/runner_step.go b/tools/ghx/runner/runner_step.go index 5e0e43c3..13ff36f7 100644 --- a/tools/ghx/runner/runner_step.go +++ b/tools/ghx/runner/runner_step.go @@ -101,6 +101,9 @@ func (s *StepAction) mainCondition() TaskConditionalFn { func (s *StepAction) main() TaskExecutorFn { return func(ctx context.Context) (model.Conclusion, error) { + + NewCmdExecutorFromStepAction(s, s.Action.Meta.Runs.Main).Execute(ctx) + return model.ConclusionFailure, fmt.Errorf("main run is not implemented yet") } }