<commit_before>package main

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"

	"github.com/fsamin/go-repo"

	"github.com/ovh/cds/cli"
	"github.com/ovh/cds/sdk"
	"github.com/pkg/browser"
)

var workflowRunManualCmd = cli.Command{
	Name:  "run",
	Short: "Run a CDS workflow",
	Ctx: []cli.Arg{
		{Name: _ProjectKey},
		{Name: _WorkflowName},
	},
	Flags: []cli.Flag{
		{
			Name:      "data",
			ShortHand: "d",
			Usage:     "Run the workflow with payload data",
			IsValid: func(s string) bool {
				if strings.TrimSpace(s) == "" {
					return true
				}
				data := map[string]interface{}{}
				return json.Unmarshal([]byte(s), &data) == nil
			},
		},
		{
			Name:      "parameter",
			ShortHand: "p",
			Usage:     "Run the workflow with pipeline parameter",
			IsValid: func(s string) bool {
				if s == "" {
					return true
				}
				// Hacking cobra which split param with a double pipe
				splittedParam := strings.Split(s, "||")
				for _, p := range splittedParam {
					if strings.Count(p, "=") < 1 {
						return false
					}
				}
				return true
			},
			Type: cli.FlagSlice,
		},
		{
			Name:  "run-number",
			Usage: "Existing Workflow RUN Number",
			IsValid: func(s string) bool {
				match, _ := regexp.MatchString(`[0-9]?`, s)
				return match
			},
		},
		{
			Name:  "node-name",
			Usage: "Node Name to relaunch; Flag run-number is mandatory",
		},
		{
			Name:      "interactive",
			ShortHand: "i",
			Usage:     "Follow the workflow run in an interactive terminal user interface",
			Type:      cli.FlagBool,
		},
		{
			Name:      "open-web-browser",
			ShortHand: "o",
			Usage:     "Open web browser on the workflow run",
			Type:      cli.FlagBool,
		},
		{
			Name:      "sync",
			ShortHand: "s",
			Usage:     "Synchronise your pipelines with your last editions. Must be used with flag run-number",
			Type:      cli.FlagBool,
		},
	},
}

func workflowRunManualRun(v cli.Values) error {
	if v.GetBool("sync") && v.GetString("run-number") == "" {
		return fmt.Errorf("could not use flag --sync without flag --run-number")
	}

	manual := sdk.WorkflowNodeRunManual{}
	if strings.TrimSpace(v.GetString("data")) != "" {
		data := map[string]interface{}{}
		if err := json.Unmarshal([]byte(v.GetString("data")), &data); err != nil {
			return fmt.Errorf("error payload isn't a valid json")
		}
		manual.Payload = data
	} else {
		dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
		if err != nil {
			return fmt.Errorf("Unable to get current path: %s", err)
		}
		var gitBranch, currentBranch, remoteURL string
		ctx := context.Background()
		r, err := repo.New(ctx, dir)
		if err == nil { // If the directory is a git repository
			currentBranch, _ = r.CurrentBranch(ctx)
			remoteURL, err = r.FetchURL(ctx)
			if err != nil {
				return sdk.WrapError(err, "cannot fetch the remote url")
			}
		}

		wf, err := client.WorkflowGet(v.GetString(_ProjectKey), v.GetString(_WorkflowName))
		if err != nil {
			return sdk.WrapError(err, "cannot load workflow")
		}

		// Check if we are on the same repository and if we have a git.branch in the default payload
		if wf.WorkflowData.Node.Context != nil && wf.WorkflowData.Node.Context.ApplicationID != 0 {
			app := wf.GetApplication(wf.WorkflowData.Node.Context.ApplicationID)
			if remoteURL != "" && strings.Contains(remoteURL, app.RepositoryFullname) && currentBranch != "" {
				defaultPayload, err := wf.WorkflowData.Node.Context.DefaultPayloadToMap()
				if err == nil && defaultPayload["git.branch"] != "" {
					gitBranch = currentBranch
				}
			}
		}

		if gitBranch != "" {
			m := map[string]string{}
			m["git.branch"] = gitBranch
			manual.Payload = m
		}
	}

	pipParams := v.GetStringSlice("parameter")
	if len(pipParams) > 0 {
		for _, sParam := range pipParams {
			if sParam == "" {
				continue
			}
			splittedParam := strings.SplitN(sParam, "=", 2)
			sdk.AddParameter(&manual.PipelineParameters, splittedParam[0], sdk.StringParameter, splittedParam[1])
		}
	}

	var runNumber, fromNodeID int64

	if v.GetString("run-number") != "" {
		var errp error
		runNumber, errp = strconv.ParseInt(v.GetString("run-number"), 10, 64)
		if errp != nil {
			return fmt.Errorf("run-number invalid: not a integer")
		}
	}

	if v.GetBool("sync") {
		if _, err := client.WorkflowRunResync(v.GetString(_ProjectKey), v.GetString(_WorkflowName), runNumber); err != nil {
			return fmt.Errorf("Cannot resync your workflow run %d : %v", runNumber, err)
		}
	}

	if v.GetString("node-name") != "" {
		if runNumber <= 0 {
			return fmt.Errorf("You can use flag node-name without flag run-number")
		}
		wr, err := client.WorkflowRunGet(v.GetString(_ProjectKey), v.GetString(_WorkflowName), runNumber)
		if err != nil {
			return err
		}
		for _, wnrs := range wr.WorkflowNodeRuns {
			for _, wnr := range wnrs {
				wn := wr.Workflow.WorkflowData.NodeByID(wnr.WorkflowNodeID)
				if wn.Name == v.GetString("node-name") {
					fromNodeID = wnr.WorkflowNodeID
					break
				}
			}
		}
	}<commit_msg>feat(cdsctl): allow manual run a pipeline not launched (#5552)<commit_after>package main

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"

	"github.com/fsamin/go-repo"

	"github.com/ovh/cds/cli"
	"github.com/ovh/cds/sdk"
	"github.com/pkg/browser"
)

var workflowRunManualCmd = cli.Command{
	Name:  "run",
	Short: "Run a CDS workflow",
	Ctx: []cli.Arg{
		{Name: _ProjectKey},
		{Name: _WorkflowName},
	},
	Flags: []cli.Flag{
		{
			Name:      "data",
			ShortHand: "d",
			Usage:     "Run the workflow with payload data",
			IsValid: func(s string) bool {
				if strings.TrimSpace(s) == "" {
					return true
				}
				data := map[string]interface{}{}
				return json.Unmarshal([]byte(s), &data) == nil
			},
		},
		{
			Name:      "parameter",
			ShortHand: "p",
			Usage:     "Run the workflow with pipeline parameter",
			IsValid: func(s string) bool {
				if s == "" {
					return true
				}
				// Hacking cobra which split param with a double pipe
				splittedParam := strings.Split(s, "||")
				for _, p := range splittedParam {
					if strings.Count(p, "=") < 1 {
						return false
					}
				}
				return true
			},
			Type: cli.FlagSlice,
		},
		{
			Name:  "run-number",
			Usage: "Existing Workflow RUN Number",
			IsValid: func(s string) bool {
				match, _ := regexp.MatchString(`[0-9]?`, s)
				return match
			},
		},
		{
			Name:  "node-name",
			Usage: "Node Name to launch; Flag run-number is mandatory",
		},
		{
			Name:      "interactive",
			ShortHand: "i",
			Usage:     "Follow the workflow run in an interactive terminal user interface",
			Type:      cli.FlagBool,
		},
		{
			Name:      "open-web-browser",
			ShortHand: "o",
			Usage:     "Open web browser on the workflow run",
			Type:      cli.FlagBool,
		},
		{
			Name:      "sync",
			ShortHand: "s",
			Usage:     "Synchronise your pipelines with your last editions. Must be used with flag run-number",
			Type:      cli.FlagBool,
		},
	},
}

func workflowRunManualRun(v cli.Values) error {
	if v.GetBool("sync") && v.GetString("run-number") == "" {
		return fmt.Errorf("could not use flag --sync without flag --run-number")
	}

	manual := sdk.WorkflowNodeRunManual{}
	if strings.TrimSpace(v.GetString("data")) != "" {
		data := map[string]interface{}{}
		if err := json.Unmarshal([]byte(v.GetString("data")), &data); err != nil {
			return fmt.Errorf("error payload isn't a valid json")
		}
		manual.Payload = data
	} else {
		dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
		if err != nil {
			return fmt.Errorf("Unable to get current path: %s", err)
		}
		var gitBranch, currentBranch, remoteURL string
		ctx := context.Background()
		r, err := repo.New(ctx, dir)
		if err == nil { // If the directory is a git repository
			currentBranch, _ = r.CurrentBranch(ctx)
			remoteURL, err = r.FetchURL(ctx)
			if err != nil {
				return sdk.WrapError(err, "cannot fetch the remote url")
			}
		}

		wf, err := client.WorkflowGet(v.GetString(_ProjectKey), v.GetString(_WorkflowName))
		if err != nil {
			return sdk.WrapError(err, "cannot load workflow")
		}

		// Check if we are on the same repository and if we have a git.branch in the default payload
		if wf.WorkflowData.Node.Context != nil && wf.WorkflowData.Node.Context.ApplicationID != 0 {
			app := wf.GetApplication(wf.WorkflowData.Node.Context.ApplicationID)
			if remoteURL != "" && strings.Contains(remoteURL, app.RepositoryFullname) && currentBranch != "" {
				defaultPayload, err := wf.WorkflowData.Node.Context.DefaultPayloadToMap()
				if err == nil && defaultPayload["git.branch"] != "" {
					gitBranch = currentBranch
				}
			}
		}

		if gitBranch != "" {
			m := map[string]string{}
			m["git.branch"] = gitBranch
			manual.Payload = m
		}
	}

	pipParams := v.GetStringSlice("parameter")
	if len(pipParams) > 0 {
		for _, sParam := range pipParams {
			if sParam == "" {
				continue
			}
			splittedParam := strings.SplitN(sParam, "=", 2)
			sdk.AddParameter(&manual.PipelineParameters, splittedParam[0], sdk.StringParameter, splittedParam[1])
		}
	}

	var runNumber, fromNodeID int64

	if v.GetString("run-number") != "" {
		var errp error
		runNumber, errp = strconv.ParseInt(v.GetString("run-number"), 10, 64)
		if errp != nil {
			return fmt.Errorf("run-number invalid: not a integer")
		}
	}

	if v.GetBool("sync") {
		if _, err := client.WorkflowRunResync(v.GetString(_ProjectKey), v.GetString(_WorkflowName), runNumber); err != nil {
			return fmt.Errorf("Cannot resync your workflow run %d : %v", runNumber, err)
		}
	}

	if v.GetString("node-name") != "" {
		if runNumber <= 0 {
			return fmt.Errorf("You can use flag node-name without flag run-number")
		}
		wr, err := client.WorkflowRunGet(v.GetString(_ProjectKey), v.GetString(_WorkflowName), runNumber)
		if err != nil {
			return err
		}
		for _, node := range wr.Workflow.WorkflowData.Array() {
			if node.Name == v.GetString("node-name") {
				fromNodeID = node.ID
				break
			}
		}

		if fromNodeID == 0 {
			return fmt.Errorf("--node-name %v node found", v.GetString("node-name"))
		}
	}
{'fraction_non_alphanumeric': 0.15040650406504066, 'fraction_numerical': 0.003668451318659528, 'mean_word_length': 3.1104319478402607, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2456867', 'n_tokens_mistral': 4062, 'n_tokens_neox': 3504, 'n_words': 869}