Skip to content

feat: add multiple tasks support #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 37 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,16 @@ The `trx.yaml` file inside the project repository defines commands and environme
Example:

```yaml
commands:
- werf converge
- echo "{{ .RepoUrl }} / {{ .RepoTag }} / {{ .RepoCommit }}"
env:
WERF_ENV: "production"
tasks:
- name: deploy
commands:
- werf converge
- echo "{{ .RepoUrl }} / {{ .RepoTag }} / {{ .RepoCommit }}"
- name: export
env:
WERF_SET_GIT_REV: "werf.git_rev={{ .RepoCommit }}"
commands:
- werf export
```

Available template variables:
Expand All @@ -108,21 +113,23 @@ repo:

# Optional, default is `trx.yaml` in the repository.
configFile: "trx.yaml"

# Optional. Commands defined here have a higher priority than those specified in `trx.yaml`.
commands:
- werf converge
- echo "{{ .RepoUrl }} / {{ .RepoTag }} / {{ .RepoCommit }}"

# Optional. Set environment variables here to be used in the commands.
# Environment variables defined here are merged with those in the configFile,
# but have higher priority (values in this section will override those in the configFile).
env:
WERF_ENV: "production"

# Optional. Ensures processing starts from a specific tag and prevents processing older tags (safeguard against freeze attacks).
initialLastProcessedTag: "v0.10.1"

# Optional. Tasks defined here have a higher priority than those specified in `trx.yaml`.
tasks:
- name: deploy
commands:
- werf converge
- echo "{{ .RepoUrl }} / {{ .RepoTag }} / {{ .RepoCommit }}"
env:
WERF_BUILDAH_MODE: auto
- name: export
env:
WERF_SET_GIT_REV: "werf.git_rev={{ .RepoCommit }}"
commands:
- werf export

quorums:
- name: main
minNumberOfKeys: 1
Expand All @@ -138,12 +145,14 @@ quorums:

# Optional. Define actions to be taken at different stages of command execution.
hooks:
env:
MSG: 'Task {{ .FailedTaskName }} failed'
onCommandStarted:
- "echo 'Command started: {{ .RepoTag }} at {{ .RepoCommit }}'"
onCommandSuccess:
- "echo 'Success: {{ .RepoTag }}'"
onCommandFailure:
- "echo 'Failure: {{ .RepoTag }}'"
- "echo $MSG"
onCommandSkipped:
- "echo 'Skipped: {{ .RepoTag }}'"
onQuorumFailure:
Expand Down Expand Up @@ -182,7 +191,16 @@ trx --config trx.yaml -- ls -la
```

To force the execution even if no new version is detected, use the `--force` flag:

```sh
trx --force
```

To run on specific tag use `-r` or `--reference` flags:
```sh
trx --reference v0.0.0
```

To disable quorum checking use `--disable-quorums-check` flag:
```sh
trx --disable-quorums-check
```
17 changes: 13 additions & 4 deletions cmd/trx/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ package main

import (
"log"
"os"

"github.com/spf13/cobra"
)

var (
configPath string
force bool
disableLock bool
configPath string
force bool
disableLock bool
disableQuorumsCheck bool
reference string
task string
)

type runOptions struct {
Expand All @@ -32,12 +36,17 @@ By default, it uses the ./trx.yaml configuration file, but you can specify a dif
}

rootCmd.SilenceUsage = true
rootCmd.SilenceErrors = true
rootCmd.PersistentFlags().StringVar(&configPath, "config", "./trx.yaml", "Path to config file")
rootCmd.Flags().BoolVarP(&force, "force", "f", false, "Force execution if no new version found")
rootCmd.Flags().BoolVarP(&disableLock, "disable-lock", "", false, "Disable execution locking")
rootCmd.Flags().BoolVarP(&disableQuorumsCheck, "disable-quorums-check", "", false, "Run without checking quorums")
rootCmd.Flags().StringVarP(&reference, "reference", "r", "", "Tag to run on (default: latest tag)")
rootCmd.Flags().StringVarP(&task, "task", "t", "", "Name of the task to run. If no name provided use ordinal number e.g. 1,2,3...etc. (default: first task in config)")

if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
log.Println(err)
os.Exit(1)
}
}

Expand Down
175 changes: 82 additions & 93 deletions cmd/trx/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,23 @@ import (
"log"
"os"
"os/signal"
"strings"
"syscall"
"time"

"trx/internal/command"
"trx/internal/config"
"trx/internal/git"
"trx/internal/hooks"
"trx/internal/lock"
"trx/internal/quorum"
"trx/internal/storage"
"trx/internal/tasks"
"trx/internal/templates"
)

type Executor interface {
RunTasks(tasks []tasks.Task) error
}

func run(opts runOptions) error {
log.SetFlags(0)
log.SetOutput(os.Stdout)
Expand All @@ -38,14 +43,7 @@ func run(opts runOptions) error {

cfg, err := config.NewConfig(configPath)
if err != nil {
return fmt.Errorf("config error: %w", err)
}

storage, err := storage.NewStorage(&storage.StorageOpts{
Config: cfg,
})
if err != nil {
return fmt.Errorf("init storage error: %w", err)
return err
}

locker := lock.NewManager(lock.NewLocalLocker(disableLock))
Expand All @@ -56,126 +54,117 @@ func run(opts runOptions) error {
log.Println("Processing without execution lock")
}

gitClient, err := git.NewGitClient(cfg.Repo)
gitClient, err := git.NewGitClient(*cfg.Repo)
if err != nil {
return fmt.Errorf("new git client error: %w", err)
}

gitTargetObject, err := gitClient.GetTargetGitObject()
gitTargetObject, err := gitClient.GetTargetGitObject(reference)
if err != nil {
return fmt.Errorf("get target git object error: %w", err)
}

lastSucceedTag, err := storage.CheckLastSucceedTag()
if err != nil {
return fmt.Errorf("check last published commit error: %w", err)
}
repoTemplatevars := templates.GetRepoTemplateVars(templates.RepoTemplateVarsData{
RepoTag: gitTargetObject.Tag,
RepoUrl: cfg.Repo.Url,
RepoCommit: gitTargetObject.Commit,
})

executor, err := command.NewExecutor(ctx, cfg.Env, generateCmdVars(cfg, gitTargetObject))
hookExecutor, err := hooks.NewHookExecutor(ctx, cfg, hooks.HookExecutorOptions{
TemplateVars: repoTemplatevars,
WorkDir: gitClient.RepoPath,
})
if err != nil {
return fmt.Errorf("command executor error: %w", err)
return fmt.Errorf("hooks executor error: %w", err)
}

isNewVersion, err := git.IsNewerVersion(gitTargetObject.Tag, lastSucceedTag, cfg.Repo.InitialLastProcessedTag)
if err != nil {
return fmt.Errorf("can't check if tag is new: %w", err)
}
if !isNewVersion {
switch force {
case true:
log.Println("No new version, but force flag specified. Proceeding... ")
case false:
if hookErr := executor.RunOnCommandSkippedHook(cfg); hookErr != nil {
log.Println("WARNING onCommandSkipped hook execution error: %w", hookErr)
}
log.Println("No new version. execution will be skipped")
return nil
if !disableQuorumsCheck {
if err := quorum.CheckQuorums(&quorum.CheckQuorumsRequest{
Quorums: cfg.Quorums,
Repo: gitClient.Repo,
Tag: gitTargetObject.Tag,
HookExecutor: hookExecutor,
}); err != nil {
return err
}
}

err = quorum.CheckQuorums(cfg.Quorums, gitClient.Repo, gitTargetObject.Tag)
storage, err := storage.NewStorage(&storage.StorageOpts{
Config: cfg,
})
if err != nil {
var qErr *quorum.Error
if errors.As(err, &qErr) {
executor.Vars["FailedQuorumName"] = qErr.QuorumName
if hookErr := executor.RunOnQuorumFailedHook(cfg); hookErr != nil {
log.Println("WARNING onCommandSkipped hook execution error: %w", hookErr)
}
return fmt.Errorf("quorum error: %w", qErr.Err)
} else {
return fmt.Errorf("quorum error: %w", err)
}
return fmt.Errorf("init storage error: %w", err)
}

cmdsToRun, err := getCmdsToRun(cfg, opts, executor)
taskExecutor, err := getExecutor(ctx, tasks.TaskExecutorOptions{
Storage: storage,
TemplateVars: repoTemplatevars,
WorkDir: gitClient.RepoPath,
})
if err != nil {
return fmt.Errorf("get commands to run error: %w", err)
}

// TODO: think about running this hook concurrently with the command
if hookErr := executor.RunOnCommandStartedHook(cfg); hookErr != nil {
log.Printf("WARNING onCommandStarted hook execution error: %s", hookErr.Error())
return fmt.Errorf("task executor error: %w", err)
}

if err := executor.Exec(cmdsToRun); err != nil {
if hookErr := executor.RunOnCommandFailureHook(cfg); hookErr != nil {
log.Println("WARNING onCommandFailure hook execution error: %w", hookErr)
}
return fmt.Errorf("run command error: %w", err)
tasksToRun, err := tasks.GetTasksToRun(cfg, gitClient.RepoPath, tasks.GetTasksToRunOpts{
CmdFromCli: opts.cmdFromCli,
Forced: force,
TargetTaskName: task,
Version: gitTargetObject.Tag,
})
if err != nil {
return fmt.Errorf("task executor error: %w", err)
}

if err := storage.StoreSucceedTag(gitTargetObject.Tag); err != nil {
return fmt.Errorf("store last successed tag error: %w", err)
// TODO: think about running this hook concurrently with the command
for _, t := range tasksToRun {
hookExecutor.RunOnCommandStartedHook(t.Name)
}

if hookErr := executor.RunOnCommandSuccessHook(cfg); hookErr != nil {
log.Println("WARNING onCommandSuccess hook execution error: %w", hookErr)
if err := taskExecutor.RunTasks(tasksToRun); err != nil {
return handleRunTasksError(err, hookExecutor)
}

log.Println("All done")
hookExecutor.RunOnCommandSuccessHook()
return nil
}

func generateCmdVars(cfg *config.Config, t *git.TargetGitObject) map[string]string {
vars := make(map[string]string)
vars["RepoTag"] = t.Tag
vars["RepoUrl"] = cfg.Repo.Url
vars["RepoCommit"] = t.Commit
return vars
}
func handleRunTasksError(err error, hookExecutor *hooks.HookExecutor) error {
var runErr *tasks.Error
if errors.As(err, &runErr) {
switch {
case errors.Is(runErr.Err, tasks.ErrNoNewVersion):
hookExecutor.RunOnCommandSkippedHook()
log.Printf("task %s skipped: no new version detected\n", runErr.TaskName)
return nil

func mergeEnvs(envs, cfgEnv map[string]string) []string {
for k, v := range cfgEnv {
envs[k] = v
}
newEnv := make([]string, 0, len(envs))
for k, v := range envs {
newEnv = append(newEnv, fmt.Sprintf("%s=%s", k, v))
case errors.Is(runErr.Err, tasks.ErrExcutionFailed):
hookExecutor.RunOnCommandFailureHook(runErr.TaskName)
return fmt.Errorf("task %s failed: %w", runErr.TaskName, runErr.Err)

default:
return fmt.Errorf("task running error: %w", runErr.Err)
}
}
return newEnv
return fmt.Errorf("tasks running error: %w", err)
}

func getCmdsToRun(cfg *config.Config, opts runOptions, executor *command.Executor) ([]string, error) {
var cmdsToRun []string
if len(opts.cmdFromCli) > 0 {
cmdsToRun = []string{strings.Join(opts.cmdFromCli, " ")}
return cmdsToRun, nil
func getExecutor(ctx context.Context, opts tasks.TaskExecutorOptions) (Executor, error) {
commonOpts := tasks.TaskExecutorOptions{
Storage: opts.Storage,
TemplateVars: opts.TemplateVars,
WorkDir: opts.WorkDir,
}

if len(cfg.Commands) > 0 {
cmdsToRun = cfg.Commands
if force {
taskExecutor, err := tasks.NewTaskForceExecutor(ctx, commonOpts)
if err != nil {
return nil, fmt.Errorf("task executor error: %w", err)
}
return taskExecutor, nil
} else {
runCfg, err := config.NewRunnerConfig(command.WorkDir, cfg.Repo.ConfigFile)
taskExecutor, err := tasks.NewTaskExecutor(ctx, commonOpts)
if err != nil {
return nil, fmt.Errorf("config error: %w", err)
return nil, fmt.Errorf("task executor error: %w", err)
}
cmdsToRun = runCfg.Commands
executor.Env = mergeEnvs(cfg.Env, runCfg.Env)
return taskExecutor, nil
}

if len(cmdsToRun) == 0 {
return nil, fmt.Errorf("no commands to run")
}

return cmdsToRun, nil
}
Loading