Repository: ovh/cds
Pull request #5256 (2020-06-15 16:49:55)

fix(api): update permission for workflow when exists in upsert
1. Description
1. Related issues
1. About tests
1. Mentions

@ovh/cds


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=VULNERABILITY) (and [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=SECURITY_HOTSPOT) to review)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/100.png' alt='100.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5256&metric=new_coverage&view=list) [100.0% Coverage](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5256&metric=new_coverage&view=list)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5256&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5256&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: SonarCloud Quality Gate failed.

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/C.png' alt='C' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=BUG) [2 Bugs](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=VULNERABILITY) (and [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=SECURITY_HOTSPOT) [2 Security Hotspots](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=SECURITY_HOTSPOT) to review)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=CODE_SMELL) [4 Code Smells](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/0.png' alt='11.7%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5256&metric=new_coverage&view=list) [11.7% Coverage](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5256&metric=new_coverage&view=list)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5256&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5256&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=VULNERABILITY) (and [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=SECURITY_HOTSPOT) to review)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=ovh_cds&pullRequest=5256&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60.png' alt='70.4%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5256&metric=new_coverage&view=list) [70.4% Coverage](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5256&metric=new_coverage&view=list)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5256&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=ovh_cds&pullRequest=5256&metric=new_duplicated_lines_density&view=list)


ovh-cds: 
CDS Report build-all-cds#13611.0 ✘  
* Build
  * Build and Package All ✔ 
  * Build Debpacker ✔ 
  * Build OS-Ansible-Inventory ✔ 
  * Build SMTPMock ✔ 
  * Lint UI Translate ✔ 
  * Test CLI ✔ 
  * Test Engine ✔ 
  * Test SDK ✔ 
  * Test SDK Rust ✔ 
  * Test UI ✔ 
* Package and Analyze
  * Docker ✔ 
  * Docs ✔ 
  * Sonar ✔ 
* Integration
  * Docker Compose Tests ✔ 
  * Standalone Tests ✘  
  * Standalone tests 1 -   
  * Standalone tests 2 -   
  * Standalone tests 3 -   


Patch:
diff --git a/cli/cdsctl/worker_model.go b/cli/cdsctl/worker_model.go
index d5f4cfefd..fdc10dfeb 100644
--- a/cli/cdsctl/worker_model.go
+++ b/cli/cdsctl/worker_model.go
@@ -44,6 +44,53 @@ var workerModelListCmd = cli.Command{
 	},
 }
 
+type workerModelDisplay struct {
+	Name             string `json:"name" cli:"name,key"`
+	Type             string `json:"type" cli:"type"`
+	Disabled         bool   `json:"disabled" cli:"disabled"`
+	Restricted       bool   `json:"restricted" cli:"restricted"`
+	NeedRegistration bool   `json:"need_registration" cli:"need_registration"`
+	NbSpawnErr       int64  `json:"nb_spawn_err" cli:"nb_spawn_err"`
+	IsDeprecated     bool   `json:"is_deprecated" cli:"deprecated"`
+	IsOfficial       bool   `json:"is_official" cli:"official"`
+	Image            string `json:"image" cli:"image"`
+	Flavor           string `json:"flavor" cli:"flavor"`
+}
+
+func newWorkerModelDisplay(wm sdk.Model) workerModelDisplay {
+	name := wm.Name
+	if wm.Group != nil {
+		name = fmt.Sprintf("%s/%s", wm.Group.Name, wm.Name)
+	} else {
+		name = wm.Name
+	}
+
+	var image, flavor string
+
+	switch wm.Type {
+	case sdk.Docker:
+		image = wm.ModelDocker.Image
+	case sdk.Openstack, sdk.VSphere:
+		image = wm.ModelVirtualMachine.Image
+		flavor = wm.ModelVirtualMachine.Flavor
+	}
+
+	w := workerModelDisplay{
+		Name:             name,
+		Type:             wm.Type,
+		Disabled:         wm.Disabled,
+		Restricted:       wm.Restricted,
+		NeedRegistration: wm.NeedRegistration,
+		NbSpawnErr:       wm.NbSpawnErr,
+		IsDeprecated:     wm.IsDeprecated,
+		Image:            image,
+		Flavor:           flavor,
+		IsOfficial:       wm.IsOfficial,
+	}
+
+	return w
+}
+
 func workerModelListRun(v cli.Values) (cli.ListResult, error) {
 	var err error
 	var workerModels []sdk.Model
@@ -63,7 +110,13 @@ func workerModelListRun(v cli.Values) (cli.ListResult, error) {
 	if err != nil {
 		return nil, err
 	}
-	return cli.AsListResult(workerModels), nil
+
+	wms := make([]workerModelDisplay, len(workerModels))
+	for i := range workerModels {
+		wms[i] = newWorkerModelDisplay(workerModels[i])
+	}
+
+	return cli.AsListResult(wms), nil
 }
 
 var workerModelImportCmd = cli.Command{
diff --git a/cli/cdsctl/workflow_artifact.go b/cli/cdsctl/workflow_artifact.go
index 94b271513..d5a5acf27 100644
--- a/cli/cdsctl/workflow_artifact.go
+++ b/cli/cdsctl/workflow_artifact.go
@@ -59,7 +59,7 @@ var workflowArtifactDownloadCmd = cli.Command{
 		{Name: "number"},
 	},
 	OptionalArgs: []cli.Arg{
-		{Name: "artefact-name"},
+		{Name: "artifact-name"},
 	},
 	Flags: []cli.Flag{
 		{
@@ -92,7 +92,7 @@ func workflowArtifactDownloadRun(v cli.Values) error {
 
 	var ok bool
 	for _, a := range artifacts {
-		if v.GetString("artefact-name") != "" && v.GetString("artefact-name") != a.Name {
+		if v.GetString("artifact-name") != "" && v.GetString("artifact-name") != a.Name {
 			continue
 		}
 		if v.GetString("exclude") != "" && reg.MatchString(a.Name) {
diff --git a/cli/cdsctl/workflow_init.go b/cli/cdsctl/workflow_init.go
index 5ecef73cb..419ee91e0 100644
--- a/cli/cdsctl/workflow_init.go
+++ b/cli/cdsctl/workflow_init.go
@@ -197,6 +197,30 @@ func searchRepository(pkey, repoManagerName, repoFullname string) (string, error
 	return "", fmt.Errorf("unable to find repository %s from %s: please check your credentials", repoFullname, repoManagerName)
 }
 
+// Check for given repo URL that it exists in project vcs.
+// This func will resync the repositories list if not found at first time.
+func checkRepositoryExists(proj sdk.Project, repoURL string) error {
+	for _, vcs := range proj.VCSServers {
+		var resync bool
+		for {
+			repos, err := client.RepositoriesList(proj.Key, vcs.Name, resync)
+			if err != nil {
+				return fmt.Errorf("unable to list repositories from %s: %v", vcs.Name, err)
+			}
+			for _, r := range repos {
+				if repoURL == r.HTTPCloneURL || repoURL == r.SSHCloneURL {
+					return nil
+				}
+			}
+			if resync {
+				break
+			}
+			resync = true
+		}
+	}
+	return fmt.Errorf("unable to find repository %s for project: please check project vcs configuration", repoURL)
+}
+
 func interactiveChoosePipeline(pkey, defaultPipeline string) (string, *sdk.Pipeline, error) {
 	// Try to find pipeline or create a new pipeline
 	pips, err := client.PipelineList(pkey)
@@ -427,9 +451,9 @@ func craftPipelineFile(proj *sdk.Project, existingPip *sdk.Pipeline, pipName, de
 
 func workflowInitRun(c cli.Values) error {
 	path := "."
-	gitRepo, errRepo := repo.New(path)
-	if errRepo != nil {
-		return errRepo
+	gitRepo, err := repo.New(path)
+	if err != nil {
+		return err
 	}
 
 	pkey, err := interactiveChooseProject(gitRepo, c.GetString(_ProjectKey))
@@ -476,7 +500,7 @@ func workflowInitRun(c cli.Values) error {
 
 	if len(files) > 0 {
 		if c.GetString("pipeline") != "" {
-			return errors.New("you can't set a pipeline name while files alerady exists in .cds/ folder")
+			return errors.New("you can't set a pipeline name while files already exists in .cds/ folder")
 		}
 	}
 
@@ -536,6 +560,10 @@ func workflowInitRun(c cli.Values) error {
 			files = append(files, pipFilePath)
 		}
 	} else {
+		// Check that current repository is accessible for CDS, also resync repositories
+		if err := checkRepositoryExists(*proj, fetchURL); err != nil {
+			return err
+		}
 		fmt.Println("Reading files:")
 		for _, f := range files {
 			fmt.Printf(" * %s", cli.Magenta(f))
diff --git a/cli/cdsctl/workflow_transform_as_code.go b/cli/cdsctl/workflow_transform_as_code.go
index c197fbcb9..4df1f757a 100644
--- a/cli/cdsctl/workflow_transform_as_code.go
+++ b/cli/cdsctl/workflow_transform_as_code.go
@@ -81,7 +81,7 @@ func workflowTransformAsCodeRun(v cli.Values) (interface{}, error) {
 	}
 	switch ope.Status {
 	case sdk.OperationStatusError:
-		return nil, fmt.Errorf("cannot perform operation: %s", ope.Error)
+		return nil, fmt.Errorf("cannot perform operation: %v", ope.Error)
 	}
 	return response, nil
 }
diff --git a/contrib/README.md b/contrib/README.md
index 232424477..1d6526450 100644
--- a/contrib/README.md
+++ b/contrib/README.md
@@ -17,7 +17,7 @@ See [User Actions Documentation](https://ovh.github.io/cds/docs/actions)
 
 ## Worker Models
 
-See [Worker Models Documentation](https://ovh.github.io/cds/docs/concepts/requirement/worker-model/)
+See [Worker Models Documentation](https://ovh.github.io/cds/docs/concepts/worker-model/)
 
 ## Workflow Templates
 
diff --git a/docs/content/hosting/ready-to-run/from-binaries.md b/docs/content/hosting/ready-to-run/from-binaries.md
index d0e7de78a..a9e71b3b2 100644
--- a/docs/content/hosting/ready-to-run/from-binaries.md
+++ b/docs/content/hosting/ready-to-run/from-binaries.md
@@ -51,7 +51,11 @@ can use the `./cds-engine config edit` command.
 Example:
 
 ```bash
-./engine config edit conf.toml api.directories.download=./binaries --output conf.toml
+mkdir -p $HOME/cds/download $HOME/cds/download $HOME/cds/hatchery-basedir
+./cds-engine config edit conf.toml --output conf.toml \
+  api.artifact.local.baseDirectory=$HOME/cds/artifacts \
+  api.directories.download=$HOME/cds/download \
+  hatchery.local.basedir=$HOME/cds/hatchery-basedir
 ```
 
 ## Prepare Cache
@@ -105,18 +109,19 @@ curl http://localhost:8081/mon/status
 
 ## Launch Signup & CDS UI
 
-Signup with cdsctl:
+Signup with cdsctl. `INIT_TOKEN` is used to validate the user as an administrator.
 
 ```bash
 export INIT_TOKEN=`./cds-engine config init-token --config conf.toml`
 ./cdsctl signup --api-url http://localhost:8081 --email admin@localhost.local --username admin --fullname admin
 ```
 
-If you don't have email service configured you just have to check your CDS API logs to have the confirmation link.
+If you don't have email service configured you just have to check your CDS API logs to get the `cdsctl signup verify...` command to run.
 
 
 ```bash
 cd $HOME/cds
+./cdsctl signup verify --api-url ... # Get this command from the API Logs
 ./cds-engine download ui --config conf.toml
 ./cds-engine start ui --config conf.toml
 ```
@@ -136,10 +141,10 @@ Start the local hatchery:
 
 ## Note about CDS Engine
 
-It is possible to start all services as a single process `$ ./engine start api ui hooks hatchery:local --config config.toml`.
+It is possible to start all services as a single process `$ ./cds-engine start api ui hooks hatchery:local --config config.toml`.
 
 ```bash
-$ ./engine start api hooks hatchery:local --config config.toml
+$ ./cds-engine start api hooks hatchery:local --config config.toml
 Reading configuration file config.toml
 Starting service api
 ...
@@ -157,27 +162,27 @@ For serious deployment, we strongly suggest to run each service as a dedicated p
 
 ```bash
 
-$ ./engine start api --config config.toml
+$ ./cds-engine start api --config config.toml
 
-$ ./engine start ui --config config.toml
+$ ./cds-engine start ui --config config.toml
 
-$ ./engine start hooks --config config.toml
+$ ./cds-engine start hooks --config config.toml
 
-$ ./engine start vcs --config config.toml
+$ ./cds-engine start vcs --config config.toml
 
-$ ./engine start hatchery:local --config config.toml
-$ ./engine start hatchery:docker --config config.toml
-$ ./engine start hatchery:swarm --config config.toml
-$ ./engine start hatchery:marathon --config config.toml
-$ ./engine start hatchery:openstack --config config.toml
-$ ./engine start hatchery:vsphere --config config.toml
+$ ./cds-engine start hatchery:local --config config.toml
+$ ./cds-engine start hatchery:docker --config config.toml
+$ ./cds-engine start hatchery:swarm --config config.toml
+$ ./cds-engine start hatchery:marathon --config config.toml
+$ ./cds-engine start hatchery:openstack --config config.toml
+$ ./cds-engine start hatchery:vsphere --config config.toml
 
 ```
 
 You can scale as you want each of this component, you probably will have to create a configuration for each instance of each service expect the API.
 
 ```bash
-$ ./engine config new > config.api.toml # All API instance can share the same configuration.
+$ ./cds-engine config new > config.api.toml # All API instance can share the same configuration.
 
 $ cp config.api.toml config.hatchery.swarm-1.toml
 $ cp config.api.toml config.hatchery.swarm-2.toml
diff --git a/engine/api/api.go b/engine/api/api.go
index 94bb1a9dd..7b8bebbc8 100644
--- a/engine/api/api.go
+++ b/engine/api/api.go
@@ -81,6 +81,10 @@ type Configuration struct {
 	Directories struct {
 		Download string `toml:"download" default:"/var/lib/cds-engine" json:"download"`
 	} `toml:"directories" json:"directories"`
+	InternalServiceMesh struct {
+		RequestSecondsTimeout int  `toml:"requestSecondsTimeout" json:"requestSecondsTimeout" default:"60"`
+		InsecureSkipVerifyTLS bool `toml:"insecureSkipVerifyTLS" json:"insecureSkipVerifyTLS" default:"false"`
+	} `toml:"internalServiceMesh" json:"internalServiceMesh"`
 	Auth struct {
 		DefaultGroup  string `toml:"defaultGroup" default:"" comment:"The default group is the group in which every new user will be granted at signup" json:"defaultGroup"`
 		RSAPrivateKey string `toml:"rsaPrivateKey" default:"" comment:"The RSA Private Key used to sign and verify the JWT Tokens issued by the API \nThis is mandatory." json:"-"`
@@ -404,6 +408,15 @@ func (a *API) Serve(ctx context.Context) error {
 		return sdk.WrapError(err, "unable to initialize the JWT Layer")
 	}
 
+	// Intialize service mesh httpclient
+	if a.Config.InternalServiceMesh.RequestSecondsTimeout == 0 {
+		a.Config.InternalServiceMesh.RequestSecondsTimeout = 60
+	}
+	services.HTTPClient = cdsclient.NewHTTPClient(
+		time.Duration(a.Config.InternalServiceMesh.RequestSecondsTimeout)*time.Second,
+		a.Config.InternalServiceMesh.InsecureSkipVerifyTLS,
+	)
+
 	// Initialize mail package
 	log.Info(ctx, "Initializing mail driver...")
 	mail.Init(a.Config.SMTP.User,
@@ -649,7 +662,7 @@ func (a *API) Serve(ctx context.Context) error {
 		a.serviceAPIHeartbeat(ctx)
 	}, a.PanicDump())
 	sdk.GoRoutine(ctx, "authentication.SessionCleaner", func(ctx context.Context) {
-		authentication.SessionCleaner(ctx, a.mustDB)
+		authentication.SessionCleaner(ctx, a.mustDB, 10*time.Second)
 	}, a.PanicDump())
 
 	isFreshInstall, errF := version.IsFreshInstall(a.mustDB())
diff --git a/engine/api/api_routes.go b/engine/api/api_routes.go
index 5c9882416..b5b67f9dd 100644
--- a/engine/api/api_routes.go
+++ b/engine/api/api_routes.go
@@ -258,12 +258,12 @@ func (api *API) InitRouter() {
 	// Workflows run
 	r.Handle("/project/{permProjectKey}/runs", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getWorkflowAllRunsHandler, EnableTracing()))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/artifact/{artifactId}", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getDownloadArtifactHandler))
-	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowRunsHandler, EnableTracing()), r.POSTEXECUTE(api.postWorkflowRunHandler /*, AllowServices(true)*/, EnableTracing()))
-	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/branch/{branch}", Scope(sdk.AuthConsumerScopeRun), r.DELETE(api.deleteWorkflowRunsBranchHandler /*, NeedService()*/))
+	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowRunsHandler, EnableTracing()), r.POSTEXECUTE(api.postWorkflowRunHandler, EnableTracing()))
+	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/branch/{branch}", Scope(sdk.AuthConsumerScopeRun), r.DELETE(api.deleteWorkflowRunsBranchHandler))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/latest", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getLatestWorkflowRunHandler))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/tags", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowRunTagsHandler))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/num", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowRunNumHandler), r.POST(api.postWorkflowRunNumHandler))
-	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/{number}", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowRunHandler /*, AllowServices(true)*/, EnableTracing()), r.DELETE(api.deleteWorkflowRunHandler))
+	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/{number}", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowRunHandler, EnableTracing()), r.DELETE(api.deleteWorkflowRunHandler))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/{number}/stop", Scope(sdk.AuthConsumerScopeRun), r.POSTEXECUTE(api.stopWorkflowRunHandler, EnableTracing(), MaintenanceAware()))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/{number}/vcs/resync", Scope(sdk.AuthConsumerScopeRun), r.POSTEXECUTE(api.postResyncVCSWorkflowRunHandler))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/{number}/artifacts", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowRunArtifactsHandler))
@@ -278,8 +278,8 @@ func (api *API) InitRouter() {
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/hook/triggers/condition", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowTriggerHookConditionHandler))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/triggers/condition", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowTriggerConditionHandler))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/{number}/nodes/{nodeRunID}/release", Scope(sdk.AuthConsumerScopeRun), r.POST(api.releaseApplicationWorkflowHandler, MaintenanceAware()))
-	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/{number}/hooks/{hookRunID}/callback", Scope(sdk.AuthConsumerScopeRun), r.POST(api.postWorkflowJobHookCallbackHandler, MaintenanceAware() /*, AllowServices(true)*/))
-	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/{number}/hooks/{hookRunID}/details", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowJobHookDetailsHandler /*, NeedService()*/))
+	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/{number}/hooks/{hookRunID}/callback", Scope(sdk.AuthConsumerScopeRun), r.POST(api.postWorkflowJobHookCallbackHandler, MaintenanceAware()))
+	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/{number}/hooks/{hookRunID}/details", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowJobHookDetailsHandler))
 
 	// Environment
 	r.Handle("/project/{permProjectKey}/environment", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getEnvironmentsHandler), r.POST(api.addEnvironmentHandler))
diff --git a/engine/api/ascode/pull_request.go b/engine/api/ascode/pull_request.go
index 86770ba94..4c5decc84 100644
--- a/engine/api/ascode/pull_request.go
+++ b/engine/api/ascode/pull_request.go
@@ -54,12 +54,12 @@ forLoop:
 		case <-tick.C:
 			ope, err := operation.GetRepositoryOperation(ctx, db, ed.OperationUUID)
 			if err != nil {
-				globalErr = sdk.NewErrorFrom(err, "unable to get repository operation %s", ed.OperationUUID)
+				globalErr = sdk.WrapError(err, "unable to get repository operation %s", ed.OperationUUID)
 				break forLoop
 			}
-
 			if ope.Status == sdk.OperationStatusError {
-				globalErr = sdk.NewErrorFrom(sdk.ErrUnknownError, "repository operation in error: %s", ope.Error)
+				globalOperation.Error = ope.Error
+				globalErr = ope.Error.ToError()
 				break forLoop
 			}
 			if ope.Status == sdk.OperationStatusDone {
@@ -76,7 +76,6 @@ forLoop:
 		}
 	}
 	if globalErr != nil {
-		httpErr := sdk.ExtractHTTPError(globalErr, "")
 		isErrWithStack := sdk.IsErrorWithStack(globalErr)
 		fields := logrus.Fields{}
 		if isErrWithStack {
@@ -85,7 +84,9 @@ forLoop:
 		log.ErrorWithFields(ctx, fields, "%s", globalErr)
 
 		globalOperation.Status = sdk.OperationStatusError
-		globalOperation.Error = httpErr.Error()
+		if globalOperation.Error == nil {
+			globalOperation.Error = sdk.ToOperationError(globalErr)
+		}
 	}
 
 	_ = store.SetWithTTL(cache.Key(operation.CacheOperationKey, globalOperation.UUID), globalOperation, 300)
diff --git a/engine/api/authentication/session.go b/engine/api/authentication/session.go
index 57fe0ec2c..bb4863383 100644
--- a/engine/api/authentication/session.go
+++ b/engine/api/authentication/session.go
@@ -70,10 +70,10 @@ func CheckSessionJWT(jwtToken string) (*jwt.Token, error) {
 }
 
 // SessionCleaner must be run as a goroutine
-func SessionCleaner(ctx context.Context, dbFunc func() *gorp.DbMap) {
+func SessionCleaner(ctx context.Context, dbFunc func() *gorp.DbMap, tickerDuration time.Duration) {
 	log.Info(ctx, "Initializing session cleaner...")
 	db := dbFunc()
-	tick := time.NewTicker(10 * time.Second)
+	tick := time.NewTicker(tickerDuration)
 	tickCorruped := time.NewTicker(12 * time.Hour)
 	defer tick.Stop()
 	defer tickCorruped.Stop()
diff --git a/engine/api/group/workflow_group.go b/engine/api/group/workflow_group.go
index 5838507f5..d43e43f8a 100644
--- a/engine/api/group/workflow_group.go
+++ b/engine/api/group/workflow_group.go
@@ -83,6 +83,7 @@ func UpdateWorkflowGroup(ctx context.Context, db gorp.SqlExecutor, w *sdk.Workfl
 	if !ok {
 		return sdk.WithStack(sdk.ErrLastGroupWithWriteRole)
 	}
+
 	return nil
 }
 
@@ -96,11 +97,12 @@ func UpsertAllWorkflowGroups(db gorp.SqlExecutor, w *sdk.Workflow, gps []sdk.Gro
 
 	ok, err := checkAtLeastOneGroupWithWriteRoleOnWorkflow(db, w.ID)
 	if err != nil {
-		return sdk.WrapError(err, "U")
+		return err
 	}
 	if !ok {
-		return sdk.WrapError(sdk.ErrLastGroupWithWriteRole, "U")
+		return sdk.WithStack(sdk.ErrLastGroupWithWriteRole)
 	}
+
 	return nil
 }
 
@@ -111,7 +113,7 @@ func UpsertWorkflowGroup(db gorp.SqlExecutor, projectID, workflowID int64, gp sd
 				(SELECT id FROM project_group WHERE project_group.project_id = $1 AND project_group.group_id = $2),
 				$3,
 				$4
-			) ON CONFLICT DO NOTHING`
+			) ON CONFLICT (project_group_id, workflow_id) DO UPDATE SET role = $4`
 	if _, err := db.Exec(query, projectID, gp.Group.ID, workflowID, gp.Permission); err != nil {
 		if strings.Contains(err.Error(), `null value in column "project_group_id"`) {
 			return sdk.WrapError(sdk.ErrNotFound, "cannot add this group on workflow because there isn't in the project groups : %v", err)
@@ -123,9 +125,11 @@ func UpsertWorkflowGroup(db gorp.SqlExecutor, projectID, workflowID int64, gp sd
 
 // DeleteWorkflowGroup remove group permission on the given workflow
 func DeleteWorkflowGroup(db gorp.SqlExecutor, w *sdk.Workflow, groupID int64, index int) error {
-	query := `DELETE FROM workflow_perm
+	query := `
+    DELETE FROM workflow_perm
 		USING project_group
-	WHERE workflow_perm.project_group_id = project_group.id AND workflow_perm.workflow_id = $1 AND project_group.group_id = $2`
+    WHERE workflow_perm.project_group_id = project_group.id AND workflow_perm.workflow_id = $1 AND project_group.group_id = $2
+  `
 	if _, err := db.Exec(query, w.ID, groupID); err != nil {
 		return sdk.WithStack(err)
 	}
@@ -141,6 +145,18 @@ func DeleteWorkflowGroup(db gorp.SqlExecutor, w *sdk.Workflow, groupID int64, in
 	return nil
 }
 
+// DeleteAllWorkflowGroups removes all group permission for the given workflow.
+func DeleteAllWorkflowGroups(db gorp.SqlExecutor, workflowID int64) error {
+	query := `
+    DELETE FROM workflow_perm
+    WHERE workflow_id = $1
+  `
+	if _, err := db.Exec(query, workflowID); err != nil {
+		return sdk.WrapError(err, "unable to remove group permissions for workflow %d", workflowID)
+	}
+	return nil
+}
+
 func checkAtLeastOneGroupWithWriteRoleOnWorkflow(db gorp.SqlExecutor, wID int64) (bool, error) {
 	query := `select count(project_group_id) from workflow_perm where workflow_id = $1 and role = $2`
 	nb, err := db.SelectInt(query, wID, 7)
diff --git a/engine/api/observability/init.go b/engine/api/observability/init.go
index a79de4bb9..cd08649c5 100644
--- a/engine/api/observability/init.go
+++ b/engine/api/observability/init.go
@@ -38,6 +38,7 @@ func StatsHTTPExporter() *HTTPExporter {
 
 // Init the opencensus exporter
 func Init(ctx context.Context, cfg Configuration, s service) (context.Context, error) {
+	log.Info(ctx, "observability> initializing observability for %s/%s", s.Type(), s.Name())
 	ctx = ContextWithTag(ctx,
 		TagServiceType, s.Type(),
 		TagServiceName, s.Name(),
diff --git a/engine/api/permission.go b/engine/api/permission.go
deleted file mode 100644
index eebd3c888..000000000
--- a/engine/api/permission.go
+++ /dev/null
@@ -1,50 +0,0 @@
-package api
-
-/*
-func (api *API) checkWorkerPermission(ctx context.Context, db gorp.SqlExecutor, rc *service.HandlerConfig, routeVar map[string]string) bool {
-	if getWorker(ctx) == nil {
-		log.Error(ctx, "checkWorkerPermission> no worker in ctx")
-		return false
-	}
-
-	idS, ok := routeVar["permJobID"]
-	if !ok {
-		return true
-	}
-
-	id, err := strconv.ParseInt(idS, 10, 64)
-	if err != nil {
-		log.Error(ctx, "checkWorkerPermission> Unable to parse permJobID:%s err:%v", idS, err)
-		return false
-	}
-
-	//IF it is POSTEXECUTE, it means that the job is must be taken by the worker
-	if rc.Options["isExecution"] == "true" {
-		k := cache.Key("workers", getWorker(ctx).ID, "perm", idS)
-		find, err := api.Cache.Get(k, &ok)
-		if err != nil {
-			log.Error(ctx, "cannot get from cache %s: %v", k, err)
-		}
-		if find {
-			if ok {
-				return ok
-			}
-		}
-
-		runNodeJob, err := workflow.LoadNodeJobRun(db, api.Cache, id)
-		if err != nil {
-			log.Error(ctx, "checkWorkerPermission> Unable to load job %d err:%v", id, err)
-			return false
-		}
-
-		ok = runNodeJob.ID == getWorker(ctx).ActionBuildID
-		if err := api.Cache.SetWithTTL(k, ok, 60*15); err != nil {
-			log.Error(ctx, "cannot SetWithTTL: %s: %v", k, err)
-		}
-		if !ok {
-			log.Error(ctx, "checkWorkerPermission> actionBuildID:%v runNodeJob.ID:%v", getWorker(ctx).ActionBuildID, runNodeJob.ID)
-		}
-		return ok
-	}
-	return true
-} */
diff --git a/engine/api/project_group.go b/engine/api/project_group.go
index 2fe65ffe9..f9336e202 100644
--- a/engine/api/project_group.go
+++ b/engine/api/project_group.go
@@ -136,7 +136,7 @@ func (api *API) putGroupRoleOnProjectHandler() service.Handler {
 		}
 
 		if err := tx.Commit(); err != nil {
-			return sdk.WrapError(err, "updateGroupRoleHandler: Cannot start transaction")
+			return sdk.WrapError(err, "cannot start transaction")
 		}
 
 		newGroupPermission := sdk.GroupPermission{Permission: newLink.Role, Group: *grp}
diff --git a/engine/api/router_middleware_auth.go b/engine/api/router_middleware_auth.go
index 912564319..8592687e2 100644
--- a/engine/api/router_middleware_auth.go
+++ b/engine/api/router_middleware_auth.go
@@ -233,7 +233,8 @@ func (api *API) xsrfMiddleware(ctx context.Context, w http.ResponseWriter, req *
 	// else if its a read request we want to reuse a cached XSRF token or generate one
 	if rc.PermissionLevel > sdk.PermissionRead {
 		if !existXSRFTokenInCache || xsrfToken != existingXSRFToken {
-			return ctx, sdk.WithStack(sdk.ErrUnauthorized)
+			// We want to return a forbidden to allow the user to retry with a new token.
+			return ctx, sdk.WithStack(sdk.ErrForbidden)
 		}
 
 		newXSRFToken, err := authentication.NewSessionXSRFToken(api.Cache, sessionID)
diff --git a/engine/api/services/http.go b/engine/api/services/http.go
index 86d6ccf86..7a9901dce 100644
--- a/engine/api/services/http.go
+++ b/engine/api/services/http.go
@@ -142,6 +142,9 @@ func doJSONRequest(ctx context.Context, db gorp.SqlExecutor, srvs []sdk.Service,
 			}
 			return headers, code, nil
 		}
+		if lastCode < 409 {
+			break
+		}
 	}
 
 	log.Error(ctx, "unable to call service: maximum attempt exceed : %+v", lastErr)
diff --git a/engine/api/status.go b/engine/api/status.go
index 2ff58791c..62e341a43 100644
--- a/engine/api/status.go
+++ b/engine/api/status.go
@@ -220,7 +220,7 @@ func (api *API) initMetrics(ctx context.Context) error {
 		"number of failed workflow runs",
 		stats.UnitDimensionless)
 	api.Metrics.DatabaseConns = stats.Int64(
-		fmt.Sprintf("cds/cds-api/%s/database_conn°", api.Name()),
+		fmt.Sprintf("cds/cds-api/%s/database_conn", api.Name()),
 		"number database connections",
 		stats.UnitDimensionless)
 
@@ -258,6 +258,12 @@ func (api *API) initMetrics(ctx context.Context) error {
 }
 
 func (api *API) computeMetrics(ctx context.Context) {
+	tags := observability.ContextGetTags(ctx, observability.TagServiceType, observability.TagServiceName)
+	ctx, err := tag.New(ctx, tags...)
+	if err != nil {
+		log.Error(ctx, "api.computeMetrics> unable to tag observability context: %v", err)
+	}
+
 	sdk.GoRoutine(ctx, "api.computeMetrics", func(ctx context.Context) {
 		tick := time.NewTicker(9 * time.Second).C
 		for {
diff --git a/engine/api/workflow/dao.go b/engine/api/workflow/dao.go
index cfe6a19c0..a047c3478 100644
--- a/engine/api/workflow/dao.go
+++ b/engine/api/workflow/dao.go
@@ -986,7 +986,7 @@ func checkApplication(db gorp.SqlExecutor, proj sdk.Project, w *sdk.Workflow, n
 		appDB, err := application.LoadByName(db, proj.Key, n.Context.ApplicationName, application.LoadOptions.WithDeploymentStrategies, application.LoadOptions.WithVariables)
 		if err != nil {
 			if sdk.ErrorIs(err, sdk.ErrNotFound) {
-				return sdk.WithData(sdk.ErrNotFound, n.Context.ApplicationName)
+				return sdk.NewErrorFrom(sdk.WithData(sdk.ErrNotFound, n.Context.ApplicationName), "cannot find application with name: %s", n.Context.ApplicationName)
 			}
 			return sdk.WrapError(err, "unable to load application %s", n.Context.ApplicationName)
 		}
diff --git a/engine/api/workflow/dao_run.go b/engine/api/workflow/dao_run.go
index 3c5160d73..60dbb6149 100644
--- a/engine/api/workflow/dao_run.go
+++ b/engine/api/workflow/dao_run.go
@@ -494,7 +494,7 @@ func CanBeRun(workflowRun *sdk.WorkflowRun, workflowNodeRun *sdk.WorkflowNodeRun
 	}
 	ancestorsID := node.Ancestors(workflowRun.Workflow.WorkflowData)
 
-	if ancestorsID == nil || len(ancestorsID) == 0 {
+	if len(ancestorsID) == 0 {
 		return true
 	}
 	for _, ancestorID := range ancestorsID {
diff --git a/engine/api/workflow/execute_node_job_run.go b/engine/api/workflow/execute_node_job_run.go
index 875cc1b78..c9e8fbe82 100644
--- a/engine/api/workflow/execute_node_job_run.go
+++ b/engine/api/workflow/execute_node_job_run.go
@@ -581,6 +581,7 @@ func AddLog(db gorp.SqlExecutor, job *sdk.WorkflowNodeJobRun, logs *sdk.Log, max
 
 	// ignore the log if max size already reached
 	if maxReached := truncateLogs(maxLogSize, size, logs); maxReached {
+		log.Debug("truncated logs")
 		return nil
 	}
 
diff --git a/engine/api/workflow/execute_node_run.go b/engine/api/workflow/execute_node_run.go
index 468feb493..b41119595 100644
--- a/engine/api/workflow/execute_node_run.go
+++ b/engine/api/workflow/execute_node_run.go
@@ -956,14 +956,14 @@ func stopWorkflowNodeJobRun(ctx context.Context, dbFunc func() *gorp.DbMap, stor
 		njr, errNRJ := LoadAndLockNodeJobRunWait(ctx, tx, store, njrID)
 		if errNRJ != nil {
 			chanErr <- sdk.WrapError(errNRJ, "StopWorkflowNodeRun> Cannot load node job run id")
-			tx.Rollback()
+			_ = tx.Rollback()
 			wg.Done()
 			return report
 		}
 
 		if err := AddSpawnInfosNodeJobRun(tx, njr.WorkflowNodeRunID, njr.ID, []sdk.SpawnInfo{stopInfos}); err != nil {
 			chanErr <- sdk.WrapError(err, "Cannot save spawn info job %d", njr.ID)
-			tx.Rollback()
+			_ = tx.Rollback()
 			wg.Done()
 			return report
 		}
@@ -973,14 +973,14 @@ func stopWorkflowNodeJobRun(ctx context.Context, dbFunc func() *gorp.DbMap, stor
 		report.Merge(ctx, r)
 		if err != nil {
 			chanErr <- sdk.WrapError(err, "cannot update node job run")
-			tx.Rollback()
+			_ = tx.Rollback()
 			wg.Done()
 			return report
 		}
 
 		if err := tx.Commit(); err != nil {
 			chanErr <- sdk.WithStack(err)
-			tx.Rollback()
+			_ = tx.Rollback()
 			wg.Done()
 			return report
 		}
@@ -1077,7 +1077,7 @@ func getVCSInfos(ctx context.Context, db gorp.SqlExecutor, store cache.Store, pr
 	// Check repository value
 	if vcsInfos.Repository == "" {
 		vcsInfos.Repository = applicationRepositoryFullname
-	} else if strings.ToLower(vcsInfos.Repository) != strings.ToLower(applicationRepositoryFullname) {
+	} else if !strings.EqualFold(vcsInfos.Repository, applicationRepositoryFullname) {
 		//The input repository is not the same as the application, we have to check if it is a fork
 		forks, err := client.ListForks(ctx, applicationRepositoryFullname)
 		if err != nil {
diff --git a/engine/api/workflow/hook.go b/engine/api/workflow/hook.go
index 20be683fe..70c1215bf 100644
--- a/engine/api/workflow/hook.go
+++ b/engine/api/workflow/hook.go
@@ -131,15 +131,22 @@ func hookRegistration(ctx context.Context, db gorp.SqlExecutor, store cache.Stor
 			previousHook, has := oldHooks[h.UUID]
 			// If previous hook is the same, we do nothing
 			if has && h.Equals(*previousHook) {
+				// If this a repowebhook with an empty eventFilter, let's keep the old one because vcs won't be called to get the default eventFilter
+				eventFilter, has := h.GetConfigValue(sdk.HookConfigEventFilter)
+				if previousHook.IsRepositoryWebHook() && h.IsRepositoryWebHook() &&
+					(!has || eventFilter == "") {
+					h.Config[sdk.HookConfigEventFilter] = previousHook.Config[sdk.HookConfigEventFilter]
+				}
 				continue
 			}
+
 		}
 		// initialize a UUID is there no uuid
 		if h.UUID == "" {
 			h.UUID = sdk.UUID()
 		}
 
-		if h.HookModelName == sdk.RepositoryWebHookModelName || h.HookModelName == sdk.GitPollerModelName || h.HookModelName == sdk.GerritHookModelName {
+		if h.IsRepositoryWebHook() || h.HookModelName == sdk.GitPollerModelName || h.HookModelName == sdk.GerritHookModelName {
 			if wf.WorkflowData.Node.Context.ApplicationID == 0 || wf.Applications[wf.WorkflowData.Node.Context.ApplicationID].RepositoryFullname == "" || wf.Applications[wf.WorkflowData.Node.Context.ApplicationID].VCSServer == "" {
 				return sdk.NewErrorFrom(sdk.ErrForbidden, "cannot create a git poller or repository webhook on an application without a repository")
 			}
@@ -157,6 +164,7 @@ func hookRegistration(ctx context.Context, db gorp.SqlExecutor, store cache.Stor
 			return err
 		}
 		hookToUpdate[h.UUID] = *h
+		log.Debug("workflow.hookrRegistration> following hook must be updated: %+v", h)
 	}
 
 	if len(hookToUpdate) > 0 {
@@ -179,7 +187,10 @@ func hookRegistration(ctx context.Context, db gorp.SqlExecutor, store cache.Stor
 				continue
 			}
 			v, ok := h.Config[sdk.HookConfigWebHookID]
-			if h.HookModelName == sdk.RepositoryWebHookModelName && h.Config["vcsServer"].Value != "" {
+			if h.IsRepositoryWebHook() {
+				log.Debug("workflow.hookRegistration> managing vcs configuration: %+v", h)
+			}
+			if h.IsRepositoryWebHook() && h.Config["vcsServer"].Value != "" {
 				if !ok || v.Value == "" {
 					if err := createVCSConfiguration(ctx, db, store, proj, h); err != nil {
 						return sdk.WithStack(err)
diff --git a/engine/api/workflow/process_start.go b/engine/api/workflow/process_start.go
index bd8ce7d35..a6e4bc650 100644
--- a/engine/api/workflow/process_start.go
+++ b/engine/api/workflow/process_start.go
@@ -115,7 +115,6 @@ func processAllJoins(ctx context.Context, db gorp.SqlExecutor, store cache.Store
 		//now checks if all sources have been completed
 		var ok = true
 
-		nodeRunIDs := []int64{}
 		sourcesParams := map[string]string{}
 		for _, nodeRun := range sources {
 			if nodeRun == nil {
@@ -136,7 +135,6 @@ func processAllJoins(ctx context.Context, db gorp.SqlExecutor, store cache.Store
 				}
 			}
 
-			nodeRunIDs = append(nodeRunIDs, nodeRun.ID)
 			//Merge build parameters from all sources
 			sourcesParams = sdk.ParametersMapMerge(sourcesParams, sdk.ParametersToMap(nodeRun.BuildParameters))
 		}
diff --git a/engine/api/workflow/repository.go b/engine/api/workflow/repository.go
index acd7177f1..f739c9c6c 100644
--- a/engine/api/workflow/repository.go
+++ b/engine/api/workflow/repository.go
@@ -189,7 +189,7 @@ func pollRepositoryOperation(c context.Context, db gorp.SqlExecutor, store cache
 				opeTrusted := *ope
 				opeTrusted.RepositoryStrategy.SSHKeyContent = sdk.PasswordPlaceholder
 				opeTrusted.RepositoryStrategy.Password = sdk.PasswordPlaceholder
-				return nil, sdk.WrapError(fmt.Errorf("%s", ope.Error), "operation in error: %+v", opeTrusted)
+				return nil, sdk.WrapError(fmt.Errorf("%+v", ope.Error), "operation in error: %+v", opeTrusted)
 			case sdk.OperationStatusDone:
 				return ope, nil
 			}
diff --git a/engine/api/workflow/resync_workflow.go b/engine/api/workflow/resync_workflow.go
index f80bc728e..6b1b4b30d 100644
--- a/engine/api/workflow/resync_workflow.go
+++ b/engine/api/workflow/resync_workflow.go
@@ -42,7 +42,7 @@ func Resync(ctx context.Context, db gorp.SqlExecutor, store cache.Store, proj sd
 	wr.Workflow.HookModels = wf.HookModels
 	wr.Workflow.OutGoingHookModels = wf.OutGoingHookModels
 
-	return UpdateWorkflowRun(nil, db, wr)
+	return UpdateWorkflowRun(ctx, db, wr)
 }
 
 //ResyncWorkflowRunStatus resync the status of workflow if you stop a node run when workflow run is building
@@ -70,10 +70,11 @@ func ResyncWorkflowRunStatus(ctx context.Context, db gorp.SqlExecutor, wr *sdk.W
 		newStatus = getRunStatus(counterStatus)
 	}
 
+	log.Debug("ResyncWorkflowRunStatus> %s/%s %+v", newStatus, wr.Status, counterStatus)
+
 	if newStatus != wr.Status {
 		wr.Status = newStatus
 		report.Add(ctx, *wr)
-
 		return report, UpdateWorkflowRunStatus(db, wr)
 	}
 
diff --git a/engine/api/workflow/workflow_importer.go b/engine/api/workflow/workflow_importer.go
index 20e32f95e..2cde7b37a 100644
--- a/engine/api/workflow/workflow_importer.go
+++ b/engine/api/workflow/workflow_importer.go
@@ -92,15 +92,18 @@ func Import(ctx context.Context, db gorp.SqlExecutor, store cache.Store, proj sd
 
 func importWorkflowGroups(db gorp.SqlExecutor, w *sdk.Workflow) error {
 	if len(w.Groups) > 0 {
+		if err := group.DeleteAllWorkflowGroups(db, w.ID); err != nil {
+			return err
+		}
 		for i := range w.Groups {
 			g, err := group.LoadByName(context.Background(), db, w.Groups[i].Group.Name)
 			if err != nil {
-				return sdk.WrapError(err, "Unable to load group %s", w.Groups[i].Group.Name)
+				return sdk.WrapError(err, "unable to load group %s", w.Groups[i].Group.Name)
 			}
 			w.Groups[i].Group = *g
 		}
 		if err := group.UpsertAllWorkflowGroups(db, w, w.Groups); err != nil {
-			return sdk.WrapError(err, "Unable to update workflow")
+			return sdk.WrapError(err, "unable to update workflow")
 		}
 	}
 	return nil
diff --git a/engine/api/workflow/workflow_parser.go b/engine/api/workflow/workflow_parser.go
index 82f6c1d20..a063ad973 100644
--- a/engine/api/workflow/workflow_parser.go
+++ b/engine/api/workflow/workflow_parser.go
@@ -99,7 +99,7 @@ func ParseAndImport(ctx context.Context, db gorp.SqlExecutor, store cache.Store,
 		if oldW != nil {
 			for i := range oldW.WorkflowData.Node.Hooks {
 				h := &oldW.WorkflowData.Node.Hooks[i]
-				if h.HookModelName == sdk.RepositoryWebHookModel.Name {
+				if h.IsRepositoryWebHook() {
 					oldRepoWebHook = h
 					break
 				}
@@ -111,24 +111,36 @@ func ParseAndImport(ctx context.Context, db gorp.SqlExecutor, store cache.Store,
 				// Get current webhook
 				for i := range w.WorkflowData.Node.Hooks {
 					h := &w.WorkflowData.Node.Hooks[i]
-					if h.HookModelName == sdk.RepositoryWebHookModel.Name {
+					if h.IsRepositoryWebHook() {
 						h.UUID = oldRepoWebHook.UUID
-						h.Config = oldRepoWebHook.Config.Clone()
-						h.Config[sdk.HookConfigWorkflow] = sdk.WorkflowNodeHookConfigValue{Value: w.Name}
+						h.Config.MergeWith(
+							oldRepoWebHook.Config.Filter(
+								func(k string, v sdk.WorkflowNodeHookConfigValue) bool {
+									return !v.Configurable
+								},
+							),
+						)
+						// get only non cofigurable stuff
 						currentRepoWebHook = h
+						log.Debug("workflow.ParseAndImport> keeping the old repository web hook: %+v (%+v)", h, oldRepoWebHook)
 						break
 					}
 				}
 
-				// If not found
+				// If not found, take the default config
 				if currentRepoWebHook == nil {
 					h := sdk.NodeHook{
 						UUID:          oldRepoWebHook.UUID,
 						HookModelName: oldRepoWebHook.HookModelName,
-						Config:        oldRepoWebHook.Config.Clone(),
-						HookModelID:   oldRepoWebHook.HookModelID,
+						Config:        sdk.RepositoryWebHookModel.DefaultConfig.Clone(),
+						HookModelID:   sdk.RepositoryWebHookModel.ID,
+					}
+					oldNonConfigurableConfig := oldRepoWebHook.Config.Filter(func(k string, v sdk.WorkflowNodeHookConfigValue) bool {
+						return !v.Configurable
+					})
+					for k, v := range oldNonConfigurableConfig {
+						h.Config[k] = v
 					}
-					h.Config[sdk.HookConfigWorkflow] = sdk.WorkflowNodeHookConfigValue{Value: w.Name}
 					w.WorkflowData.Node.Hooks = append(w.WorkflowData.Node.Hooks, h)
 				}
 			}
@@ -144,13 +156,18 @@ func ParseAndImport(ctx context.Context, db gorp.SqlExecutor, store cache.Store,
 				Config:        sdk.RepositoryWebHookModel.DefaultConfig.Clone(),
 			}
 
-			// If the new workflow already contains a repowebhook (comparing refs), we dont have to add a new one
+			// If the new workflow already contains a repowebhook, we dont have to add a new one
 			var hasARepoWebHook bool
 			for _, h := range w.WorkflowData.Node.Hooks {
 				if h.Ref() == newRepoWebHook.Ref() {
 					hasARepoWebHook = true
 					break
 				}
+				if h.HookModelName == newRepoWebHook.HookModelName &&
+					h.ConfigValueContainsEventsDefault() {
+					hasARepoWebHook = true
+					break
+				}
 			}
 			if !hasARepoWebHook {
 				w.WorkflowData.Node.Hooks = append(w.WorkflowData.Node.Hooks, newRepoWebHook)
diff --git a/engine/api/workflow_group.go b/engine/api/workflow_group.go
index efe303c82..189e6be26 100644
--- a/engine/api/workflow_group.go
+++ b/engine/api/workflow_group.go
@@ -44,14 +44,13 @@ func (api *API) deleteWorkflowGroupHandler() service.Handler {
 				break
 			}
 		}
-
 		if oldGp.Permission == 0 {
 			return sdk.WithStack(sdk.ErrNotFound)
 		}
 
-		tx, errT := api.mustDB().Begin()
-		if errT != nil {
-			return sdk.WrapError(errT, "cannot start transaction")
+		tx, err := api.mustDB().Begin()
+		if err != nil {
+			return sdk.WrapError(err, "cannot start transaction")
 		}
 		defer tx.Rollback() // nolint
 
diff --git a/engine/api/workflow_queue.go b/engine/api/workflow_queue.go
index 7bbd7150c..642f388f5 100644
--- a/engine/api/workflow_queue.go
+++ b/engine/api/workflow_queue.go
@@ -567,8 +567,6 @@ func (api *API) postWorkflowJobLogsHandler() service.Handler {
 			return err
 		}
 
-		log.Debug("postWorkflowJobLogsHandler> Logs: %+v", logs)
-
 		if err := workflow.AddLog(api.mustDB(), pbJob, &logs, api.Config.Log.StepMaxSize); err != nil {
 			return err
 		}
diff --git a/engine/api/workflow_run.go b/engine/api/workflow_run.go
index 6f94cc0af..d134a9a2c 100644
--- a/engine/api/workflow_run.go
+++ b/engine/api/workflow_run.go
@@ -697,12 +697,20 @@ func (api *API) stopWorkflowNodeRunHandler() service.Handler {
 			return sdk.WrapError(err, "unable to stop workflow node run")
 		}
 
+		// Reload workflow run then resync its status
 		tx, err := api.mustDB().Begin()
 		if err != nil {
 			return sdk.WithStack(err)
 		}
 		defer tx.Rollback() // nolint
 
+		workflowRun, err = workflow.LoadRun(ctx, tx, p.Key, workflowName, workflowRunNumber, workflow.LoadRunOptions{
+			WithDeleted: true,
+		})
+		if err != nil {
+			return sdk.WrapError(err, "unable to load workflow run with number %d for workflow %s", workflowRunNumber, workflowName)
+		}
+
 		r1, err := workflow.ResyncWorkflowRunStatus(ctx, tx, workflowRun)
 		report.Merge(ctx, r1)
 		if err != nil {
@@ -1014,7 +1022,6 @@ func (api *API) initWorkflowRun(ctx context.Context, projKey string, wf *sdk.Wor
 		report.Merge(ctx, r)
 		return
 	}
-
 	workflow.ResyncNodeRunsWithCommits(ctx, api.mustDB(), api.Cache, *p, report)
 
 	// Purge workflow run
@@ -1023,6 +1030,16 @@ func (api *API) initWorkflowRun(ctx context.Context, projKey string, wf *sdk.Wor
 			log.Error(ctx, "workflow.PurgeWorkflowRun> error %v", err)
 		}
 	}, api.PanicDump())
+
+	// Update parent
+	for i := range report.WorkflowRuns() {
+		run := &report.WorkflowRuns()[i]
+		reportParent, err := updateParentWorkflowRun(ctx, api.mustDB, api.Cache, run)
+		if err != nil {
+			log.Error(ctx, "unable to update parent workflow run: %v", err)
+		}
+		go WorkflowSendEvent(context.Background(), api.mustDB(), api.Cache, *p, reportParent)
+	}
 }
 
 func failInitWorkflowRun(ctx context.Context, db *gorp.DbMap, wfRun *sdk.WorkflowRun, err error) *workflow.ProcessorReport {
@@ -1333,8 +1350,6 @@ func (api *API) getWorkflowNodeRunJobStepHandler() service.Handler {
 			StepLogs: *ls,
 		}
 
-		log.Debug("logs: %+v", result)
-
 		return service.WriteJSON(w, result, http.StatusOK)
 	}
 }
diff --git a/engine/api/workflowtemplate/instance.go b/engine/api/workflowtemplate/instance.go
index 228453c84..e7ec57f0a 100644
--- a/engine/api/workflowtemplate/instance.go
+++ b/engine/api/workflowtemplate/instance.go
@@ -149,7 +149,7 @@ func CheckAndExecuteTemplate(ctx context.Context, db *gorp.DbMap, consumer sdk.A
 		return allMsgs, nil, sdk.NewErrorFrom(sdk.ErrWrongRequest, "could not find given workflow template")
 	}
 
-	wt, err := LoadBySlugAndGroupID(ctx, db, templateSlug, grp.ID, LoadOptions.Default)
+	wt, err := LoadBySlugAndGroupID(ctx, db, templateSlug, grp.ID)
 	if err != nil {
 		return allMsgs, nil, sdk.NewErrorFrom(err, "could not find a template with slug %s in group %s", templateSlug, grp.Name)
 	}
@@ -160,7 +160,9 @@ func CheckAndExecuteTemplate(ctx context.Context, db *gorp.DbMap, consumer sdk.A
 		}
 		wt = &wta.DataAfter
 	}
-
+	if err := LoadOptions.Default(ctx, db, wt); err != nil {
+		return allMsgs, nil, err
+	}
 	allMsgs = append(allMsgs, sdk.NewMessage(sdk.MsgWorkflowGeneratedFromTemplateVersion, wt.PathWithVersion()))
 
 	req := sdk.WorkflowTemplateRequest{
diff --git a/engine/cdn/cdn.go b/engine/cdn/cdn.go
index 236b5108e..22eec4c7a 100644
--- a/engine/cdn/cdn.go
+++ b/engine/cdn/cdn.go
@@ -88,11 +88,9 @@ func (s *Service) Serve(c context.Context) error {
 
 	//Gracefully shutdown the http server
 	go func() {
-		select {
-		case <-ctx.Done():
-			log.Info(ctx, "CDN> Shutdown HTTP Server")
-			_ = server.Shutdown(ctx)
-		}
+		<-ctx.Done()
+		log.Info(ctx, "CDN> Shutdown HTTP Server")
+		_ = server.Shutdown(ctx)
 	}()
 
 	//Start the http server
diff --git a/engine/cdn/cdn_log.go b/engine/cdn/cdn_log.go
index 2be656b23..d079f627d 100644
--- a/engine/cdn/cdn_log.go
+++ b/engine/cdn/cdn_log.go
@@ -9,6 +9,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/go-gorp/gorp"
 	gocache "github.com/patrickmn/go-cache"
 
 	"github.com/ovh/cds/engine/api/observability"
@@ -34,11 +35,9 @@ func (s *Service) RunTcpLogServer(ctx context.Context) {
 
 	//Gracefully shutdown the tcp server
 	go func() {
-		select {
-		case <-ctx.Done():
-			log.Info(ctx, "CDN> Shutdown tcp log Server")
-			_ = listener.Close()
-		}
+		<-ctx.Done()
+		log.Info(ctx, "CDN> Shutdown tcp log Server")
+		_ = listener.Close()
 	}()
 
 	go func() {
@@ -57,10 +56,24 @@ func (s *Service) RunTcpLogServer(ctx context.Context) {
 	}()
 }
 
+func (s *Service) handleConnectionChannel(ctx context.Context) chan<- handledMessage {
+	chanMessages := make(chan handledMessage, 1000)
+	sdk.GoRoutine(context.Background(), "cdn-msgreader-"+sdk.UUID(), func(ctx context.Context) {
+		if err := s.processLogs(ctx, chanMessages); err != nil {
+			log.Error(ctx, "error while processing logs: %v", err)
+		}
+	})
+	return chanMessages
+}
+
 func (s *Service) handleConnection(ctx context.Context, conn net.Conn) {
 	defer func() {
 		_ = conn.Close()
 	}()
+
+	chanMessages := s.handleConnectionChannel(ctx)
+	defer close(chanMessages)
+
 	bufReader := bufio.NewReader(conn)
 	for {
 		bytes, err := bufReader.ReadBytes(byte(0))
@@ -71,7 +84,7 @@ func (s *Service) handleConnection(ctx context.Context, conn net.Conn) {
 		// remove byte(0)
 		bytes = bytes[:len(bytes)-1]
 
-		if err := s.handleLogMessage(ctx, bytes); err != nil {
+		if err := s.handleLogMessage(ctx, chanMessages, bytes); err != nil {
 			observability.Record(ctx, Errors, 1)
 			log.Error(ctx, "cdn.log> %v", err)
 			continue
@@ -79,7 +92,7 @@ func (s *Service) handleConnection(ctx context.Context, conn net.Conn) {
 	}
 }
 
-func (s *Service) handleLogMessage(ctx context.Context, messageReceived []byte) error {
+func (s *Service) handleLogMessage(ctx context.Context, chanMessages chan<- handledMessage, messageReceived []byte) error {
 	m := hook.Message{}
 	if err := m.UnmarshalJSON(messageReceived); err != nil {
 		return sdk.WrapError(err, "unable to unmarshall gelf message: %s", string(messageReceived))
@@ -99,7 +112,7 @@ func (s *Service) handleLogMessage(ctx context.Context, messageReceived []byte)
 	switch {
 	case signature.Worker != nil:
 		observability.Record(ctx, WorkerLogReceived, 1)
-		return s.handleWorkerLog(ctx, signature.Worker.WorkerID, sig, m)
+		return s.handleWorkerLog(ctx, chanMessages, signature.Worker.WorkerID, sig, m)
 	case signature.Service != nil:
 		observability.Record(ctx, ServiceLogReceived, 1)
 		return s.handleServiceLog(ctx, signature.Service.HatcheryID, signature.Service.HatcheryName, signature.Service.WorkerName, sig, m)
@@ -108,7 +121,7 @@ func (s *Service) handleLogMessage(ctx context.Context, messageReceived []byte)
 	}
 }
 
-func (s *Service) handleWorkerLog(ctx context.Context, workerID string, sig interface{}, m hook.Message) error {
+func (s *Service) handleWorkerLog(ctx context.Context, chanMessages chan<- handledMessage, workerID string, sig interface{}, m hook.Message) error {
 	var signature log.Signature
 	var workerData sdk.Worker
 	cacheData, ok := logCache.Get(fmt.Sprintf("worker-%s", workerID))
@@ -128,16 +141,72 @@ func (s *Service) handleWorkerLog(ctx context.Context, workerID string, sig inte
 		return sdk.WithStack(sdk.ErrForbidden)
 	}
 
-	pbJob, err := workflow.LoadNodeJobRun(ctx, s.Db, s.Cache, signature.JobID)
-	if err != nil {
-		return err
+	chanMessages <- handledMessage{
+		signature: signature,
+		m:         m,
+	}
+
+	return nil
+}
+
+type handledMessage struct {
+	signature log.Signature
+	m         hook.Message
+}
+
+func (s *Service) processLogs(ctx context.Context, chanMessages <-chan handledMessage) error {
+	var t0 = time.Now()
+	var nbMessages int
+	defer func() {
+		delta := time.Since(t0).Seconds()
+		log.Info(ctx, "processLogs - %d messages received in %.3f seconds", nbMessages, delta)
+	}()
+	for {
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+
+		case msg, more := <-chanMessages:
+			nbMessages++
+			if msg.signature.Worker == nil {
+				if !more {
+					return nil
+				}
+				continue
+			}
+
+			tx, err := s.Db.Begin()
+			if err != nil {
+				log.Error(ctx, "unable to start tx: %v", err)
+				continue
+			}
+
+			currentLog := buildMessage(msg.signature, msg.m)
+			if err := s.processLog(ctx, tx, msg.signature, currentLog); err != nil {
+				log.Error(ctx, "unable to process log: %+v", err)
+				tx.Rollback() // nolint
+				continue
+			}
+
+			if err := tx.Commit(); err != nil {
+				log.Error(ctx, "unable to commit tx: %+v", err)
+				tx.Rollback() // nolint
+				continue
+			}
+
+			if !more {
+				return nil
+			}
+		}
 	}
+}
 
+func buildMessage(signature log.Signature, m hook.Message) string {
 	logDate := time.Unix(0, int64(m.Time*1e9))
 	logs := sdk.Log{
-		JobID:        pbJob.ID,
+		JobID:        signature.JobID,
 		LastModified: &logDate,
-		NodeRunID:    pbJob.WorkflowNodeRunID,
+		NodeRunID:    signature.NodeRunID,
 		Start:        &logDate,
 		StepOrder:    signature.Worker.StepOrder,
 		Val:          m.Full,
@@ -165,16 +234,19 @@ func (s *Service) handleWorkerLog(ctx context.Context, workerID string, sig inte
 		lvl = "EMERGENCY"
 	}
 	logs.Val = fmt.Sprintf("[%s] %s", lvl, logs.Val)
-	tx, err := s.Db.Begin()
-	if err != nil {
-		return sdk.WithStack(err)
-	}
-	defer tx.Rollback() // nolint
+	return logs.Val
+}
 
-	if err := workflow.AddLog(tx, pbJob, &logs, s.Cfg.Log.StepMaxSize); err != nil {
-		return err
+func (s *Service) processLog(ctx context.Context, db gorp.SqlExecutor, signature log.Signature, message string) error {
+	now := time.Now()
+	l := sdk.Log{
+		JobID:        signature.JobID,
+		NodeRunID:    signature.NodeRunID,
+		LastModified: &now,
+		StepOrder:    signature.Worker.StepOrder,
+		Val:          message,
 	}
-	return sdk.WithStack(tx.Commit())
+	return workflow.AddLog(db, nil, &l, s.Cfg.Log.StepMaxSize)
 }
 
 func (s *Service) handleServiceLog(ctx context.Context, hatcheryID int64, hatcheryName string, workerName string, sig interface{}, m hook.Message) error {
diff --git a/engine/hatchery/serve.go b/engine/hatchery/serve.go
index 68d9be05d..1f9871815 100644
--- a/engine/hatchery/serve.go
+++ b/engine/hatchery/serve.go
@@ -23,6 +23,7 @@ import (
 	"github.com/ovh/cds/sdk/hatchery"
 	"github.com/ovh/cds/sdk/jws"
 	"github.com/ovh/cds/sdk/log"
+	"github.com/ovh/cds/sdk/log/hook"
 )
 
 type Common struct {
@@ -193,7 +194,12 @@ func (c *Common) getPanicDumpListHandler() service.Handler {
 func (c *Common) InitServiceLogger() error {
 	var signer jose.Signer
 	if c.Common.ServiceInstance.LogServerAdress != "" {
-		logger, _, err := log.New(context.Background(), c.Common.ServiceInstance.LogServerAdress)
+		var graylogCfg = &hook.Config{
+			Addr:     c.Common.ServiceInstance.LogServerAdress,
+			Protocol: "tcp",
+		}
+		logger, _, err := log.New(context.Background(), graylogCfg)
+
 		if err != nil {
 			return sdk.WithStack(err)
 		}
@@ -218,6 +224,7 @@ func (c *Common) SendServiceLog(ctx context.Context, servicesLogs []sdk.ServiceL
 				WorkerName:      s.WorkerName,
 			},
 			JobID:     s.WorkflowNodeJobRunID,
+			NodeRunID: s.WorkflowNodeRunID,
 			Timestamp: time.Now().UnixNano(),
 		}
 		signature, err := jws.Sign(c.Signer, dataToSign)
diff --git a/engine/hatchery/vsphere/init.go b/engine/hatchery/vsphere/init.go
index 90f99e0b4..33df197c3 100644
--- a/engine/hatchery/vsphere/init.go
+++ b/engine/hatchery/vsphere/init.go
@@ -13,25 +13,27 @@ import (
 
 // InitHatchery create new client for vsphere
 func (h *HatcheryVSphere) InitHatchery(ctx context.Context) error {
+	h.user = h.Config.VSphereUser
+	h.password = h.Config.VSpherePassword
+	h.endpoint = h.Config.VSphereEndpoint
+
 	// Connect and login to ESX or vCenter
-	c, errNc := h.newClient(ctx)
-	if errNc != nil {
-		return fmt.Errorf("Unable to vsphere.newClient: %s", errNc)
+	c, err := h.newClient(ctx)
+	if err != nil {
+		return fmt.Errorf("Unable to vsphere.newClient: %v", err)
 	}
 	h.vclient = c
 
 	finder := find.NewFinder(h.vclient.Client, false)
 	h.finder = finder
 
-	var errDc error
-	if h.datacenter, errDc = finder.DatacenterOrDefault(ctx, h.datacenterString); errDc != nil {
-		return fmt.Errorf("Unable to find datacenter %s : %s", h.datacenterString, errDc)
+	if h.datacenter, err = finder.DatacenterOrDefault(ctx, h.datacenterString); err != nil {
+		return fmt.Errorf("Unable to find datacenter %s: %v", h.datacenterString, err)
 	}
 	finder.SetDatacenter(h.datacenter)
 
-	var errN error
-	if h.network, errN = finder.NetworkOrDefault(ctx, h.networkString); errN != nil {
-		return fmt.Errorf("Unable to find network %s : %s", h.networkString, errN)
+	if h.network, err = finder.NetworkOrDefault(ctx, h.networkString); err != nil {
+		return fmt.Errorf("Unable to find network %s: %v", h.networkString, err)
 	}
 
 	go h.main()
diff --git a/engine/hooks/webhook.go b/engine/hooks/webhook.go
index 2974e4467..a045c1c7f 100644
--- a/engine/hooks/webhook.go
+++ b/engine/hooks/webhook.go
@@ -139,15 +139,15 @@ func executeWebHook(t *sdk.TaskExecution) (*sdk.WorkflowNodeRunHookEvent, error)
 		}
 		//Parse the content type
 		ct, _, _ = mime.ParseMediaType(ct)
-		switch {
-		case ct == "application/x-www-form-urlencoded":
+		switch ct {
+		case "application/x-www-form-urlencoded":
 			formValues, err := url.ParseQuery(string(t.WebHook.RequestBody))
-			if err == nil {
+			if err != nil {
 				return nil, sdk.WrapError(err, "Unable webhook to parse body %s", t.WebHook.RequestBody)
 			}
 			copyValues(values, formValues)
 			h.Payload["payload"] = string(t.WebHook.RequestBody)
-		case ct == "application/json":
+		case "application/json":
 			var bodyJSON interface{}
 
 			//Try to parse the body as an array
diff --git a/engine/repositories/processor.go b/engine/repositories/processor.go
index ef0a0f0c6..b84b38810 100644
--- a/engine/repositories/processor.go
+++ b/engine/repositories/processor.go
@@ -20,10 +20,17 @@ func (s *Service) processor(ctx context.Context) error {
 		}
 		if uuid != "" {
 			op := s.dao.loadOperation(ctx, uuid)
+			ctx = context.WithValue(ctx, log.ContextLoggingRequestIDKey, op.RequestID)
 			if err := s.do(ctx, *op); err != nil {
 				if err == errLockUnavailable {
-					log.Info(ctx, "repositories > processor > lock unavailable. Retry")
-					s.dao.pushOperation(op)
+					sdk.GoRoutine(ctx, "operation "+uuid+" retry", func(ctx context.Context) {
+						op.NbRetries++
+						log.Info(ctx, "repositories > processor > lock unavailable. retry")
+						time.Sleep(time.Duration(2*op.NbRetries) * time.Second)
+						if err := s.dao.pushOperation(op); err != nil {
+							log.Error(ctx, "repositories > processor > %v", err)
+						}
+					})
 				} else {
 					log.Error(ctx, "repositories > processor > %v", err)
 				}
@@ -55,10 +62,10 @@ func (s *Service) do(ctx context.Context, op sdk.Operation) error {
 			}
 			log.ErrorWithFields(ctx, fields, "%s", err)
 
-			op.Error = sdk.ExtractHTTPError(err, "").Error()
+			op.Error = sdk.ToOperationError(err)
 			op.Status = sdk.OperationStatusError
 		} else {
-			op.Error = ""
+			op.Error = nil
 			op.Status = sdk.OperationStatusDone
 			switch {
 			case op.LoadFiles.Pattern != "":
@@ -70,14 +77,14 @@ func (s *Service) do(ctx context.Context, op sdk.Operation) error {
 					}
 					log.ErrorWithFields(ctx, fields, "%s", err)
 
-					op.Error = sdk.ExtractHTTPError(err, "").Error()
+					op.Error = sdk.ToOperationError(err)
 					op.Status = sdk.OperationStatusError
 				} else {
-					op.Error = ""
+					op.Error = nil
 					op.Status = sdk.OperationStatusDone
 				}
 			default:
-				op.Error = "unrecognized operation"
+				op.Error = sdk.ToOperationError(sdk.NewErrorFrom(sdk.ErrUnknownError, "unrecognized operation"))
 				op.Status = sdk.OperationStatusError
 			}
 		}
@@ -91,14 +98,14 @@ func (s *Service) do(ctx context.Context, op sdk.Operation) error {
 			}
 			log.ErrorWithFields(ctx, fields, "%s", err)
 
-			op.Error = sdk.ExtractHTTPError(err, "").Error()
+			op.Error = sdk.ToOperationError(err)
 			op.Status = sdk.OperationStatusError
 		} else {
-			op.Error = ""
+			op.Error = nil
 			op.Status = sdk.OperationStatusDone
 		}
 	default:
-		op.Error = "unrecognized setup"
+		op.Error = sdk.ToOperationError(sdk.NewErrorFrom(sdk.ErrUnknownError, "unrecognized setup"))
 		op.Status = sdk.OperationStatusError
 	}
 
diff --git a/engine/repositories/processor_push.go b/engine/repositories/processor_push.go
index ecdd19e22..e14d23642 100644
--- a/engine/repositories/processor_push.go
+++ b/engine/repositories/processor_push.go
@@ -101,8 +101,7 @@ func (s *Service) processPush(ctx context.Context, op *sdk.Operation) error {
 
 	// In case that there are no changes (ex: push changes on an existing branch that was not merged)
 	if !gitRepo.ExistsDiff() {
-		log.Debug("processPush> no files changes")
-		return nil
+		return sdk.WrapError(sdk.ErrNothingToPush, "processPush> %s : no files changes", op.UUID)
 	}
 
 	// Commit files
@@ -119,6 +118,6 @@ func (s *Service) processPush(ctx context.Context, op *sdk.Operation) error {
 		return sdk.WrapError(err, "push %s", op.Setup.Push.FromBranch)
 	}
 
-	log.Debug("processPush> files pushed")
+	log.Debug("processPush> %s : files pushed", op.UUID)
 	return nil
 }
diff --git a/engine/repositories/repositories_handlers.go b/engine/repositories/repositories_handlers.go
index 609c7283f..482c41fee 100644
--- a/engine/repositories/repositories_handlers.go
+++ b/engine/repositories/repositories_handlers.go
@@ -14,6 +14,7 @@ import (
 
 	"github.com/ovh/cds/engine/service"
 	"github.com/ovh/cds/sdk"
+	"github.com/ovh/cds/sdk/log"
 )
 
 func muxVar(r *http.Request, s string) string {
@@ -38,6 +39,10 @@ func (s *Service) postOperationHandler() service.Handler {
 			}
 		}
 
+		requestID := ctx.Value(log.ContextLoggingRequestIDKey)
+		log.Info(ctx, "setting request_id:%s on operation:%s", requestID, op.UUID)
+		op.RequestID, _ = requestID.(string)
+
 		uuid := sdk.UUID()
 		op.UUID = uuid
 		now := time.Now()
@@ -111,6 +116,16 @@ func (s *Service) getOperationsHandler() service.Handler {
 		op := s.dao.loadOperation(ctx, uuid)
 		op.RepositoryStrategy.SSHKeyContent = sdk.PasswordPlaceholder
 		op.RepositoryStrategy.Password = sdk.PasswordPlaceholder
+
+		// Handle old representation of operation error
+		if op.DeprecatedError != "" && op.Error == nil {
+			op.Error = &sdk.OperationError{
+				ID:      sdk.ErrUnknownError.ID,
+				Message: op.DeprecatedError,
+				Status:  sdk.ErrUnknownError.Status,
+			}
+		}
+
 		return service.WriteJSON(w, op, http.StatusOK)
 	}
 }
diff --git a/engine/sql/208_worker_model.sql b/engine/sql/208_worker_model.sql
new file mode 100644
index 000000000..eba0fb0a0
--- /dev/null
+++ b/engine/sql/208_worker_model.sql
@@ -0,0 +1,7 @@
+-- +migrate Up
+
+ALTER TABLE "worker_model" DROP COLUMN IF EXISTS image;
+
+-- +migrate Down
+
+ALTER TABLE "worker_model" ADD COLUMN IF NOT EXISTS image TEXT;
diff --git a/engine/vcs/bitbucketcloud/client_hook.go b/engine/vcs/bitbucketcloud/client_hook.go
index 15c119296..72f9f6ba0 100644
--- a/engine/vcs/bitbucketcloud/client_hook.go
+++ b/engine/vcs/bitbucketcloud/client_hook.go
@@ -24,7 +24,7 @@ func (client *bitbucketcloudClient) CreateHook(ctx context.Context, repo string,
 	}
 
 	if len(hook.Events) == 0 {
-		hook.Events = []string{"repo:push"}
+		hook.Events = sdk.BitbucketCloudEventsDefault
 	}
 	r := WebhookCreate{
 		Description: "CDS webhook - " + hook.Name,
@@ -140,7 +140,7 @@ func (client *bitbucketcloudClient) UpdateHook(ctx context.Context, repo string,
 	}
 
 	if len(hook.Events) == 0 {
-		hook.Events = []string{"repo:push"}
+		hook.Events = sdk.BitbucketCloudEventsDefault
 	}
 
 	bitbucketHook.Events = hook.Events
diff --git a/engine/vcs/bitbucketserver/client_hook.go b/engine/vcs/bitbucketserver/client_hook.go
index 9f62db9d7..dff551b12 100644
--- a/engine/vcs/bitbucketserver/client_hook.go
+++ b/engine/vcs/bitbucketserver/client_hook.go
@@ -83,7 +83,7 @@ func (b *bitbucketClient) CreateHook(ctx context.Context, repo string, hook *sdk
 	}
 
 	if len(hook.Events) == 0 {
-		hook.Events = []string{"repo:refs_changed"}
+		hook.Events = sdk.BitbucketEventsDefault
 	}
 
 	url := fmt.Sprintf("/projects/%s/repos/%s/webhooks", project, slug)
@@ -119,7 +119,7 @@ func (b *bitbucketClient) UpdateHook(ctx context.Context, repo string, hook *sdk
 	}
 
 	if len(hook.Events) == 0 {
-		hook.Events = []string{"repo:refs_changed"}
+		hook.Events = sdk.BitbucketEventsDefault
 	}
 
 	bitbucketHook.Events = hook.Events
diff --git a/engine/vcs/gerrit/client_hook.go b/engine/vcs/gerrit/client_hook.go
index fcaf769f8..6365403f6 100644
--- a/engine/vcs/gerrit/client_hook.go
+++ b/engine/vcs/gerrit/client_hook.go
@@ -14,7 +14,7 @@ func (c *gerritClient) GetHook(ctx context.Context, repo, id string) (sdk.VCSHoo
 //CreateHook enables the default HTTP POST Hook in Gerrit
 func (c *gerritClient) CreateHook(ctx context.Context, repo string, hook *sdk.VCSHook) error {
 	if len(hook.Events) == 0 {
-		hook.Events = []string{"patchset-created"}
+		hook.Events = sdk.GerritEventsDefault
 	}
 	return nil
 }
@@ -22,7 +22,7 @@ func (c *gerritClient) CreateHook(ctx context.Context, repo string, hook *sdk.VC
 //UpdateHook enables the default HTTP POST Hook in Gerrit
 func (c *gerritClient) UpdateHook(ctx context.Context, repo string, hook *sdk.VCSHook) error {
 	if len(hook.Events) == 0 {
-		hook.Events = []string{"patchset-created"}
+		hook.Events = sdk.GerritEventsDefault
 	}
 	return nil
 }
diff --git a/engine/vcs/github/client_hook.go b/engine/vcs/github/client_hook.go
index 5f9cd5942..392f3243e 100644
--- a/engine/vcs/github/client_hook.go
+++ b/engine/vcs/github/client_hook.go
@@ -25,7 +25,7 @@ func (g *githubClient) CreateHook(ctx context.Context, repo string, hook *sdk.VC
 		hook.URL = g.proxyURL + hook.URL[lastIndexSlash:]
 	}
 	if len(hook.Events) == 0 {
-		hook.Events = []string{"push"}
+		hook.Events = sdk.GitHubEventsDefault
 	}
 
 	r := WebhookCreate{
@@ -80,7 +80,7 @@ func (g *githubClient) UpdateHook(ctx context.Context, repo string, hook *sdk.VC
 		hook.URL = g.proxyURL + hook.URL[lastIndexSlash:]
 	}
 	if len(hook.Events) == 0 {
-		hook.Events = []string{"push"}
+		hook.Events = sdk.GitHubEventsDefault
 	}
 
 	githubWebHook.Events = hook.Events
diff --git a/engine/vcs/github/client_pull_request.go b/engine/vcs/github/client_pull_request.go
index 8f19c14ee..2ac132ea2 100644
--- a/engine/vcs/github/client_pull_request.go
+++ b/engine/vcs/github/client_pull_request.go
@@ -33,7 +33,12 @@ func (g *githubClient) PullRequest(ctx context.Context, fullname string, id int)
 			if err := g.Cache.Delete(cachePullRequestKey); err != nil {
 				log.Error(ctx, "githubclient.PullRequest > unable to delete cache key %v: %v", cachePullRequestKey, err)
 			}
-			return sdk.VCSPullRequest{}, sdk.NewError(sdk.ErrUnknownError, errorAPI(body))
+			if status > 500 {
+				return sdk.VCSPullRequest{}, sdk.NewError(sdk.ErrUnknownError, errorAPI(body))
+			} else {
+				return sdk.VCSPullRequest{}, sdk.NewError(sdk.ErrNotFound, errorAPI(body))
+			}
+
 		}
 
 		//Github may return 304 status because we are using conditional request with ETag based headers
@@ -198,11 +203,11 @@ func (g *githubClient) PullRequestComment(ctx context.Context, repo string, prRe
 func (g *githubClient) PullRequestCreate(ctx context.Context, repo string, pr sdk.VCSPullRequest) (sdk.VCSPullRequest, error) {
 	canWrite, err := g.UserHasWritePermission(ctx, repo)
 	if err != nil {
-		return sdk.VCSPullRequest{}, nil
+		return sdk.VCSPullRequest{}, err
 	}
 	if !canWrite {
 		if err := g.GrantWritePermission(ctx, repo); err != nil {
-			return sdk.VCSPullRequest{}, nil
+			return sdk.VCSPullRequest{}, err
 		}
 	}
 
diff --git a/engine/vcs/github/client_repos.go b/engine/vcs/github/client_repos.go
index d5c709d37..ab9311e2c 100644
--- a/engine/vcs/github/client_repos.go
+++ b/engine/vcs/github/client_repos.go
@@ -153,9 +153,14 @@ func (g *githubClient) repoByFullname(ctx context.Context, fullname string) (Rep
 
 func (g *githubClient) UserHasWritePermission(ctx context.Context, fullname string) (bool, error) {
 	owner := strings.SplitN(fullname, "/", 2)[0]
-	if g.username == "" || owner == g.username {
+	if g.username == "" {
 		return false, sdk.WrapError(sdk.ErrUserNotFound, "No user found in configuration")
 	}
+	if g.username == owner {
+		log.Debug("githubClient.UserHasWritePermission> nothing to do ¯\\_(ツ)_/¯")
+		return true, nil
+	}
+
 	url := "/repos/" + fullname + "/collaborators/" + g.username + "/permission"
 	k := cache.Key("vcs", "github", "user-write", g.OAuthToken, url)
 
diff --git a/engine/vcs/gitlab/client_hook.go b/engine/vcs/gitlab/client_hook.go
index 925b1a5d9..7aef4e442 100644
--- a/engine/vcs/gitlab/client_hook.go
+++ b/engine/vcs/gitlab/client_hook.go
@@ -58,7 +58,7 @@ func (c *gitlabClient) CreateHook(ctx context.Context, repo string, hook *sdk.VC
 
 	var pushEvent, mergeRequestEvent, TagPushEvent, issueEvent, noteEvent, wikiPageEvent, pipelineEvent, jobEvent bool
 	if len(hook.Events) == 0 {
-		hook.Events = []string{string(gitlab.EventTypePush), string(gitlab.EventTypeTagPush)}
+		hook.Events = sdk.GitlabEventsDefault
 	}
 
 	for _, e := range hook.Events {
@@ -117,7 +117,7 @@ func (c *gitlabClient) UpdateHook(ctx context.Context, repo string, hook *sdk.VC
 
 	var pushEvent, mergeRequestEvent, TagPushEvent, issueEvent, noteEvent, wikiPageEvent, pipelineEvent, jobEvent bool
 	if len(hook.Events) == 0 {
-		hook.Events = []string{string(gitlab.EventTypePush), string(gitlab.EventTypeTagPush)}
+		hook.Events = sdk.GitlabEventsDefault
 	}
 
 	for _, e := range hook.Events {
diff --git a/engine/vcs/vcs_handlers.go b/engine/vcs/vcs_handlers.go
index 51e0057e1..6fd95b404 100644
--- a/engine/vcs/vcs_handlers.go
+++ b/engine/vcs/vcs_handlers.go
@@ -102,102 +102,19 @@ func (s *Service) getVCSServersHooksHandler() service.Handler {
 			res.WebhooksDisabled = cfg.Bitbucket.DisableWebHooks
 			res.WebhooksIcon = sdk.BitbucketIcon
 			// https://confluence.atlassian.com/bitbucketserver/event-payload-938025882.html
-			res.Events = []string{
-				"repo:refs_changed",
-				"repo:modified",
-				"repo:forked",
-				"repo:comment:added",
-				"repo:comment:edited",
-				"repo:comment:deleted",
-				"pr:opened",
-				"pr:modified",
-				"pr:reviewer:updated",
-				"pr:reviewer:approved",
-				"pr:reviewer:unapproved",
-				"pr:reviewer:needs_work",
-				"pr:merged",
-				"pr:declined",
-				"pr:deleted",
-				"pr:comment:added",
-				"pr:comment:edited",
-				"pr:comment:deleted",
-			}
+			res.Events = sdk.BitbucketEvents
 		case cfg.BitbucketCloud != nil:
 			res.WebhooksSupported = true
 			res.WebhooksDisabled = cfg.BitbucketCloud.DisableWebHooks
 			res.WebhooksIcon = sdk.BitbucketIcon
 			// https://developer.atlassian.com/bitbucket/api/2/reference/resource/hook_events/%7Bsubject_type%7D
-			res.Events = []string{
-				"repo:push",
-				"pullrequest:unapproved",
-				"issue:comment_created",
-				"pullrequest:approved",
-				"repo:created",
-				"repo:deleted",
-				"repo:imported",
-				"pullrequest:comment_updated",
-				"issue:updated",
-				"project:updated",
-				"pullrequest:comment_created",
-				"repo:commit_status_updated",
-				"pullrequest:updated",
-				"issue:created",
-				"repo:fork",
-				"pullrequest:comment_deleted",
-				"repo:commit_status_created",
-				"repo:updated",
-				"pullrequest:rejected",
-				"pullrequest:fulfilled",
-				"pullrequest:created",
-				"repo:transfer",
-				"repo:commit_comment_created",
-			}
+			res.Events = sdk.BitbucketCloudEvents
 		case cfg.Github != nil:
 			res.WebhooksSupported = true
 			res.WebhooksDisabled = cfg.Github.DisableWebHooks
 			res.WebhooksIcon = sdk.GitHubIcon
 			// https://developer.github.com/v3/activity/events/types/
-			res.Events = []string{
-				"push",
-				"check_run",
-				"check_suite",
-				"commit_comment",
-				"create",
-				"delete",
-				"deployment",
-				"deployment_status",
-				"fork",
-				"github_app_authorization",
-				"gollum",
-				"installation",
-				"installation_repositories",
-				"issue_comment",
-				"issues",
-				"label",
-				"marketplace_purchase",
-				"member",
-				"membership",
-				"milestone",
-				"organization",
-				"org_block",
-				"page_build",
-				"project_card",
-				"project_column",
-				"project",
-				"public",
-				"pull-request_review_comment",
-				"pull-request_review",
-				"pull_request",
-				"repository",
-				"repository_import",
-				"repository_vulnerability_alert",
-				"release",
-				"security_advisory",
-				"status",
-				"team",
-				"team_add",
-				"watch",
-			}
+			res.Events = sdk.GitHubEvents
 		case cfg.Gitlab != nil:
 			res.WebhooksSupported = true
 			res.WebhooksDisabled = cfg.Gitlab.DisableWebHooks
@@ -218,26 +135,7 @@ func (s *Service) getVCSServersHooksHandler() service.Handler {
 			res.GerritHookDisabled = cfg.Gerrit.DisableGerritEvent
 			res.WebhooksIcon = sdk.GerritIcon
 			// https://git.eclipse.org/r/Documentation/cmd-stream-events.html#events
-			res.Events = []string{
-				"patchset-created",
-				"assignee-changed",
-				"change-abandoned",
-				"change-deleted",
-				"change-merged",
-				"change-restored",
-				"comment-added",
-				"draft-published",
-				"dropped-output",
-				"hashtags-changed",
-				"project-created",
-				"ref-updated",
-				"reviewer-added",
-				"reviewer-deleted",
-				"topic-changed",
-				"wip-state-changed",
-				"private-state-changed",
-				"vote-deleted",
-			}
+			res.Events = sdk.GerritEvents
 		}
 
 		return service.WriteJSON(w, res, http.StatusOK)
diff --git a/engine/worker/cmd_artifacts.go b/engine/worker/cmd_artifacts.go
index 7d40ae766..5dd4df036 100644
--- a/engine/worker/cmd_artifacts.go
+++ b/engine/worker/cmd_artifacts.go
@@ -34,7 +34,7 @@ Inside a job, you can list artifacts of a workflow:
 	}
 	c.Flags().StringVar(&cmdDownloadWorkflowName, "workflow", "", "Workflow name. Optional, default: current workflow")
 	c.Flags().StringVar(&cmdDownloadNumber, "number", "", "Workflow Number. Optional, default: current workflow run")
-	c.Flags().StringVar(&cmdDownloadArtefactName, "pattern", "", "Pattern matching files to list. Optional, default: *")
+	c.Flags().StringVar(&cmdDownloadArtifactName, "pattern", "", "Pattern matching files to list. Optional, default: *")
 	c.Flags().StringVar(&cmdDownloadTag, "tag", "", "Tag matching files to list. Optional")
 
 	return c
@@ -64,7 +64,7 @@ func artifactsCmd() func(cmd *cobra.Command, args []string) {
 		a := workerruntime.DownloadArtifact{
 			Workflow: cmdDownloadWorkflowName,
 			Number:   number,
-			Pattern:  cmdDownloadArtefactName,
+			Pattern:  cmdDownloadArtifactName,
 			Tag:      cmdDownloadTag,
 		}
 
diff --git a/engine/worker/cmd_download.go b/engine/worker/cmd_download.go
index c47d33de5..e2264e8b4 100644
--- a/engine/worker/cmd_download.go
+++ b/engine/worker/cmd_download.go
@@ -19,7 +19,7 @@ import (
 var (
 	cmdDownloadWorkflowName string
 	cmdDownloadNumber       string
-	cmdDownloadArtefactName string
+	cmdDownloadArtifactName string
 	cmdDownloadTag          string
 )
 
@@ -51,7 +51,7 @@ Theses two commands have the same result:
 	}
 	c.Flags().StringVar(&cmdDownloadWorkflowName, "workflow", "", "Workflow name to download from. Optional, default: current workflow")
 	c.Flags().StringVar(&cmdDownloadNumber, "number", "", "Workflow Number to download from. Optional, default: current workflow run")
-	c.Flags().StringVar(&cmdDownloadArtefactName, "pattern", "", "Pattern matching files to download. Optional, default: *")
+	c.Flags().StringVar(&cmdDownloadArtifactName, "pattern", "", "Pattern matching files to download. Optional, default: *")
 	c.Flags().StringVar(&cmdDownloadTag, "tag", "", "Tag matching files to download. Optional")
 	return c
 }
@@ -81,7 +81,7 @@ func downloadCmd() func(cmd *cobra.Command, args []string) {
 		a := workerruntime.DownloadArtifact{
 			Workflow:    cmdDownloadWorkflowName,
 			Number:      number,
-			Pattern:     cmdDownloadArtefactName,
+			Pattern:     cmdDownloadArtifactName,
 			Tag:         cmdDownloadTag,
 			Destination: wd,
 		}
@@ -107,7 +107,7 @@ func downloadCmd() func(cmd *cobra.Command, args []string) {
 		if resp.StatusCode >= 300 {
 			body, err := ioutil.ReadAll(resp.Body)
 			if err != nil {
-				sdk.Exit("cannot artefact download HTTP %v\n", err)
+				sdk.Exit("cannot artifact download HTTP %v\n", err)
 			}
 			cdsError := sdk.DecodeError(body)
 			sdk.Exit("download failed: %v\n", cdsError)
diff --git a/engine/worker/cmd_upload.go b/engine/worker/cmd_upload.go
index f7f7c94f7..238b5eea7 100644
--- a/engine/worker/cmd_upload.go
+++ b/engine/worker/cmd_upload.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
+	"io/ioutil"
 	"net/http"
 	"net/url"
 	"os"
@@ -88,7 +89,12 @@ func uploadCmd() func(cmd *cobra.Command, args []string) {
 			}
 
 			if resp.StatusCode >= 300 {
-				sdk.Exit("cannot artifact upload HTTP %d\n", resp.StatusCode)
+				body, err := ioutil.ReadAll(resp.Body)
+				if err != nil {
+					sdk.Exit("cannot artifact upload HTTP %v\n", err)
+				}
+				cdsError := sdk.DecodeError(body)
+				sdk.Exit("artifact upload failed: %v\n", cdsError)
 			}
 		}
 	}
diff --git a/engine/worker/internal/action/builtin_artifact_upload.go b/engine/worker/internal/action/builtin_artifact_upload.go
index f88a11b89..99b7c9950 100644
--- a/engine/worker/internal/action/builtin_artifact_upload.go
+++ b/engine/worker/internal/action/builtin_artifact_upload.go
@@ -2,7 +2,6 @@ package action
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"path/filepath"
 	"strings"
@@ -47,17 +46,17 @@ func RunArtifactUpload(ctx context.Context, wk workerruntime.Runtime, a sdk.Acti
 
 	tag := sdk.ParameterFind(a.Parameters, "tag")
 	if tag == nil {
-		return res, errors.New("tag variable is empty. aborting")
+		return res, sdk.NewError(sdk.ErrWorkerErrorCommand, fmt.Errorf("tag variable is empty. aborting"))
 	}
 
 	// Global all files matching filePath
 	filesPath, err := afero.Glob(afero.NewOsFs(), artifactPath)
 	if err != nil {
-		return res, fmt.Errorf("cannot perform globbing of pattern '%s': %s", artifactPath, err)
+		return res, sdk.NewError(sdk.ErrWorkerErrorCommand, fmt.Errorf("cannot perform globbing of pattern '%s': %s", artifactPath, err))
 	}
 
 	if len(filesPath) == 0 {
-		return res, fmt.Errorf("pattern '%s' matched no file", artifactPath)
+		return res, sdk.NewError(sdk.ErrWorkerErrorCommand, fmt.Errorf("pattern '%s' matched no file", artifactPath))
 	}
 
 	var globalError = &sdk.MultiError{}
@@ -105,8 +104,7 @@ func RunArtifactUpload(ctx context.Context, wk workerruntime.Runtime, a sdk.Acti
 	wgErrors.Wait()
 
 	if !globalError.IsEmpty() {
-		log.Error(ctx, "Error while uploading artifact: %v", globalError.Error())
-		return res, fmt.Errorf("error: %v", globalError.Error())
+		return res, sdk.NewError(sdk.ErrUnknownError, fmt.Errorf("error: %v", globalError.Error()))
 	}
 
 	return res, nil
diff --git a/engine/worker/internal/handler_download.go b/engine/worker/internal/handler_download.go
index 750f1b27c..35d007124 100644
--- a/engine/worker/internal/handler_download.go
+++ b/engine/worker/internal/handler_download.go
@@ -141,7 +141,7 @@ func downloadHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFunc {
 
 		wg.Wait()
 		if isInError {
-			newError := sdk.NewError(sdk.ErrUnknownError, fmt.Errorf("Error while downloading artefacts - see previous logs"))
+			newError := sdk.NewError(sdk.ErrUnknownError, fmt.Errorf("Error while downloading artifacts - see previous logs"))
 			writeError(w, r, newError)
 		}
 	}
diff --git a/engine/worker/internal/handler_key.go b/engine/worker/internal/handler_key.go
index f1b5a9293..85f7f444d 100644
--- a/engine/worker/internal/handler_key.go
+++ b/engine/worker/internal/handler_key.go
@@ -21,8 +21,7 @@ func keyInstallHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFunc
 
 		body, err := ioutil.ReadAll(r.Body)
 		if err != nil {
-			sdkerr := sdk.NewError(sdk.ErrWrongRequest, err).(sdk.Error)
-			writeJSON(w, sdkerr, sdkerr.Status)
+			writeError(w, r, sdk.NewError(sdk.ErrWrongRequest, err))
 			return
 		}
 
@@ -31,10 +30,7 @@ func keyInstallHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFunc
 		var mapBody = make(map[string]string)
 		if len(body) > 0 {
 			if err := json.Unmarshal(body, &mapBody); err != nil {
-				sdkerr := sdk.Error{
-					Status:  sdk.ErrWrongRequest.Status,
-					Message: err.Error()}
-				writeJSON(w, sdkerr, sdkerr.Status)
+				writeError(w, r, sdk.NewError(sdk.ErrWrongRequest, err))
 				return
 			}
 		}
@@ -42,12 +38,8 @@ func keyInstallHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFunc
 		var key *sdk.Variable
 
 		if wk.currentJob.secrets == nil {
-			err := sdk.Error{
-				Message: "Cannot find any keys for your job",
-				Status:  http.StatusBadRequest,
-			}
 			log.Error(ctx, "%v", err)
-			writeJSON(w, err, err.Status)
+			writeError(w, r, sdk.NewError(sdk.ErrWrongRequest, fmt.Errorf("Cannot find any keys for your job")))
 			return
 		}
 
@@ -59,12 +51,8 @@ func keyInstallHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFunc
 		}
 
 		if key == nil {
-			err := sdk.Error{
-				Message: fmt.Sprintf("Key %s not found", keyName),
-				Status:  http.StatusNotFound,
-			}
 			log.Error(ctx, "%v", err)
-			writeJSON(w, err, err.Status)
+			writeError(w, r, sdk.NewError(sdk.ErrNotFound, fmt.Errorf("Cannot find any keys for your job")))
 			return
 		}
 
@@ -73,13 +61,9 @@ func keyInstallHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFunc
 		if err != nil {
 			log.Error(ctx, "Unable to install key %s: %v", key.Name, err)
 			if sdkerr, ok := err.(*sdk.Error); ok {
-				writeJSON(w, sdkerr, sdkerr.Status)
+				writeError(w, r, sdkerr)
 			} else {
-				err := sdk.Error{
-					Message: err.Error(),
-					Status:  sdk.ErrUnknownError.Status,
-				}
-				writeJSON(w, err, err.Status)
+				writeError(w, r, sdk.NewError(sdk.ErrUnknownError, err))
 			}
 			return
 		}
diff --git a/engine/worker/internal/handler_upload.go b/engine/worker/internal/handler_upload.go
index 8db4e24e9..28045b9fe 100644
--- a/engine/worker/internal/handler_upload.go
+++ b/engine/worker/internal/handler_upload.go
@@ -58,7 +58,7 @@ func uploadHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFunc {
 		workingDir, err := workerruntime.WorkingDirectory(wk.currentJob.context)
 		if err != nil {
 			wk.SendLog(ctx, workerruntime.LevelError, fmt.Sprintf("Artifact upload failed: %v", err))
-			log.Error(ctx, "Artifact upload failed: No woorking directory: %v", err)
+			log.Error(ctx, "Artifact upload failed: No working directory: %v", err)
 			writeError(w, r, err)
 			return
 		}
@@ -74,7 +74,7 @@ func uploadHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFunc {
 		if result.Status != sdk.StatusSuccess {
 			wk.SendLog(ctx, workerruntime.LevelError, fmt.Sprintf("Artifact upload failed: %s", result.Reason))
 			log.Error(ctx, "Artifact upload failed: %v", result)
-			w.WriteHeader(http.StatusInternalServerError)
+			writeError(w, r, fmt.Errorf("Artifact upload failed: %s", result.Reason))
 			return
 		}
 	}
diff --git a/engine/worker/internal/keys.go b/engine/worker/internal/keys.go
index 2ca01628a..6d8f2a1e0 100644
--- a/engine/worker/internal/keys.go
+++ b/engine/worker/internal/keys.go
@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"fmt"
 	"io/ioutil"
-	"net/http"
 	"os"
 	"os/exec"
 	"path"
@@ -27,19 +26,11 @@ func (wk *CurrentWorker) InstallKey(key sdk.Variable) (*workerruntime.KeyRespons
 
 		installedKeyPath := path.Join(keysDirectory.Name(), key.Name)
 		if err := vcs.CleanAllSSHKeys(wk.basedir, keysDirectory.Name()); err != nil {
-			errClean := sdk.Error{
-				Message: fmt.Sprintf("Cannot clean ssh keys : %v", err),
-				Status:  http.StatusInternalServerError,
-			}
-			return nil, sdk.WithStack(errClean)
+			return nil, sdk.NewError(sdk.ErrUnknownError, fmt.Errorf("Cannot clean ssh keys : %v", err))
 		}
 
 		if err := vcs.SetupSSHKey(wk.basedir, keysDirectory.Name(), key); err != nil {
-			errSetup := sdk.Error{
-				Message: fmt.Sprintf("Cannot setup ssh key %s : %v", key.Name, err),
-				Status:  http.StatusInternalServerError,
-			}
-			return nil, sdk.WithStack(errSetup)
+			return nil, sdk.NewError(sdk.ErrUnknownError, fmt.Errorf("Cannot setup ssh key %s : %v", key.Name, err))
 		}
 
 		if x, ok := wk.BaseDir().(*afero.BasePathFs); ok {
@@ -61,40 +52,24 @@ func (wk *CurrentWorker) InstallKey(key sdk.Variable) (*workerruntime.KeyRespons
 
 		if !gpg2Found {
 			if _, err := exec.LookPath("gpg"); err != nil {
-				errBinary := sdk.Error{
-					Message: fmt.Sprintf("Cannot use gpg in your worker because you haven't gpg or gpg2 binary"),
-					Status:  http.StatusBadRequest,
-				}
-				return nil, sdk.WithStack(errBinary)
+				return nil, sdk.NewError(sdk.ErrWorkerErrorCommand, fmt.Errorf("Cannot use gpg in your worker because you haven't gpg or gpg2 binary"))
 			}
 		}
 		content := []byte(key.Value)
 		tmpfile, errTmpFile := ioutil.TempFile("", key.Name)
 		if errTmpFile != nil {
-			errFile := sdk.Error{
-				Message: fmt.Sprintf("Cannot setup pgp key %s : %v", key.Name, errTmpFile),
-				Status:  http.StatusInternalServerError,
-			}
-			return nil, sdk.WithStack(errFile)
+			return nil, sdk.NewError(sdk.ErrWorkerErrorCommand, fmt.Errorf("Cannot setup pgp key %s : %v", key.Name, errTmpFile))
 		}
 		defer func() {
 			_ = os.Remove(tmpfile.Name())
 		}()
 
 		if _, err := tmpfile.Write(content); err != nil {
-			errW := sdk.Error{
-				Message: fmt.Sprintf("Cannot setup pgp key file %s : %v", key.Name, err),
-				Status:  http.StatusInternalServerError,
-			}
-			return nil, sdk.WithStack(errW)
+			return nil, sdk.NewError(sdk.ErrWorkerErrorCommand, fmt.Errorf("Cannot setup pgp key file %s : %v", key.Name, err))
 		}
 
 		if err := tmpfile.Close(); err != nil {
-			errC := sdk.Error{
-				Message: fmt.Sprintf("Cannot setup pgp key file %s (close) : %v", key.Name, err),
-				Status:  http.StatusInternalServerError,
-			}
-			return nil, sdk.WithStack(errC)
+			return nil, sdk.NewError(sdk.ErrWorkerErrorCommand, fmt.Errorf("Cannot setup pgp key file %s (close) : %v", key.Name, err))
 		}
 
 		gpgBin := "gpg"
@@ -105,11 +80,7 @@ func (wk *CurrentWorker) InstallKey(key sdk.Variable) (*workerruntime.KeyRespons
 		var out bytes.Buffer
 		cmd.Stdout = &out
 		if err := cmd.Run(); err != nil {
-			errR := sdk.Error{
-				Message: fmt.Sprintf("Cannot import pgp key %s : %v", key.Name, err),
-				Status:  http.StatusInternalServerError,
-			}
-			return nil, sdk.WithStack(errR)
+			return nil, sdk.NewError(sdk.ErrWorkerErrorCommand, fmt.Errorf("Cannot import pgp key %s : %v", key.Name, err))
 		}
 		return &workerruntime.KeyResponse{
 			Type:    sdk.KeyTypePGP,
@@ -118,11 +89,7 @@ func (wk *CurrentWorker) InstallKey(key sdk.Variable) (*workerruntime.KeyRespons
 		}, nil
 
 	default:
-		err := sdk.Error{
-			Message: fmt.Sprintf("Type key %s is not implemented", key.Type),
-			Status:  http.StatusNotImplemented,
-		}
-		return nil, sdk.WithStack(err)
+		return nil, sdk.NewError(sdk.ErrWorkerErrorCommand, fmt.Errorf("Type key %s is not implemented", key.Type))
 	}
 }
 
@@ -145,11 +112,7 @@ func (wk *CurrentWorker) InstallKeyTo(key sdk.Variable, destinationPath string)
 		}
 
 		if err := vcs.WriteKey(afero.NewOsFs(), destinationPath, key.Value); err != nil {
-			errSetup := sdk.Error{
-				Message: fmt.Sprintf("Cannot setup ssh key %s : %v", key.Name, err),
-				Status:  http.StatusInternalServerError,
-			}
-			return nil, sdk.WithStack(errSetup)
+			return nil, sdk.NewError(sdk.ErrWorkerErrorCommand, fmt.Errorf("Cannot setup ssh key %s : %v", key.Name, err))
 		}
 
 		return &workerruntime.KeyResponse{
@@ -167,40 +130,24 @@ func (wk *CurrentWorker) InstallKeyTo(key sdk.Variable, destinationPath string)
 
 		if !gpg2Found {
 			if _, err := exec.LookPath("gpg"); err != nil {
-				errBinary := sdk.Error{
-					Message: fmt.Sprintf("Cannot use gpg in your worker because you haven't gpg or gpg2 binary"),
-					Status:  http.StatusBadRequest,
-				}
-				return nil, sdk.WithStack(errBinary)
+				return nil, sdk.NewError(sdk.ErrWorkerErrorCommand, fmt.Errorf("Cannot use gpg in your worker because you haven't gpg or gpg2 binary"))
 			}
 		}
 		content := []byte(key.Value)
 		tmpfile, errTmpFile := ioutil.TempFile("", key.Name)
 		if errTmpFile != nil {
-			errFile := sdk.Error{
-				Message: fmt.Sprintf("Cannot setup pgp key %s : %v", key.Name, errTmpFile),
-				Status:  http.StatusInternalServerError,
-			}
-			return nil, sdk.WithStack(errFile)
+			return nil, sdk.NewError(sdk.ErrWorkerErrorCommand, fmt.Errorf("Cannot setup pgp key %s : %v", key.Name, errTmpFile))
 		}
 		defer func() {
 			_ = os.Remove(tmpfile.Name())
 		}()
 
 		if _, err := tmpfile.Write(content); err != nil {
-			errW := sdk.Error{
-				Message: fmt.Sprintf("Cannot setup pgp key file %s : %v", key.Name, err),
-				Status:  http.StatusInternalServerError,
-			}
-			return nil, sdk.WithStack(errW)
+			return nil, sdk.NewError(sdk.ErrWorkerErrorCommand, fmt.Errorf("Cannot setup pgp key file %s : %v", key.Name, err))
 		}
 
 		if err := tmpfile.Close(); err != nil {
-			errC := sdk.Error{
-				Message: fmt.Sprintf("Cannot setup pgp key file %s (close) : %v", key.Name, err),
-				Status:  http.StatusInternalServerError,
-			}
-			return nil, sdk.WithStack(errC)
+			return nil, sdk.NewError(sdk.ErrWorkerErrorCommand, fmt.Errorf("Cannot setup pgp key file %s (close) : %v", key.Name, err))
 		}
 
 		gpgBin := "gpg"
@@ -211,11 +158,7 @@ func (wk *CurrentWorker) InstallKeyTo(key sdk.Variable, destinationPath string)
 		var out bytes.Buffer
 		cmd.Stdout = &out
 		if err := cmd.Run(); err != nil {
-			errR := sdk.Error{
-				Message: fmt.Sprintf("Cannot import pgp key %s : %v", key.Name, err),
-				Status:  http.StatusInternalServerError,
-			}
-			return nil, sdk.WithStack(errR)
+			return nil, sdk.NewError(sdk.ErrWorkerErrorCommand, fmt.Errorf("Cannot import pgp key %s : %v", key.Name, err))
 		}
 		return &workerruntime.KeyResponse{
 			Type:    sdk.KeyTypePGP,
@@ -224,10 +167,6 @@ func (wk *CurrentWorker) InstallKeyTo(key sdk.Variable, destinationPath string)
 		}, nil
 
 	default:
-		err := sdk.Error{
-			Message: fmt.Sprintf("Type key %s is not implemented", key.Type),
-			Status:  http.StatusNotImplemented,
-		}
-		return nil, sdk.WithStack(err)
+		return nil, sdk.NewError(sdk.ErrWorkerErrorCommand, fmt.Errorf("Type key %s is not implemented", key.Type))
 	}
 }
diff --git a/engine/worker/internal/run.go b/engine/worker/internal/run.go
index 3ec74f150..767f995fb 100644
--- a/engine/worker/internal/run.go
+++ b/engine/worker/internal/run.go
@@ -189,6 +189,9 @@ func (w *CurrentWorker) runAction(ctx context.Context, a sdk.Action, jobID int64
 	var t0 = time.Now()
 	defer func() {
 		w.SendLog(ctx, workerruntime.LevelInfo, fmt.Sprintf("End of step \"%s\" (%s)", actionName, sdk.Round(time.Since(t0), time.Second).String()))
+		if w.logger.gelfLogger != nil {
+			w.logger.gelfLogger.hook.Flush()
+		}
 	}()
 
 	//If the action is disabled; skip it
@@ -516,16 +519,16 @@ func (w *CurrentWorker) ProcessJob(jobInfo sdk.WorkflowNodeJobRunData) (res sdk.
 	w.logger.logChan = make(chan sdk.Log)
 	go func() {
 		if err := w.logProcessor(ctx, jobInfo.NodeJobRun.ID); err != nil {
-			log.Error(ctx, "processJob> Logs processor error: %v", err)
+			log.Warning(ctx, "processJob> Logs processor error: %v", err)
 		}
 	}()
 	defer func() {
 		if err := w.drainLogsAndCloseLogger(ctx); err != nil {
-			log.Error(ctx, "processJob> Drain logs error: %v", err)
+			log.Warning(ctx, "processJob> Drain logs error: %v", err)
 		}
 	}()
 	defer func() {
-		log.Error(ctx, "processJob> Status: %s | Reason: %s", res.Status, res.Reason)
+		log.Warning(ctx, "processJob> Status: %s | Reason: %s", res.Status, res.Reason)
 	}()
 
 	wdFile, wdAbs, err := w.setupWorkingDirectory(ctx, jobInfo)
@@ -619,6 +622,10 @@ func (w *CurrentWorker) ProcessJob(jobInfo sdk.WorkflowNodeJobRunData) (res sdk.
 	if err := teardownDirectory(w.basedir, ""); err != nil {
 		log.Error(ctx, "Cannot remove basedir content: %s", err)
 	}
+	// Flushing logs
+	if w.logger.gelfLogger != nil {
+		w.logger.gelfLogger.hook.Flush()
+	}
 
 	return res
 }
diff --git a/engine/worker/internal/take.go b/engine/worker/internal/take.go
index 12ca07bc6..472cbe2df 100644
--- a/engine/worker/internal/take.go
+++ b/engine/worker/internal/take.go
@@ -10,6 +10,7 @@ import (
 	"github.com/ovh/cds/sdk"
 	"github.com/ovh/cds/sdk/jws"
 	"github.com/ovh/cds/sdk/log"
+	"github.com/ovh/cds/sdk/log/hook"
 )
 
 func (w *CurrentWorker) Take(ctx context.Context, job sdk.WorkflowNodeJobRun) error {
@@ -48,10 +49,23 @@ func (w *CurrentWorker) Take(ctx context.Context, job sdk.WorkflowNodeJobRun) er
 
 	if info.GelfServiceAddr != "" {
 		log.Info(ctx, "Setup step logger %s", info.GelfServiceAddr)
-		l, h, err := log.New(ctx, info.GelfServiceAddr)
+		throttlePolicy := hook.NewDefaultThrottlePolicy()
+
+		var graylogCfg = &hook.Config{
+			Addr:     info.GelfServiceAddr,
+			Protocol: "tcp",
+			ThrottlePolicy: &hook.ThrottlePolicyConfig{
+				Amount: 100,
+				Period: 10 * time.Millisecond,
+				Policy: throttlePolicy,
+			},
+		}
+
+		l, h, err := log.New(ctx, graylogCfg)
 		if err != nil {
 			return sdk.WithStack(err)
 		}
+
 		w.logger.gelfLogger = new(logger)
 		w.logger.gelfLogger.logger = l
 		w.logger.gelfLogger.hook = h
diff --git a/engine/worker/internal/types.go b/engine/worker/internal/types.go
index cc998f525..7a5b5ffd0 100644
--- a/engine/worker/internal/types.go
+++ b/engine/worker/internal/types.go
@@ -134,6 +134,7 @@ func (wk *CurrentWorker) SendLog(ctx context.Context, level workerruntime.Level,
 			StepOrder: int64(stepOrder),
 		},
 		JobID:     wk.currentJob.wJob.ID,
+		NodeRunID: wk.currentJob.wJob.WorkflowNodeRunID,
 		Timestamp: time.Now().UnixNano(),
 	}
 	signature, err := jws.Sign(wk.currentJob.signer, dataToSign)
@@ -141,7 +142,6 @@ func (wk *CurrentWorker) SendLog(ctx context.Context, level workerruntime.Level,
 		log.Error(ctx, "unable to sign logs: %v", err)
 	}
 	wk.logger.gelfLogger.logger.WithField(log.ExtraFieldSignature, signature).Log(logLevel, s)
-	wk.logger.gelfLogger.hook.Flush()
 }
 
 func (wk *CurrentWorker) Name() string {
diff --git a/sdk/cdsclient/client.go b/sdk/cdsclient/client.go
index 7a2438d74..9a6d52c44 100644
--- a/sdk/cdsclient/client.go
+++ b/sdk/cdsclient/client.go
@@ -1,10 +1,11 @@
+// For more information about how to user cdsclient package have a look at https://ovh.github.io/cds/development/sdk/golang/.
+
 package cdsclient
 
 import (
 	"context"
 	"crypto/tls"
 	"encoding/base64"
-	"io"
 	"net"
 	"net/http"
 	"os"
@@ -97,16 +98,6 @@ func NewWorker(endpoint string, name string, c *http.Client) WorkerInterface {
 	return cli
 }
 
-// NewClientFromConfig returns a client from the config file
-func NewClientFromConfig(r io.Reader) (Interface, error) {
-	return nil, nil
-}
-
-// NewClientFromEnv returns a client from the environment variables
-func NewClientFromEnv() (Interface, error) {
-	return nil, nil
-}
-
 // NewProviderClient returns an implementation for ProviderClient interface
 func NewProviderClient(cfg ProviderConfig) ProviderClient {
 	conf := Config{
diff --git a/sdk/cdsclient/http_websocket.go b/sdk/cdsclient/http_websocket.go
index 45b0106c8..efc0ccb1f 100644
--- a/sdk/cdsclient/http_websocket.go
+++ b/sdk/cdsclient/http_websocket.go
@@ -40,7 +40,7 @@ func (c *client) RequestWebsocket(ctx context.Context, path string, msgToSend <-
 	urlWebsocket := url.URL{
 		Scheme: strings.Replace(uHost.Scheme, "http", "ws", -1),
 		Host:   uHost.Host,
-		Path:   "/ws",
+		Path:   uHost.Path + "/ws",
 	}
 
 	headers := make(map[string][]string)
diff --git a/sdk/error.go b/sdk/error.go
index e756c3f00..4c5d4a3f4 100644
--- a/sdk/error.go
+++ b/sdk/error.go
@@ -196,6 +196,8 @@ var (
 	ErrWorkflowAsCodeResync                          = Error{ID: 186, Status: http.StatusForbidden}
 	ErrWorkflowNodeNameDuplicate                     = Error{ID: 187, Status: http.StatusBadRequest}
 	ErrUnsupportedMediaType                          = Error{ID: 188, Status: http.StatusUnsupportedMediaType}
+	ErrNothingToPush                                 = Error{ID: 189, Status: http.StatusBadRequest}
+	ErrWorkerErrorCommand                            = Error{ID: 190, Status: http.StatusBadRequest}
 )
 
 var errorsAmericanEnglish = map[int]string{
@@ -374,6 +376,8 @@ var errorsAmericanEnglish = map[int]string{
 	ErrWorkflowAsCodeResync.ID:                          "You cannot resynchronize an as-code workflow",
 	ErrWorkflowNodeNameDuplicate.ID:                     "You cannot have same name for different pipelines in your workflow",
 	ErrUnsupportedMediaType.ID:                          "Request format invalid",
+	ErrNothingToPush.ID:                                 "No diff to push",
+	ErrWorkerErrorCommand.ID:                            "Worker command in error",
 }
 
 var errorsFrench = map[int]string{
@@ -552,6 +556,8 @@ var errorsFrench = map[int]string{
 	ErrWorkflowAsCodeResync.ID:                          "Impossible de resynchroniser un workflow en mode as-code",
 	ErrWorkflowNodeNameDuplicate.ID:                     "Vous ne pouvez pas avoir plusieurs fois le même nom de pipeline dans votre workflow",
 	ErrUnsupportedMediaType.ID:                          "Le format de la requête est invalide",
+	ErrNothingToPush.ID:                                 "Aucune modification à pousser",
+	ErrWorkerErrorCommand.ID:                            "Commande du worker en erreur",
 }
 
 var errorsLanguages = []map[int]string{
diff --git a/sdk/exportentities/v2/workflow.go b/sdk/exportentities/v2/workflow.go
index 9d1b4f225..26f0e212e 100644
--- a/sdk/exportentities/v2/workflow.go
+++ b/sdk/exportentities/v2/workflow.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"math/rand"
 	"sort"
+	"strings"
 	"time"
 
 	"github.com/fsamin/go-dump"
@@ -48,7 +49,7 @@ type NodeEntry struct {
 }
 
 type ConditionEntry struct {
-	PlainConditions []PlainConditionEntry `json:"plain,omitempty" yaml:"check,omitempty"`
+	PlainConditions []PlainConditionEntry `json:"check,omitempty" yaml:"check,omitempty"`
 	LuaScript       string                `json:"script,omitempty" yaml:"script,omitempty"`
 }
 
@@ -66,6 +67,32 @@ type HookEntry struct {
 	Conditions *sdk.WorkflowNodeConditions `json:"conditions,omitempty" yaml:"conditions,omitempty" jsonschema_description:"Conditions to run this hook.\nhttps://ovh.github.io/cds/docs/concepts/workflow/run-conditions."`
 }
 
+func (h HookEntry) IsDefault(model sdk.WorkflowHookModel) bool {
+	if h.Conditions != nil {
+		if h.Conditions.LuaScript != "" || len(h.Conditions.PlainConditions) > 0 {
+			return false
+		}
+	}
+
+	if h.Config != nil {
+		for k, v := range h.Config {
+			dfault, has := model.DefaultConfig[k]
+			if has {
+				if dfault.Configurable && dfault.Value != v &&
+					v != strings.Join(sdk.BitbucketCloudEventsDefault, ";") &&
+					v != strings.Join(sdk.BitbucketEventsDefault, ";") &&
+					v != strings.Join(sdk.GitHubEventsDefault, ";") &&
+					v != strings.Join(sdk.GitlabEventsDefault, ";") &&
+					v != strings.Join(sdk.GerritEventsDefault, ";") {
+					return false
+				}
+			}
+		}
+	}
+
+	return true
+}
+
 type ExportOptions func(w sdk.Workflow, exportedWorkflow *Workflow) error
 
 //NewWorkflow creates a new exportable workflow
@@ -301,6 +328,9 @@ func WorkflowSkipIfOnlyOneRepoWebhook(w sdk.Workflow, exportedWorkflow *Workflow
 	for nodeName, hs := range exportedWorkflow.Hooks {
 		if nodeName == w.WorkflowData.Node.Name && len(hs) == 1 {
 			if hs[0].Model == sdk.RepositoryWebHookModelName {
+				if !hs[0].IsDefault(sdk.RepositoryWebHookModel) {
+					return nil
+				}
 				delete(exportedWorkflow.Hooks, nodeName)
 				if exportedWorkflow.Workflow != nil {
 					for nodeName := range exportedWorkflow.Workflow {
diff --git a/sdk/exportentities/worker_model.go b/sdk/exportentities/worker_model.go
index 29285113c..ac8eb13b5 100644
--- a/sdk/exportentities/worker_model.go
+++ b/sdk/exportentities/worker_model.go
@@ -6,24 +6,23 @@ import (
 
 // WorkerModel is the as code format of a worker model
 type WorkerModel struct {
-	Name          string            `json:"name" yaml:"name"`
-	Group         string            `json:"group" yaml:"group"`
-	Communication string            `json:"communication,omitempty" yaml:"communication,omitempty"`
-	Image         string            `json:"image" yaml:"image"`
-	Registry      string            `json:"registry,omitempty" yaml:"registry,omitempty"`
-	Username      string            `json:"username,omitempty" yaml:"username,omitempty"`
-	Password      string            `json:"password,omitempty" yaml:"password,omitempty"`
-	Description   string            `json:"description" yaml:"description"`
-	Type          string            `json:"type" yaml:"type"`
-	Flavor        string            `json:"flavor,omitempty" yaml:"flavor,omitempty"`
-	Envs          map[string]string `json:"envs,omitempty" yaml:"envs,omitempty"`
-	PatternName   string            `json:"pattern_name,omitempty" yaml:"pattern_name,omitempty"`
-	Shell         string            `json:"shell,omitempty" yaml:"shell,omitempty"`
-	PreCmd        string            `json:"pre_cmd,omitempty" yaml:"pre_cmd,omitempty"`
-	Cmd           string            `json:"cmd,omitempty" yaml:"cmd,omitempty"`
-	PostCmd       string            `json:"post_cmd,omitempty" yaml:"post_cmd,omitempty"`
-	Restricted    bool              `json:"restricted,omitempty" yaml:"restricted,omitempty"`
-	IsDeprecated  bool              `json:"is_deprecated,omitempty" yaml:"is_deprecated,omitempty"`
+	Name         string            `json:"name" yaml:"name"`
+	Group        string            `json:"group" yaml:"group"`
+	Image        string            `json:"image" yaml:"image"`
+	Registry     string            `json:"registry,omitempty" yaml:"registry,omitempty"`
+	Username     string            `json:"username,omitempty" yaml:"username,omitempty"`
+	Password     string            `json:"password,omitempty" yaml:"password,omitempty"`
+	Description  string            `json:"description" yaml:"description"`
+	Type         string            `json:"type" yaml:"type"`
+	Flavor       string            `json:"flavor,omitempty" yaml:"flavor,omitempty"`
+	Envs         map[string]string `json:"envs,omitempty" yaml:"envs,omitempty"`
+	PatternName  string            `json:"pattern_name,omitempty" yaml:"pattern_name,omitempty"`
+	Shell        string            `json:"shell,omitempty" yaml:"shell,omitempty"`
+	PreCmd       string            `json:"pre_cmd,omitempty" yaml:"pre_cmd,omitempty"`
+	Cmd          string            `json:"cmd,omitempty" yaml:"cmd,omitempty"`
+	PostCmd      string            `json:"post_cmd,omitempty" yaml:"post_cmd,omitempty"`
+	Restricted   bool              `json:"restricted,omitempty" yaml:"restricted,omitempty"`
+	IsDeprecated bool              `json:"is_deprecated,omitempty" yaml:"is_deprecated,omitempty"`
 }
 
 type WorkerModelOption func(sdk.Model, *WorkerModel) error
@@ -53,7 +52,6 @@ func NewWorkerModel(wm sdk.Model, opts ...WorkerModelOption) WorkerModel {
 		IsDeprecated: wm.IsDeprecated,
 		Description:  wm.Description,
 		Restricted:   wm.Restricted,
-		Image:        wm.Image,
 	}
 
 	switch wm.Type {
diff --git a/sdk/hook.go b/sdk/hook.go
index acbad69ec..cd6b8b3aa 100644
--- a/sdk/hook.go
+++ b/sdk/hook.go
@@ -145,6 +145,11 @@ var (
 				Configurable: false,
 				Type:         HookConfigTypeString,
 			},
+			HookConfigEventFilter: {
+				Value:        "",
+				Configurable: true,
+				Type:         HookConfigTypeMultiChoice,
+			},
 		},
 	}
 
diff --git a/sdk/log/hook/hook.go b/sdk/log/hook/hook.go
index d8bd88189..211513190 100644
--- a/sdk/log/hook/hook.go
+++ b/sdk/log/hook/hook.go
@@ -9,7 +9,6 @@ import (
 	"fmt"
 	"os"
 	"strings"
-	"sync"
 	"time"
 
 	"github.com/eapache/go-resiliency/retrier"
@@ -43,10 +42,6 @@ var priorities = map[logrus.Level]Priority{
 	logrus.DebugLevel: LOG_DEBUG,
 }
 
-// SendPolicy defines the policy to use when the buffer is full (drop, block, flush, ...)
-// The default policy is to drop the message as it's always copied to stderr anyway.
-type SendPolicy func(*Message, chan *Message)
-
 // MergeFields defines a function to merge fields. It used for example to define your own field
 // convientions to match with your graylog service.
 type MergeFields func(...map[string]interface{}) map[string]interface{}
@@ -58,22 +53,19 @@ var BufSize uint = 16384
 
 // Config is the required configuration for creating a Graylog hook
 type Config struct {
-	Addr       string
-	Protocol   string
-	Hostname   string
-	Facility   string
-	TLSConfig  *tls.Config
-	SendPolicy SendPolicy
-	Merge      func(...map[string]interface{}) map[string]interface{}
+	Addr           string
+	Protocol       string
+	Hostname       string
+	Facility       string
+	TLSConfig      *tls.Config
+	Merge          func(...map[string]interface{}) map[string]interface{}
+	ThrottlePolicy *ThrottlePolicyConfig
 }
 
 // Hook to send logs to a logging service compatible with the Graylog API and the GELF format.
 type Hook struct {
 	Facility string
 	Hostname string
-	// Sending policy is used to deal with Graylog connection failure.
-	// If nil, DropPolicy is used by default, dropping logs when connection failure happens.
-	SendPolicy SendPolicy
 	// Extra fields to send to Graylog for each log entry.
 	Extra map[string]interface{}
 	// Minimum logging level to send to Graylog.
@@ -82,12 +74,12 @@ type Hook struct {
 	Threshold logrus.Level
 
 	merge      MergeFields
-	pid        int
+	Pid        int
 	gelfLogger Writer
-	messages   chan *Message
-	done       chan struct{}
-	closed     bool
-	l          sync.Mutex
+
+	throttleStack  *Stack
+	throttleTicker *time.Ticker
+	throttlePolicy ThrottlePolicy
 }
 
 // NewHook creates a hook to be added to an instance of logger.
@@ -126,58 +118,39 @@ func NewHook(cfg *Config, extra map[string]interface{}) (*Hook, error) {
 		return nil, err
 	}
 
-	if cfg.SendPolicy == nil {
-		cfg.SendPolicy = DropPolicy
-	}
-
 	merge := mergeFields
 	if cfg.Merge != nil {
 		merge = cfg.Merge
 	}
 
-	fmt.Fprintf(os.Stderr, "[graylog] using endpoint: %s\n", cfg.Addr)
-
 	hook := &Hook{
 		Facility:   cfg.Facility,
 		Hostname:   hostname,
 		Extra:      extra,
-		SendPolicy: cfg.SendPolicy,
 		Threshold:  logrus.DebugLevel,
 		merge:      merge,
-		pid:        os.Getpid(),
+		Pid:        os.Getpid(),
 		gelfLogger: w,
-		messages:   make(chan *Message, BufSize),
-		done:       make(chan struct{}, 1),
 	}
 
-	go hook.fire() // Log in background
-	return hook, nil
-}
-
-// Flush sends all remaining logs in the buffer to Graylog before returning
-func (hook *Hook) Flush() {
-	hook.l.Lock()
-	defer hook.l.Unlock()
-	if hook.closed {
-		return
+	if cfg.ThrottlePolicy == nil {
+		dfault := NewDefaultThrottlePolicy()
+		cfg.ThrottlePolicy = &ThrottlePolicyConfig{
+			Amount: 5,
+			Period: 10 * time.Second,
+			Policy: dfault,
+		}
 	}
 
-	// cloes send channel to start flushing
-	close(hook.messages)
-
-	// then simply wait for fire to empty the messages
-	// or timeout after a minute
-	select {
-	case <-hook.done:
-		close(hook.done)
-	case <-time.After(time.Minute):
-		fmt.Fprintln(os.Stderr, "[graylog] flushing timed out")
-	}
+	hook.throttleStack = NewStack(cfg.ThrottlePolicy.Amount)
+	hook.throttleTicker = time.NewTicker(cfg.ThrottlePolicy.Period)
+	hook.throttlePolicy = cfg.ThrottlePolicy.Policy
+	hook.throttlePolicy.Init(hook)
+	fmt.Fprintf(os.Stdout, "[graylog] using endpoint: %s\n", cfg.Addr)
 
-	hook.messages = make(chan *Message, BufSize)
-	hook.done = make(chan struct{}, 1)
+	go hook.fire() // Log in background
 
-	go hook.fire()
+	return hook, nil
 }
 
 // Fire is called when a log event is fired.
@@ -190,38 +163,59 @@ func (hook *Hook) Fire(entry *logrus.Entry) error {
 	// performance impact due to locking
 	file, line := getCallerIgnoringLogMulti(1)
 	msg := hook.messageFromEntry(entry, file, line)
+	return hook.FireMessage(*msg)
+}
 
-	hook.l.Lock()
-	defer hook.l.Unlock()
-	if hook.closed {
-		return nil
-	}
+type FireMessageFunc func(msg Message) error
+
+func (hook *Hook) IsThrottled() bool {
+	return !hook.throttleStack.Empty()
+}
 
-	hook.SendPolicy(msg, hook.messages)
+func (hook *Hook) FireMessage(msg Message) error {
+	switch {
+	case !hook.IsThrottled() && !hook.throttlePolicy.PendingTrailingMessages():
+		hook.throttleStack.Push(msg)
+	default:
+		hook.throttlePolicy.HandleTrailingMessage(msg)
+	}
 	return nil
 }
 
-// fire will loop on the 'buf' channel, and write entries to graylog
+func (hook *Hook) Flush() {
+	hook.throttlePolicy.Flush()
+}
+
+var r = retrier.New(retrier.ExponentialBackoff(20, time.Millisecond), nil)
+
+func (hook *Hook) send(m Message) {
+	// we retry at least 3 times to write message to graylog.
+	err := r.Run(func() error {
+		if err := hook.gelfLogger.WriteMessage(&m); err != nil {
+			fmt.Fprintln(os.Stderr, "[graylog] could not write message to Graylog:", err)
+			return err
+		}
+		return nil
+	})
+	// if after all the retries we still cannot write the message, just skip
+	if err != nil {
+		fmt.Fprintln(os.Stderr, "[graylog] could not write message to Graylog after several retries:", err)
+	}
+}
+
+// fire will loop on the 'throttled' channel, and write entries to graylog
 func (hook *Hook) fire() {
-	r := retrier.New(retrier.ExponentialBackoff(3, time.Second), nil)
-	// consume message buffer
-	for message := range hook.messages {
-		// we retry at least 3 times to write message to graylog.
-		// gelf package also has its own retry behaviour, which is
-		// roughly trying for a good ~15 minutes.
-		err := r.Run(func() error {
-			if err := hook.gelfLogger.WriteMessage(message); err != nil {
-				fmt.Fprintln(os.Stderr, "[graylog] could not write message to Graylog:", err)
-				return err
+	defer hook.throttleTicker.Stop()
+	for range hook.throttleTicker.C {
+		for {
+			time.Sleep(time.Millisecond)
+			m, has := hook.throttleStack.Pop()
+			if !has {
+				continue
 			}
-			return nil
-		})
-		// if after all the retries we still cannot write the message, just skip
-		if err != nil {
-			fmt.Fprintln(os.Stderr, "[graylog] could not write message to Graylog after several retries:", err)
+			hook.send(m)
 		}
 	}
-	hook.done <- struct{}{}
 }
 
 // Levels returns the available logging levels.
@@ -259,7 +253,7 @@ func (hook *Hook) messageFromEntry(entry *logrus.Entry, file string, line int) *
 		Full:     full,
 		Time:     float64(entry.Time.UnixNano()) / 1e9,
 		Level:    int32(priorities[entry.Level]),
-		Pid:      hook.pid,
+		Pid:      hook.Pid,
 		Facility: hook.Facility,
 		File:     file,
 		Line:     line,
diff --git a/sdk/log/hook/stack.go b/sdk/log/hook/stack.go
new file mode 100644
index 000000000..75f8f6f42
--- /dev/null
+++ b/sdk/log/hook/stack.go
@@ -0,0 +1,70 @@
+package hook
+
+import "sync"
+
+// NewStack returns a new stack.
+func NewStack(cap int) *Stack {
+	s := &Stack{
+		size:     cap,
+		cap:      cap,
+		messages: make([]Message, cap),
+	}
+	return s
+}
+
+// Stack is a basic LIFO stack that resizes as needed.
+type Stack struct {
+	mutex    sync.Mutex
+	messages []Message
+	size     int
+	head     int
+	tail     int
+	count    int
+	cap      int
+}
+
+// Push adds a node to the queue.
+func (q *Stack) Push(n Message) {
+	q.mutex.Lock()
+	defer q.mutex.Unlock()
+
+	if q.head == q.tail && q.count > 0 {
+		messages := make([]Message, len(q.messages)+q.size)
+		copy(messages, q.messages[q.head:])
+		copy(messages[len(q.messages)-q.head:], q.messages[:q.head])
+		q.head = 0
+		q.tail = len(q.messages)
+		q.messages = messages
+	}
+	q.messages[q.tail] = n
+	q.tail = (q.tail + 1) % len(q.messages)
+	q.count++
+}
+
+func (q *Stack) Ready() bool {
+	q.mutex.Lock()
+	defer q.mutex.Unlock()
+
+	return q.count < q.cap
+}
+
+func (q *Stack) Empty() bool {
+	q.mutex.Lock()
+	defer q.mutex.Unlock()
+
+	return q.count == 0
+}
+
+// Pop removes and returns a node from the queue in first to last order.
+func (q *Stack) Pop() (Message, bool) {
+	q.mutex.Lock()
+	defer q.mutex.Unlock()
+
+	if q.count == 0 {
+		return Message{}, false
+	}
+	node := q.messages[q.head]
+	q.head = (q.head + 1) % len(q.messages)
+	q.count--
+	return node, true
+}
diff --git a/sdk/log/hook/throttle.go b/sdk/log/hook/throttle.go
new file mode 100644
index 000000000..9b58c5de6
--- /dev/null
+++ b/sdk/log/hook/throttle.go
@@ -0,0 +1,78 @@
+package hook
+
+import (
+	"fmt"
+	"os"
+	"sync"
+	"time"
+)
+
+type ThrottlePolicyConfig struct {
+	Amount int
+	Period time.Duration
+	Policy ThrottlePolicy
+}
+
+type ThrottlePolicy interface {
+	Init(hook *Hook)
+	HandleTrailingMessage(m Message)
+	PendingTrailingMessages() bool
+	Flush()
+}
+
+func NewDefaultThrottlePolicy() ThrottlePolicy {
+	o := &DefaultThrottlePolicy{}
+	return o
+}
+
+var _ ThrottlePolicy = new(DefaultThrottlePolicy)
+
+type DefaultThrottlePolicy struct {
+	mutex  sync.Mutex
+	buffer chan Message
+}
+
+func (d *DefaultThrottlePolicy) Init(hook *Hook) {
+	d.buffer = make(chan Message, BufSize)
+	go func() {
+		for {
+			time.Sleep(1 * time.Millisecond)
+			if !hook.IsThrottled() {
+				m, more := <-d.buffer
+				hook.throttleStack.Push(m)
+				if !more {
+					fmt.Fprintf(os.Stderr, "[graylog] exiting trailing message goroutine...\n")
+					break
+				}
+			}
+		}
+	}()
+}
+
+func (d *DefaultThrottlePolicy) HandleTrailingMessage(m Message) {
+	d.mutex.Lock()
+	defer d.mutex.Unlock()
+
+	select {
+	case d.buffer <- m:
+	default:
+		fmt.Fprintf(os.Stderr, "[graylog] message dropped\n")
+	}
+}
+
+func (d *DefaultThrottlePolicy) Flush() {
+	d.mutex.Lock()
+	defer d.mutex.Unlock()
+
+	fmt.Fprintf(os.Stderr, "[graylog] flush\n")
+	for len(d.buffer) != 0 {
+		time.Sleep(time.Second)
+	}
+}
+
+func (d *DefaultThrottlePolicy) PendingTrailingMessages() bool {
+	d.mutex.Lock()
+	defer d.mutex.Unlock()
+
+	return len(d.buffer) > 0
+}
diff --git a/sdk/log/log.go b/sdk/log/log.go
index dad041dfb..62ade2a6e 100644
--- a/sdk/log/log.go
+++ b/sdk/log/log.go
@@ -8,8 +8,9 @@ import (
 	"os"
 	"strings"
 
-	loghook "github.com/ovh/cds/sdk/log/hook"
 	log "github.com/sirupsen/logrus"
+
+	"github.com/ovh/cds/sdk/log/hook"
 )
 
 // Conf contains log configuration
@@ -36,8 +37,8 @@ const (
 )
 
 var (
-	logger Logger
-	hook   *loghook.Hook
+	logger      Logger
+	graylogHook *hook.Hook
 )
 
 // Logger defines the logs levels used
@@ -84,7 +85,7 @@ func Initialize(ctx context.Context, conf *Conf) {
 	log.SetFormatter(&CDSFormatter{})
 
 	if conf.GraylogHost != "" && conf.GraylogPort != "" {
-		graylogcfg := &loghook.Config{
+		graylogcfg := &hook.Config{
 			Addr:      fmt.Sprintf("%s:%s", conf.GraylogHost, conf.GraylogPort),
 			Protocol:  conf.GraylogProtocol,
 			TLSConfig: &tls.Config{ServerName: conf.GraylogHost},
@@ -124,12 +125,12 @@ func Initialize(ctx context.Context, conf *Conf) {
 		extra["CDSHostname"] = hostname
 
 		var errhook error
-		hook, errhook = loghook.NewHook(graylogcfg, extra)
+		graylogHook, errhook = hook.NewHook(graylogcfg, extra)
 
 		if errhook != nil {
 			log.Errorf("Error while initialize graylog hook: %v", errhook)
 		} else {
-			log.AddHook(hook)
+			log.AddHook(graylogHook)
 			log.SetOutput(ioutil.Discard)
 		}
 	}
@@ -137,8 +138,8 @@ func Initialize(ctx context.Context, conf *Conf) {
 	go func() {
 		<-ctx.Done()
 		Info(ctx, "Draining logs...")
-		if hook != nil {
-			hook.Flush()
+		if graylogHook != nil {
+			graylogHook.Flush()
 		}
 	}()
 }
@@ -241,6 +242,7 @@ type Signature struct {
 	Worker    *SignatureWorker
 	Service   *SignatureService
 	JobID     int64
+	NodeRunID int64
 	Timestamp int64
 }
 
@@ -257,14 +259,10 @@ type SignatureService struct {
 	WorkerName      string
 }
 
-func New(ctx context.Context, logServerAddr string) (*log.Logger, *loghook.Hook, error) {
+func New(ctx context.Context, graylogcfg *hook.Config) (*log.Logger, *hook.Hook, error) {
 	newLogger := log.New()
-	graylogcfg := &loghook.Config{
-		Addr:     logServerAddr,
-		Protocol: "tcp",
-	}
 	extra := map[string]interface{}{}
-	hook, err := loghook.NewHook(graylogcfg, extra)
+	hook, err := hook.NewHook(graylogcfg, extra)
 	if err != nil {
 		return nil, nil, fmt.Errorf("unable to add hook: %v", err)
 	}
diff --git a/sdk/project.go b/sdk/project.go
index 7b1e1ea97..eb20b338a 100644
--- a/sdk/project.go
+++ b/sdk/project.go
@@ -43,7 +43,7 @@ type Project struct {
 	Labels           []Label                `json:"labels,omitempty" yaml:"labels,omitempty" db:"-"  cli:"-"`
 	Permissions      Permissions            `json:"permissions" yaml:"-" db:"-"  cli:"-"`
 	Metadata         Metadata               `json:"metadata" yaml:"metadata" db:"metadata" cli:"-"`
-	Keys             []ProjectKey           `json:"keys" yaml:"keys" db:"-" cli:"-"`
+	Keys             []ProjectKey           `json:"keys,omitempty" yaml:"keys" db:"-" cli:"-"`
 	VCSServers       []ProjectVCSServerLink `json:"vcs_servers" yaml:"vcs_servers" db:"-" cli:"-"`
 	Integrations     []ProjectIntegration   `json:"integrations" yaml:"integrations" db:"-" cli:"-"`
 	Features         map[string]bool        `json:"features" yaml:"features" db:"-" cli:"-"`
diff --git a/sdk/repositories_operation.go b/sdk/repositories_operation.go
index 1fe070ba7..578a87801 100644
--- a/sdk/repositories_operation.go
+++ b/sdk/repositories_operation.go
@@ -15,7 +15,8 @@ type Operation struct {
 	Setup              OperationSetup           `json:"setup,omitempty"`
 	LoadFiles          OperationLoadFiles       `json:"load_files,omitempty"`
 	Status             OperationStatus          `json:"status"`
-	Error              string                   `json:"error,omitempty"`
+	Error              *OperationError          `json:"error_details,omitempty"`
+	DeprecatedError    string                   `json:"error,omitempty"`
 	RepositoryInfo     *OperationRepositoryInfo `json:"repository_info,omitempty"`
 	Date               *time.Time               `json:"date,omitempty"`
 	User               struct {
@@ -23,6 +24,42 @@ type Operation struct {
 		Fullname string `json:"fullname,omitempty"  db:"-" cli:"-"`
 		Email    string `json:"email,omitempty"  db:"-" cli:"-"`
 	} `json:"user,omitempty"`
+	RequestID string `json:"request_id,omitempty"`
+	NbRetries int    `json:"nb_retries,omitempty"`
+}
+
+type OperationError struct {
+	ID         int    `json:"id"`
+	Status     int    `json:"status,omitempty"`
+	Message    string `json:"message"`
+	StackTrace string `json:"stack_trace,omitempty"`
+	From       string `json:"from,omitempty"`
+}
+
+func ToOperationError(err error) *OperationError {
+	if err == nil {
+		return nil
+	}
+	sdkError := ExtractHTTPError(err, "")
+	return &OperationError{
+		ID:         sdkError.ID,
+		Status:     sdkError.Status,
+		From:       sdkError.From,
+		Message:    sdkError.Message,
+		StackTrace: sdkError.StackTrace,
+	}
+}
+
+func (opError *OperationError) ToError() error {
+	if opError == nil {
+		return nil
+	}
+	return &Error{
+		ID:      opError.ID,
+		Status:  opError.Status,
+		Message: opError.Message,
+		From:    opError.From,
+	}
 }
 
 // OperationSetup is the setup for an operation basically its a checkout
diff --git a/sdk/vcs.go b/sdk/vcs.go
index 00d310d0b..b2aa89e95 100644
--- a/sdk/vcs.go
+++ b/sdk/vcs.go
@@ -15,6 +15,158 @@ const (
 	HeaderXAccessTokenSecret  = "X-CDS-ACCESS-TOKEN-SECRET"
 )
 
+var (
+	BitbucketEvents = []string{
+		"repo:refs_changed",
+		"repo:modified",
+		"repo:forked",
+		"repo:comment:added",
+		"repo:comment:edited",
+		"repo:comment:deleted",
+		"pr:opened",
+		"pr:modified",
+		"pr:reviewer:updated",
+		"pr:reviewer:approved",
+		"pr:reviewer:unapproved",
+		"pr:reviewer:needs_work",
+		"pr:merged",
+		"pr:declined",
+		"pr:deleted",
+		"pr:comment:added",
+		"pr:comment:edited",
+		"pr:comment:deleted",
+	}
+
+	BitbucketEventsDefault = []string{
+		"repo:refs_changed",
+	}
+
+	BitbucketCloudEvents = []string{
+		"repo:push",
+		"pullrequest:unapproved",
+		"issue:comment_created",
+		"pullrequest:approved",
+		"repo:created",
+		"repo:deleted",
+		"repo:imported",
+		"pullrequest:comment_updated",
+		"issue:updated",
+		"project:updated",
+		"pullrequest:comment_created",
+		"repo:commit_status_updated",
+		"pullrequest:updated",
+		"issue:created",
+		"repo:fork",
+		"pullrequest:comment_deleted",
+		"repo:commit_status_created",
+		"repo:updated",
+		"pullrequest:rejected",
+		"pullrequest:fulfilled",
+		"pullrequest:created",
+		"repo:transfer",
+		"repo:commit_comment_created",
+	}
+
+	BitbucketCloudEventsDefault = []string{
+		"repo:push",
+	}
+
+	GitHubEvents = []string{
+		"push",
+		"check_run",
+		"check_suite",
+		"commit_comment",
+		"create",
+		"delete",
+		"deployment",
+		"deployment_status",
+		"fork",
+		"github_app_authorization",
+		"gollum",
+		"installation",
+		"installation_repositories",
+		"issue_comment",
+		"issues",
+		"label",
+		"marketplace_purchase",
+		"member",
+		"membership",
+		"milestone",
+		"organization",
+		"org_block",
+		"page_build",
+		"project_card",
+		"project_column",
+		"project",
+		"public",
+		"pull-request_review_comment",
+		"pull-request_review",
+		"pull_request",
+		"repository",
+		"repository_import",
+		"repository_vulnerability_alert",
+		"release",
+		"security_advisory",
+		"status",
+		"team",
+		"team_add",
+		"watch",
+	}
+
+	GitHubEventsDefault = []string{
+		"push",
+	}
+
+	GitlabEventsDefault = []string{
+		"Push Hook",
+		"Tag Push Hook",
+	}
+
+	GerritEvents = []string{
+		GerritEventTypePatchsetCreated,
+		GerritEventTypeAssignedChanged,
+		GerritEventTypeChangeAbandoned,
+		GerritEventTypeChangeDeleted,
+		GerritEventTypeChangeMerged,
+		GerritEventTypeChangeRestored,
+		GerritEventTypeCommentAdded,
+		GerritEventTypeDrafPublished,
+		GerritEventTypeDroppedOutput,
+		GerritEventTypeHashTagsChanged,
+		GerritEventTypeProjectCreated,
+		GerritEventTypeRefUpdated,
+		GerritEventTypeReviewerAdded,
+		GerritEventTypeReviewerDelete,
+		GerritEventTypeTopicChanged,
+		GerritEventTypeWIPStateChanged,
+		GerritEventTypePrivateStateChanged,
+		GerritEventTypeVoteDeleted,
+	}
+
+	GerritEventTypeAssignedChanged     = "assignee-changed"
+	GerritEventTypeChangeAbandoned     = "change-abandoned"
+	GerritEventTypeChangeDeleted       = "change-deleted"
+	GerritEventTypeChangeMerged        = "change-merged"
+	GerritEventTypeChangeRestored      = "change-restored"
+	GerritEventTypeCommentAdded        = "comment-added"
+	GerritEventTypeDrafPublished       = "draft-published"
+	GerritEventTypeDroppedOutput       = "dropped-output"
+	GerritEventTypeHashTagsChanged     = "hashtags-changed"
+	GerritEventTypeProjectCreated      = "project-created"
+	GerritEventTypePatchsetCreated     = "patchset-created"
+	GerritEventTypeRefUpdated          = "ref-updated"
+	GerritEventTypeReviewerAdded       = "reviewer-added"
+	GerritEventTypeReviewerDelete      = "reviewer-deleted"
+	GerritEventTypeTopicChanged        = "topic-changed"
+	GerritEventTypeWIPStateChanged     = "wip-state-changed"
+	GerritEventTypePrivateStateChanged = "private-state-changed"
+	GerritEventTypeVoteDeleted         = "vote-deleted"
+
+	GerritEventsDefault = []string{
+		GerritEventTypePatchsetCreated,
+	}
+)
+
 // BuildNumberAndHash represents BuildNumber, Commit Hash and Branch for a Pipeline Build or Node Run
 type BuildNumberAndHash struct {
 	BuildNumber int64
diff --git a/sdk/worker_model.go b/sdk/worker_model.go
index fc6914d9e..21df10a65 100644
--- a/sdk/worker_model.go
+++ b/sdk/worker_model.go
@@ -42,7 +42,6 @@ type Model struct {
 	Name                string              `json:"name" db:"name" cli:"name,key"`
 	Description         string              `json:"description" db:"description" cli:"description"`
 	Type                string              `json:"type" db:"type" cli:"type"`
-	Image               string              `json:"image" db:"image" cli:"image"` // TODO: DELETE after migration done
 	Disabled            bool                `json:"disabled" db:"disabled" cli:"disabled"`
 	Restricted          bool                `json:"restricted" db:"restricted" cli:"restricted"`
 	RegisteredOS        *string             `json:"registered_os" db:"registered_os" cli:"-"`
diff --git a/sdk/workflow_hook.go b/sdk/workflow_hook.go
index c99374b21..2eb56808d 100644
--- a/sdk/workflow_hook.go
+++ b/sdk/workflow_hook.go
@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"reflect"
 	"sort"
+	"strings"
 )
 
 // Those are icon for hooks
@@ -28,6 +29,18 @@ type NodeHook struct {
 	Conditions    WorkflowNodeConditions `json:"conditions" db:"conditions"`
 }
 
+func (h NodeHook) IsRepositoryWebHook() bool {
+	return h.HookModelName == RepositoryWebHookModel.Name || h.HookModelID == RepositoryWebHookModel.ID
+}
+
+func (h NodeHook) GetConfigValue(k string) (string, bool) {
+	v, ok := h.Config[k]
+	if !ok {
+		return "", false
+	}
+	return v.Value, true
+}
+
 func (h NodeHook) Ref() string {
 	s := "model:" + h.HookModelName + ";"
 
@@ -45,8 +58,53 @@ func (h NodeHook) Ref() string {
 	return base64.StdEncoding.EncodeToString([]byte(s))
 }
 
+func (h NodeHook) ConfigValueContainsEventsDefault() bool {
+	eventFilterValue, has := h.GetConfigValue(HookConfigEventFilter)
+	if !has {
+		return false
+	}
+	eventFilterValues := strings.Split(eventFilterValue, ";")
+
+	allDefaultsValue := [][]string{
+		BitbucketCloudEventsDefault,
+		BitbucketEventsDefault,
+		GitHubEventsDefault,
+		GitlabEventsDefault,
+		GerritEventsDefault,
+	}
+
+	var atLeastOneFound bool
+	for _, defaultValues := range allDefaultsValue {
+		var allFound = true
+		for _, s := range defaultValues {
+			if !IsInArray(s, eventFilterValues) {
+				allFound = false
+				break
+			}
+		}
+		if allFound {
+			atLeastOneFound = true
+			break
+		}
+	}
+
+	return atLeastOneFound
+}
+
 //Equals checks functional equality between two hooks
 func (h NodeHook) Equals(h1 NodeHook) bool {
+	var areRepoWebHook = (h1.HookModelID == h.HookModelID) && (h.HookModelID == RepositoryWebHookModel.ID)
+	var isEventFilter = func(s string) bool { return s == HookConfigEventFilter }
+	var isEmptyEventFilter = func(s string) bool { return s == "" }
+	var isDefaultEventFilter = func(v string) bool {
+		return v == "" ||
+			v == strings.Join(BitbucketCloudEventsDefault, ";") ||
+			v == strings.Join(BitbucketEventsDefault, ";") ||
+			v == strings.Join(GitHubEventsDefault, ";") ||
+			v == strings.Join(GitlabEventsDefault, ";") ||
+			v == strings.Join(GerritEventsDefault, ";")
+	}
+
 	if h.UUID != h1.UUID {
 		return false
 	}
@@ -58,7 +116,11 @@ func (h NodeHook) Equals(h1 NodeHook) bool {
 		if !has {
 			return false
 		}
-		if cfg.Value != cfg1.Value {
+		if areRepoWebHook && isEventFilter(k) {
+			if isEmptyEventFilter(cfg.Value) && !isDefaultEventFilter(cfg1.Value) {
+				return false
+			}
+		} else if cfg.Value != cfg1.Value {
 			return false
 		}
 	}
@@ -67,7 +129,11 @@ func (h NodeHook) Equals(h1 NodeHook) bool {
 		if !has {
 			return false
 		}
-		if cfg.Value != cfg1.Value {
+		if areRepoWebHook && isEventFilter(k) {
+			if isEmptyEventFilter(cfg1.Value) && !isDefaultEventFilter(cfg.Value) {
+				return false
+			}
+		} else if cfg.Value != cfg1.Value {
 			return false
 		}
 	}
@@ -106,6 +172,35 @@ func (w *WorkflowNodeHookConfig) Scan(src interface{}) error {
 	return WrapError(json.Unmarshal(source, w), "cannot unmarshal WorkflowNodeHookConfig")
 }
 
+func (w WorkflowNodeHookConfig) Equals(o WorkflowNodeHookConfig) bool {
+	for k, v := range w {
+		ov, has := o[k]
+		if !has {
+			return false
+		}
+		if v.Value != ov.Value {
+			return false
+		}
+	}
+	return true
+}
+
+func (w WorkflowNodeHookConfig) MergeWith(cfg WorkflowNodeHookConfig) {
+	for k, v := range cfg {
+		w[k] = v
+	}
+}
+
+func (w WorkflowNodeHookConfig) Filter(f func(k string, v WorkflowNodeHookConfigValue) bool) WorkflowNodeHookConfig {
+	var newCfg = WorkflowNodeHookConfig{}
+	for k, v := range w {
+		if f(k, v) {
+			newCfg[k] = v
+		}
+	}
+	return newCfg.Clone()
+}
+
 // GetBuiltinHookModelByName retrieve the hook model
 func GetBuiltinHookModelByName(name string) *WorkflowHookModel {
 	for _, m := range BuiltinHookModels {
diff --git a/ui/Makefile b/ui/Makefile
index ca0c40be5..4ce14bfa1 100644
--- a/ui/Makefile
+++ b/ui/Makefile
@@ -34,3 +34,6 @@ build: $(FILES_UI) $(INDEX) ui.tar.gz
 
 ui.tar.gz:
 	tar cfz ui.tar.gz dist
+
+lintfix:
+	./node_modules/.bin/ng lint --fix
diff --git a/ui/package.json b/ui/package.json
index 9508ed0fa..fb667f47e 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -75,7 +75,7 @@
     "js-beautify": "1.8.9",
     "js-yaml": "3.13.1",
     "jsonschema": "1.2.5",
-    "jsrsasign": "8.0.12",
+    "jsrsasign": "8.0.20",
     "lodash-es": "4.17.15",
     "modernizr": "3.7.1",
     "moment": "2.24.0",
diff --git a/ui/src/app/shared/ascode/save-form/ascode.save-form.html b/ui/src/app/shared/ascode/save-form/ascode.save-form.html
index e2531398d..4b94a5959 100644
--- a/ui/src/app/shared/ascode/save-form/ascode.save-form.html
+++ b/ui/src/app/shared/ascode/save-form/ascode.save-form.html
@@ -22,7 +22,8 @@
         <div class="sixteen wide column">
             <div class="wide field">
                 <div class="ui message red" *ngIf="operation?.status > 2">
-                    {{operation.error}}
+                    {{operation.error_details.message}} - 
+                    {{operation.error_details.from}}
                 </div>
                 <div class="ui message green" *ngIf="operation?.status === 2">
                     {{ 'workflow_as_code_pr_success' | translate }}
diff --git a/ui/src/app/shared/ascode/save-modal/ascode.save-modal.component.ts b/ui/src/app/shared/ascode/save-modal/ascode.save-modal.component.ts
index 6cb69d4b9..90ff98d66 100644
--- a/ui/src/app/shared/ascode/save-modal/ascode.save-modal.component.ts
+++ b/ui/src/app/shared/ascode/save-modal/ascode.save-modal.component.ts
@@ -78,7 +78,7 @@ export class AsCodeSaveModalComponent {
                 this.loading = true;
                 this._cd.markForCheck();
                 this._workflowService.updateAsCode(this.project.key, this.name, this.parameters.branch_name,
-                    this.parameters.commit_message, this.dataToSave).subscribe(o => {
+                    this.parameters.commit_message, this.dataToSave as Workflow).subscribe(o => {
                         this.asCodeOperation = o;
                         this.startPollingOperation(this.name);
                     });
diff --git a/ui/src/app/store/applications.state.ts b/ui/src/app/store/applications.state.ts
index 799340cc2..62fa533cf 100644
--- a/ui/src/app/store/applications.state.ts
+++ b/ui/src/app/store/applications.state.ts
@@ -65,6 +65,8 @@ export class ApplicationsState {
             ctx.setState({
                 ...state,
                 currentProjectKey: action.payload.projectKey,
+                editMode: false,
+                editApplication: null,
                 application: app,
                 loading: false
             });
diff --git a/ui/src/app/store/pipelines.state.ts b/ui/src/app/store/pipelines.state.ts
index ae97d2fb7..d5dec5d48 100644
--- a/ui/src/app/store/pipelines.state.ts
+++ b/ui/src/app/store/pipelines.state.ts
@@ -56,6 +56,8 @@ export class PipelinesState {
             ctx.setState({
                 ...state,
                 currentProjectKey: action.payload.projectKey,
+                editPipeline: null,
+                editMode: false,
                 pipeline: pip,
                 loading: false,
             });
diff --git a/ui/src/app/store/project.action.ts b/ui/src/app/store/project.action.ts
index 11c46f7a7..ff1be1408 100644
--- a/ui/src/app/store/project.action.ts
+++ b/ui/src/app/store/project.action.ts
@@ -215,10 +215,6 @@ export class ResyncEnvironmentsInProject {
     static readonly type = '[Project] Resync Environments in Project';
     constructor(public payload: { projectKey: string }) { }
 }
-export class FetchEnvironmentsInProject {
-    static readonly type = '[Project] Fetch Environments in Project';
-    constructor(public payload: { projectKey: string }) { }
-}
 export class AddEnvironmentKey {
     static readonly type = '[Project] Add Environment Key in Project';
     constructor(public payload: { projectKey: string, envName: string, key: Key }) { }
diff --git a/ui/src/app/store/project.state.ts b/ui/src/app/store/project.state.ts
index 442e4a070..9795aa4bf 100644
--- a/ui/src/app/store/project.state.ts
+++ b/ui/src/app/store/project.state.ts
@@ -92,14 +92,7 @@ export class ProjectState {
             });
         }
 
-        return ctx.dispatch(new ProjectAction.ResyncProject(action.payload));
-    }
-
-    @Action(ProjectAction.ResyncProject)
-    resync(ctx: StateContext<ProjectStateModel>, action: ProjectAction.ResyncProject) {
-        let params = new HttpParams();
         let opts = action.payload.opts;
-
         if (Array.isArray(opts) && opts.length) {
             opts = opts.concat([
                 new LoadOpts('withGroups', 'groups'),
@@ -111,11 +104,18 @@ export class ProjectState {
                 new LoadOpts('withPermission', 'permission')
             ];
         }
+        opts.push(new LoadOpts('withIntegrations', 'integrations'));
         opts.push(new LoadOpts('withLabels', 'labels'));
+
+        return ctx.dispatch(new ProjectAction.ResyncProject({projectKey: action.payload.projectKey, opts: opts}));
+    }
+
+    @Action(ProjectAction.ResyncProject)
+    resync(ctx: StateContext<ProjectStateModel>, action: ProjectAction.ResyncProject) {
+        let params = new HttpParams();
+        let opts = action.payload.opts;
         opts.push(new LoadOpts('withFeatures', 'features'));
-        opts.push(new LoadOpts('withIntegrations', 'integrations'));
         opts.forEach((opt) => params = params.append(opt.queryParam, 'true'));
-
         const state = ctx.getState();
         ctx.setState({
             ...state,
@@ -856,9 +856,6 @@ export class ProjectState {
     fetchEnvironment(ctx: StateContext<ProjectStateModel>, action: ProjectAction.FetchEnvironmentInProject) {
         const state = ctx.getState();
 
-        if (state.currentProjectKey && state.currentProjectKey !== action.payload.projectKey) {
-            ctx.dispatch(new ProjectAction.FetchProject({ projectKey: action.payload.projectKey, opts: [] }));
-        }
         return this._envService.getEnvironment(action.payload.projectKey, action.payload.envName)
             .pipe(tap((environment: Environment) => {
                 let envs = state.project.environments;
@@ -931,21 +928,6 @@ export class ProjectState {
             }));
     }
 
-    @Action(ProjectAction.FetchEnvironmentsInProject)
-    fetchEnvironments(ctx: StateContext<ProjectStateModel>, action: ProjectAction.FetchEnvironmentsInProject) {
-        const state = ctx.getState();
-
-        if (state.currentProjectKey && state.currentProjectKey === action.payload.projectKey &&
-            state.project && state.project.key && state.project.environments) {
-            return ctx.dispatch(new ProjectAction.LoadProject(state.project));
-        }
-        if (state.currentProjectKey && state.currentProjectKey !== action.payload.projectKey) {
-            ctx.dispatch(new ProjectAction.FetchProject({ projectKey: action.payload.projectKey, opts: [] }));
-        }
-
-        return ctx.dispatch(new ProjectAction.ResyncEnvironmentsInProject(action.payload));
-    }
-
     @Action(ProjectAction.LoadEnvironmentsInProject)
     loadEnvironments(ctx: StateContext<ProjectStateModel>, action: ProjectAction.LoadEnvironmentsInProject) {
         const state = ctx.getState();
diff --git a/ui/src/app/views/home/filter/home.filter.component.ts b/ui/src/app/views/home/filter/home.filter.component.ts
index 869ba4ef2..07487ef08 100644
--- a/ui/src/app/views/home/filter/home.filter.component.ts
+++ b/ui/src/app/views/home/filter/home.filter.component.ts
@@ -7,7 +7,7 @@ import { ProjectStore } from 'app/service/project/project.store';
 import { TimelineStore } from 'app/service/timeline/timeline.store';
 import { AutoUnsubscribe } from 'app/shared/decorator/autoUnsubscribe';
 import { ToastService } from 'app/shared/toast/ToastService';
-import { ResyncProject } from 'app/store/project.action';
+import { FetchProject } from 'app/store/project.action';
 import { ProjectState, ProjectStateModel } from 'app/store/project.state';
 import cloneDeep from 'lodash-es/cloneDeep';
 import { finalize, flatMap } from 'rxjs/operators';
@@ -93,7 +93,7 @@ export class HomeFilterComponent {
         let opts = new Array<LoadOpts>();
         opts.push(new LoadOpts('withWorkflowNames', 'workflow_names'));
 
-        this.store.dispatch(new ResyncProject({
+        this.store.dispatch(new FetchProject({
             projectKey: projFilter.key,
             opts
         })).pipe(
diff --git a/ui/src/app/views/project/show/application/application.list.component.ts b/ui/src/app/views/project/show/application/application.list.component.ts
index 20061f5f7..96ef47fbf 100644
--- a/ui/src/app/views/project/show/application/application.list.component.ts
+++ b/ui/src/app/views/project/show/application/application.list.component.ts
@@ -1,5 +1,8 @@
-import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
-import { IdName, Project } from 'app/model/project.model';
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
+import { Store } from '@ngxs/store';
+import { IdName, LoadOpts, Project } from 'app/model/project.model';
+import { ResyncProject } from 'app/store/project.action';
+import { finalize } from 'rxjs/operators';
 
 @Component({
     selector: 'app-project-applications',
@@ -7,7 +10,7 @@ import { IdName, Project } from 'app/model/project.model';
     styleUrls: ['./application.list.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush
 })
-export class ProjectApplicationListComponent {
+export class ProjectApplicationListComponent implements OnInit {
 
     @Input()
     set project(project: Project) {
@@ -34,8 +37,19 @@ export class ProjectApplicationListComponent {
 
     _project: Project;
     _filter = '';
+    loading = true;
 
     filteredApplications: Array<IdName> = [];
 
-    constructor() { }
+    constructor(private store: Store, private _cd: ChangeDetectorRef) {}
+
+    ngOnInit(): void {
+        let opts: Array<LoadOpts> = [new LoadOpts('withApplicationNames', 'application_names')];
+        this.store.dispatch(new ResyncProject({ projectKey: this.project.key, opts: opts }))
+            .pipe(finalize(() => {
+                this.loading = false;
+                this._cd.markForCheck();
+            }))
+            .subscribe();
+    }
 }
diff --git a/ui/src/app/views/project/show/application/application.list.html b/ui/src/app/views/project/show/application/application.list.html
index 9f58f7338..360b40f59 100644
--- a/ui/src/app/views/project/show/application/application.list.html
+++ b/ui/src/app/views/project/show/application/application.list.html
@@ -1,4 +1,4 @@
-<div class="ui grid">
+<div class="ui grid" *ngIf="!loading">
     <div class="ui row">
       <div class="six wide column">
           <h3>{{ 'project_applications_list' | translate }}</h3>
@@ -38,3 +38,4 @@
       </div>
     </div>
 </div>
+<div class="ui text active loader" *ngIf="loading">{{'common_loading' | translate}}</div>
diff --git a/ui/src/app/views/project/show/environment/environment.list.component.ts b/ui/src/app/views/project/show/environment/environment.list.component.ts
index 115ed49ae..b29480d24 100644
--- a/ui/src/app/views/project/show/environment/environment.list.component.ts
+++ b/ui/src/app/views/project/show/environment/environment.list.component.ts
@@ -1,5 +1,8 @@
-import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
-import { IdName, Project } from 'app/model/project.model';
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
+import { Store } from '@ngxs/store';
+import { IdName, LoadOpts, Project } from 'app/model/project.model';
+import { ResyncProject } from 'app/store/project.action';
+import { finalize } from 'rxjs/operators';
 
 @Component({
     selector: 'app-project-environments',
@@ -7,7 +10,7 @@ import { IdName, Project } from 'app/model/project.model';
     styleUrls: ['./environment.list.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush
 })
-export class ProjectEnvironmentListComponent {
+export class ProjectEnvironmentListComponent implements OnInit {
 
     @Input()
     set project(project: Project) {
@@ -36,8 +39,17 @@ export class ProjectEnvironmentListComponent {
     _filter = '';
 
     filteredEnvironments: Array<IdName> = [];
+    loading = true;
 
-    constructor() {
+    constructor(private store: Store, private _cd: ChangeDetectorRef) {}
 
+    ngOnInit(): void {
+        let opts: Array<LoadOpts> = [new LoadOpts('withEnvironmentNames', 'environment_names')];
+        this.store.dispatch(new ResyncProject({ projectKey: this.project.key, opts: opts }))
+            .pipe(finalize(() => {
+                this.loading = false;
+                this._cd.markForCheck();
+            }))
+            .subscribe();
     }
 }
diff --git a/ui/src/app/views/project/show/environment/environment.list.html b/ui/src/app/views/project/show/environment/environment.list.html
index 2ebd3eb41..597c17a17 100644
--- a/ui/src/app/views/project/show/environment/environment.list.html
+++ b/ui/src/app/views/project/show/environment/environment.list.html
@@ -1,4 +1,4 @@
-<div class="ui grid">
+<div class="ui grid" *ngIf="!loading">
     <div class="ui row">
         <div class="six wide column">
             <h3>{{ 'project_env_list_title' | translate }}</h3>
@@ -38,3 +38,5 @@
         </div>
     </div>
 </div>
+<div class="ui text active loader" *ngIf="loading">{{'common_loading' | translate}}</div>
+
diff --git a/ui/src/app/views/project/show/pipeline/pipeline.list.component.ts b/ui/src/app/views/project/show/pipeline/pipeline.list.component.ts
index 2cafaf055..f28b2ff61 100644
--- a/ui/src/app/views/project/show/pipeline/pipeline.list.component.ts
+++ b/ui/src/app/views/project/show/pipeline/pipeline.list.component.ts
@@ -1,5 +1,8 @@
-import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
-import { IdName, Project } from 'app/model/project.model';
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
+import { Store } from '@ngxs/store';
+import { IdName, LoadOpts, Project } from 'app/model/project.model';
+import { ResyncProject } from 'app/store/project.action';
+import { finalize } from 'rxjs/operators';
 
 @Component({
     selector: 'app-project-pipelines',
@@ -7,37 +10,49 @@ import { IdName, Project } from 'app/model/project.model';
     styleUrls: ['./pipeline.list.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush
 })
-export class ProjectPipelinesComponent {
-
-  @Input()
-  set project(project: Project) {
-    this._project = project;
-    if (project.pipeline_names) {
-      let filter = this.filter.toLowerCase();
-      this.filteredPipelines = project.pipeline_names.filter((pip) => pip.name.toLowerCase().indexOf(filter) !== -1);
+export class ProjectPipelinesComponent implements OnInit {
+
+    @Input()
+    set project(project: Project) {
+        this._project = project;
+        if (project.pipeline_names) {
+            let filter = this.filter.toLowerCase();
+            this.filteredPipelines = project.pipeline_names.filter((pip) => pip.name.toLowerCase().indexOf(filter) !== -1);
+        }
     }
-  }
-  get project(): Project {
-    return this._project;
-  }
-
-  set filter(filter: string) {
-    this._filter = filter;
-    if (this.project.pipeline_names) {
-      let filterLower = filter.toLowerCase();
-      this.filteredPipelines = this.project.pipeline_names.filter((pip) => pip.name.toLowerCase().indexOf(filterLower) !== -1);
+
+    get project(): Project {
+        return this._project;
     }
-  }
-  get filter(): string {
-    return this._filter;
-  }
 
-  _project: Project;
-  _filter = '';
+    set filter(filter: string) {
+        this._filter = filter;
+        if (this.project.pipeline_names) {
+            let filterLower = filter.toLowerCase();
+            this.filteredPipelines = this.project.pipeline_names.filter((pip) => pip.name.toLowerCase().indexOf(filterLower) !== -1);
+        }
+    }
 
-  filteredPipelines: Array<IdName> = [];
+    get filter(): string {
+        return this._filter;
+    }
 
-    constructor() {
+    _project: Project;
+    _filter = '';
+
+    filteredPipelines: Array<IdName> = [];
+    loading = true;
+
+    constructor(private store: Store, private _cd: ChangeDetectorRef) {
+    }
 
+    ngOnInit(): void {
+        let opts: Array<LoadOpts> = [new LoadOpts('withPipelineNames', 'pipeline_names')];
+        this.store.dispatch(new ResyncProject({projectKey: this.project.key, opts: opts}))
+            .pipe(finalize(() => {
+                this.loading = false;
+                this._cd.markForCheck();
+            }))
+            .subscribe();
     }
 }
diff --git a/ui/src/app/views/project/show/pipeline/pipeline.list.html b/ui/src/app/views/project/show/pipeline/pipeline.list.html
index 1b59aae21..460bf4f4a 100644
--- a/ui/src/app/views/project/show/pipeline/pipeline.list.html
+++ b/ui/src/app/views/project/show/pipeline/pipeline.list.html
@@ -1,4 +1,4 @@
-<div class="ui grid">
+<div class="ui grid" *ngIf="!loading">
     <div class="ui row">
       <div class="six wide column">
           <h3>{{ 'project_pipelines_list' | translate }}</h3>
@@ -37,3 +37,4 @@
         </div>
       </div>
 </div>
+<div class="ui text active loader" *ngIf="loading">{{'common_loading' | translate}}</div>
diff --git a/ui/src/app/views/project/show/project.component.ts b/ui/src/app/views/project/show/project.component.ts
index c1d097a9a..2ffc3f751 100644
--- a/ui/src/app/views/project/show/project.component.ts
+++ b/ui/src/app/views/project/show/project.component.ts
@@ -149,28 +149,9 @@ export class ProjectShowComponent implements OnInit {
     }
 
     refreshDatas(key: string): void {
-        let opts = [
-            new LoadOpts('withApplicationNames', 'application_names'),
-            new LoadOpts('withPipelineNames', 'pipeline_names'),
-            new LoadOpts('withWorkflowNames', 'workflow_names'),
-            new LoadOpts('withEnvironmentNames', 'environment_names'),
-            new LoadOpts('withLabels', 'labels'),
-        ];
-
-        if (this.selectedTab) {
-            switch (this.selectedTab.key) {
-                case 'variables':
-                    opts.push(new LoadOpts('withVariables', 'variables'));
-                    break;
-                case 'permissions':
-                    opts.push(new LoadOpts('withEnvironments', 'environments'));
-                    break;
-            }
-        }
-
+        let opts = [new LoadOpts('withLabels', 'labels')];
         this._store.dispatch(new FetchProject({ projectKey: key, opts }))
             .subscribe(null, () => this._router.navigate(['/home']));
-
     }
 
     updateFav() {
diff --git a/ui/src/app/views/project/show/workflow/workflow.list.component.ts b/ui/src/app/views/project/show/workflow/workflow.list.component.ts
index 078287e34..5581ac342 100644
--- a/ui/src/app/views/project/show/workflow/workflow.list.component.ts
+++ b/ui/src/app/views/project/show/workflow/workflow.list.component.ts
@@ -1,7 +1,10 @@
-import { ChangeDetectionStrategy, Component, Input, OnInit, ViewChild } from '@angular/core';
-import { IdName, Label, Project } from 'app/model/project.model';
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit, ViewChild } from '@angular/core';
+import { Store } from '@ngxs/store';
+import { IdName, Label, LoadOpts, Project } from 'app/model/project.model';
 import { ProjectStore } from 'app/service/project/project.store';
 import { LabelsEditComponent } from 'app/shared/labels/edit/labels.edit.component';
+import { ResyncProject } from 'app/store/project.action';
+import { finalize } from 'rxjs/operators';
 
 @Component({
     selector: 'app-project-workflows',
@@ -11,73 +14,83 @@ import { LabelsEditComponent } from 'app/shared/labels/edit/labels.edit.componen
 })
 export class ProjectWorkflowListComponent implements OnInit {
 
-  @Input()
-  set project(project: Project) {
-    this._project = project;
-    if (project.workflow_names) {
-      let filter = this.filter.toLowerCase();
-      this.filteredWorkflows = project.workflow_names.filter((wf) => wf.name.toLowerCase().indexOf(filter) !== -1);
+    @Input()
+    set project(project: Project) {
+        this._project = project;
+        if (project.workflow_names) {
+            let filter = this.filter.toLowerCase();
+            this.filteredWorkflows = project.workflow_names.filter((wf) => wf.name.toLowerCase().indexOf(filter) !== -1);
+        }
+        if (project.labels) {
+            let labelFilter = this.labelFilter.toLowerCase();
+            this.filteredLabels = project.labels.filter((lbl) => lbl.name.toLowerCase().indexOf(labelFilter) !== -1);
+        }
     }
-    if (project.labels) {
-      let labelFilter = this.labelFilter.toLowerCase();
-      this.filteredLabels = project.labels.filter((lbl) => lbl.name.toLowerCase().indexOf(labelFilter) !== -1);
+
+    get project(): Project {
+        return this._project;
+    }
+
+    set filter(filter: string) {
+        this._filter = filter;
+        if (this.project.workflow_names) {
+            let filterLower = filter.toLowerCase();
+            this.filteredWorkflows = this.project.workflow_names.filter((wf) => wf.name.toLowerCase().indexOf(filterLower) !== -1);
+        }
+    }
+
+    get filter(): string {
+        return this._filter;
     }
-  }
-  get project(): Project {
-    return this._project;
-  }
 
-  set filter(filter: string) {
-    this._filter = filter;
-    if (this.project.workflow_names) {
-      let filterLower = filter.toLowerCase();
-      this.filteredWorkflows = this.project.workflow_names.filter((wf) => wf.name.toLowerCase().indexOf(filterLower) !== -1);
+    set labelFilter(filter: string) {
+        this._filterLabel = filter;
+        if (this.project.labels) {
+            let filterLower = filter.toLowerCase();
+            this.filteredLabels = this.project.labels.filter((lbl) => lbl.name.toLowerCase().indexOf(filterLower) !== -1);
+        }
     }
-  }
-  get filter(): string {
-    return this._filter;
-  }
 
-  set labelFilter(filter: string) {
-    this._filterLabel = filter;
-    if (this.project.labels) {
-      let filterLower = filter.toLowerCase();
-      this.filteredLabels = this.project.labels.filter((lbl) => lbl.name.toLowerCase().indexOf(filterLower) !== -1);
+    get labelFilter(): string {
+        return this._filterLabel;
     }
-  }
-  get labelFilter(): string {
-    return this._filterLabel;
-  }
 
-  // Modal
-  @ViewChild('projectLabels')
-  projectLabels: LabelsEditComponent;
+    // Modal
+    @ViewChild('projectLabels')
+    projectLabels: LabelsEditComponent;
 
-  _project: Project;
-  _filter = '';
-  _filterLabel = '';
+    _project: Project;
+    _filter = '';
+    _filterLabel = '';
 
-  viewMode: 'blocs'|'labels'|'lines' = 'blocs';
-  filteredWorkflows: Array<IdName> = [];
-  filteredLabels: Array<Label> = [];
-  loadingLabel = false;
+    viewMode: 'blocs' | 'labels' | 'lines' = 'blocs';
+    filteredWorkflows: Array<IdName> = [];
+    filteredLabels: Array<Label> = [];
+    loading = true;
 
-  constructor(private _projectStore: ProjectStore) {
 
-  }
+    constructor(private store: Store, private _cd: ChangeDetectorRef, private _projectStore: ProjectStore) {
+    }
 
-  ngOnInit() {
-    this.viewMode = this._projectStore.getWorkflowViewMode(this.project.key);
-  }
+    ngOnInit(): void {
+        this.viewMode = this._projectStore.getWorkflowViewMode(this.project.key);
+        let opts: Array<LoadOpts> = [new LoadOpts('withWorkflowNames', 'workflow_names')];
+        this.store.dispatch(new ResyncProject({projectKey: this.project.key, opts: opts}))
+            .pipe(finalize(() => {
+                this.loading = false;
+                this._cd.markForCheck();
+            }))
+            .subscribe();
+    }
 
-  editLabels() {
-    if (this.projectLabels && this.projectLabels.show) {
-      this.projectLabels.show();
+    editLabels() {
+        if (this.projectLabels && this.projectLabels.show) {
+            this.projectLabels.show();
+        }
     }
-  }
 
-  setViewMode(mode: 'blocs'|'labels'|'lines') {
-    this.viewMode = mode;
-    this._projectStore.setWorkflowViewMode(this.project.key, mode);
-  }
+    setViewMode(mode: 'blocs' | 'labels' | 'lines') {
+        this.viewMode = mode;
+        this._projectStore.setWorkflowViewMode(this.project.key, mode);
+    }
 }
diff --git a/ui/src/app/views/project/show/workflow/workflow.list.html b/ui/src/app/views/project/show/workflow/workflow.list.html
index 85cb0fa10..0aaf90ec3 100644
--- a/ui/src/app/views/project/show/workflow/workflow.list.html
+++ b/ui/src/app/views/project/show/workflow/workflow.list.html
@@ -1,4 +1,4 @@
-<div class="ui grid">
+<div class="ui grid" *ngIf="!loading">
     <div class="ui row">
         <div class="six wide column">
             <h3>{{ 'project_workflows_list' | translate }}</h3>
@@ -60,6 +60,7 @@
         </div>
     </div>
 </div>
+<div class="ui text active loader" *ngIf="loading">{{'common_loading' | translate}}</div>
 <ng-container *ngIf="project">
     <app-labels-edit
         [project]="project" #projectLabels>
diff --git a/ui/src/app/views/workflow/run/workflow.run.component.ts b/ui/src/app/views/workflow/run/workflow.run.component.ts
index c9907ab6e..d74239cab 100644
--- a/ui/src/app/views/workflow/run/workflow.run.component.ts
+++ b/ui/src/app/views/workflow/run/workflow.run.component.ts
@@ -1,12 +1,13 @@
 import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
 import { Title } from '@angular/platform-browser';
-import { ActivatedRoute } from '@angular/router';
+import { ActivatedRoute, Router } from '@angular/router';
 import { TranslateService } from '@ngx-translate/core';
 import { Select, Store } from '@ngxs/store';
 import { PipelineStatus, SpawnInfo } from 'app/model/pipeline.model';
 import { Project } from 'app/model/project.model';
 import { WorkflowRun } from 'app/model/workflow.run.model';
 import { NotificationService } from 'app/service/notification/notification.service';
+import { RouterService } from 'app/service/router/router.service';
 import { WorkflowStore } from 'app/service/workflow/workflow.store';
 import { AutoUnsubscribe } from 'app/shared/decorator/autoUnsubscribe';
 import { ProjectState } from 'app/store/project.state';
@@ -55,13 +56,20 @@ export class WorkflowRunComponent implements OnInit {
         private _notification: NotificationService,
         private _translate: TranslateService,
         private _titleService: Title,
-        private _cd: ChangeDetectorRef
+        private _cd: ChangeDetectorRef,
+        private _router: Router,
+        private _routerService: RouterService
     ) {
         this.project = this._store.selectSnapshot(ProjectState.projectSnapshot);
         this.workflowName = this._store.selectSnapshot(WorkflowState.workflowSnapshot).name;
         this._store.dispatch(new ChangeToRunView({}));
 
         this.paramsSub = this._activatedRoute.params.subscribe(p => {
+            let allParamsSnapshot = this._routerService.getRouteSnapshotParams({}, this._router.routerState.snapshot.root);
+            if (allParamsSnapshot['workflowName'] !== this.workflowName) {
+                // If workflow change, component will be destroy by parent
+                return;
+            }
             this.workflowRunData = {};
             this._cd.markForCheck();
             this._store.dispatch(
@@ -93,6 +101,12 @@ export class WorkflowRunComponent implements OnInit {
                 this.workflowName = this._store.selectSnapshot(WorkflowState.workflowSnapshot).name;
             }
 
+            if (this.workflowRunData['status'] && this.workflowRunData['status'] === 'Pending'
+                && this.workflowRunData['status'] !== wr.status) {
+                this.workflowRunData['workflow'] = wr.workflow;
+                this.workflowName = this._store.selectSnapshot(WorkflowState.workflowSnapshot).name;
+            }
+
             if (wr && this.workflowRunData['id'] && this.workflowRunData['id'] === wr.id
                 && this.workflowRunData['status'] !== wr.status && PipelineStatus.isDone(wr.status)) {
                 this.handleNotification(wr);
@@ -110,7 +124,6 @@ export class WorkflowRunComponent implements OnInit {
             this.workflowRunData['infos'] = wr.infos;
             this.workflowRunData['num'] = wr.num;
             this.workflowRunData['status'] = wr.status;
-
             this.updateTitle(wr);
             this._cd.markForCheck();
         });
diff --git a/ui/src/app/views/workflow/run/workflow.run.html b/ui/src/app/views/workflow/run/workflow.run.html
index 86399a1e7..4b5602798 100644
--- a/ui/src/app/views/workflow/run/workflow.run.html
+++ b/ui/src/app/views/workflow/run/workflow.run.html
@@ -1,5 +1,5 @@
 <div *ngIf="workflowRunData && workflowRunData['id']" class="workflowRun">
-    <app-workflow-run-summary [(direction)]="direction" [project]="project" [workflowName]="workflowName"
+    <app-workflow-run-summary [(direction)]="direction" [project]="project"
         [class.above]="workflowRunData['status'] === pipelineStatusEnum.PENDING">
     </app-workflow-run-summary>
     <ng-container *ngIf="workflowRunData['status'] !== pipelineStatusEnum.PENDING; else pendingBlock">


Test patch:
diff --git a/engine/api/authentication/session_test.go b/engine/api/authentication/session_test.go
index 15b8fcadb..7f24c79eb 100644
--- a/engine/api/authentication/session_test.go
+++ b/engine/api/authentication/session_test.go
@@ -43,9 +43,9 @@ func Test_CheckSessionJWT(t *testing.T) {
 }
 
 func Test_SessionCleaner(t *testing.T) {
-	ctx, cancel := context.WithTimeout(context.TODO(), 15*time.Second)
+	ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
 	defer cancel()
 	db, _ := test.SetupPG(t, bootstrap.InitiliazeDB)
 
-	authentication.SessionCleaner(ctx, func() *gorp.DbMap { return db })
+	authentication.SessionCleaner(ctx, func() *gorp.DbMap { return db }, 1*time.Second)
 }
diff --git a/engine/api/workflow_ascode_rename_test.go b/engine/api/workflow_ascode_rename_test.go
index 7a70b26d8..05a04d1ec 100644
--- a/engine/api/workflow_ascode_rename_test.go
+++ b/engine/api/workflow_ascode_rename_test.go
@@ -264,6 +264,11 @@ version: v1.0`),
 			func(ctx context.Context, method, path string, in interface{}, out interface{}, _ interface{}) (http.Header, int, error) {
 				vcsHooks := in.(*sdk.VCSHook)
 				vcsHooks.ID = sdk.UUID()
+				require.Len(t, vcsHooks.Events, 0, "events list should be empty, default value is set by vcs")
+
+				vcsHooks.Events = []string{
+					"push",
+				}
 				*(out.(*sdk.VCSHook)) = *vcsHooks
 				return nil, 200, nil
 			},
@@ -296,6 +301,20 @@ version: v1.0`),
 	assert.NoError(t, err)
 	assert.NotNil(t, wk)
 
+	require.Len(t, wk.WorkflowData.GetHooks(), 1)
+
+	for _, h := range wk.WorkflowData.GetHooks() {
+		log.Debug("--> %T %+v", h, h)
+		require.Equal(t, "RepositoryWebHook", h.HookModelName)
+		require.Equal(t, "push", h.Config["eventFilter"].Value)
+		require.Equal(t, "Github", h.Config["hookIcon"].Value)
+		require.Equal(t, "POST", h.Config["method"].Value)
+		require.Equal(t, proj.Key, h.Config["project"].Value)
+		require.Equal(t, "fsamin/go-repo", h.Config["repoFullName"].Value)
+		require.Equal(t, "github", h.Config["vcsServer"].Value)
+		require.Equal(t, wk.Name, h.Config["workflow"].Value)
+	}
+
 	// Then we will trigger a run of the workflow wich should trigger an as-code operation with a renamed workflow
 	vars := map[string]string{
 		"key":              proj.Key,
@@ -343,13 +362,13 @@ version: v1.0`),
 
 	for _, h := range wk.WorkflowData.GetHooks() {
 		log.Debug("--> %T %+v", h, h)
-		assert.Equal(t, "RepositoryWebHook", h.HookModelName)
-		assert.Equal(t, "push", h.Config["eventFilter"].Value)
-		assert.Equal(t, "Github", h.Config["hookIcon"].Value)
-		assert.Equal(t, "POST", h.Config["method"].Value)
-		assert.Equal(t, proj.Key, h.Config["project"].Value)
-		assert.Equal(t, "fsamin/go-repo", h.Config["repoFullName"].Value)
-		assert.Equal(t, "github", h.Config["vcsServer"].Value)
-		assert.Equal(t, wk.Name, h.Config["workflow"].Value)
+		require.Equal(t, "RepositoryWebHook", h.HookModelName)
+		require.Equal(t, "push", h.Config["eventFilter"].Value)
+		require.Equal(t, "Github", h.Config["hookIcon"].Value)
+		require.Equal(t, "POST", h.Config["method"].Value)
+		require.Equal(t, proj.Key, h.Config["project"].Value)
+		require.Equal(t, "fsamin/go-repo", h.Config["repoFullName"].Value)
+		require.Equal(t, "github", h.Config["vcsServer"].Value)
+		require.Equal(t, wk.Name, h.Config["workflow"].Value)
 	}
 }
diff --git a/engine/api/workflow_import_test.go b/engine/api/workflow_import_test.go
index 96f96e16f..7b929c653 100644
--- a/engine/api/workflow_import_test.go
+++ b/engine/api/workflow_import_test.go
@@ -5,6 +5,7 @@ import (
 	"context"
 	"io/ioutil"
 	"net/http/httptest"
+	"sort"
 	"strings"
 	"testing"
 
@@ -914,3 +915,136 @@ metadata:
 	t.Logf("%+v", wUpdated.WorkflowData)
 	assert.Equal(t, 1, len(wUpdated.WorkflowData.Joins))
 }
+
+func Test_postWorkflowImportHandler_editPermissions(t *testing.T) {
+	api, db, _ := newTestAPI(t)
+
+	u, pass := assets.InsertAdminUser(t, db)
+	g1 := assets.InsertTestGroup(t, db, "b-"+sdk.RandomString(10))
+	g2 := assets.InsertTestGroup(t, db, "c-"+sdk.RandomString(10))
+
+	proj := assets.InsertTestProject(t, db, api.Cache, sdk.RandomString(10), "a-"+sdk.RandomString(10))
+	require.NoError(t, group.InsertLinkGroupProject(context.TODO(), db, &group.LinkGroupProject{
+		GroupID:   g1.ID,
+		ProjectID: proj.ID,
+		Role:      sdk.PermissionReadExecute,
+	}))
+	require.NoError(t, group.InsertLinkGroupProject(context.TODO(), db, &group.LinkGroupProject{
+		GroupID:   g2.ID,
+		ProjectID: proj.ID,
+		Role:      sdk.PermissionReadExecute,
+	}))
+
+	pip := sdk.Pipeline{
+		ProjectID:  proj.ID,
+		ProjectKey: proj.Key,
+		Name:       "pip1",
+	}
+	require.NoError(t, pipeline.InsertPipeline(db, &pip))
+
+	uri := api.Router.GetRoute("POST", api.postWorkflowImportHandler, map[string]string{
+		"permProjectKey": proj.Key,
+	})
+	req := assets.NewAuthentifiedRequest(t, u, pass, "POST", uri, nil)
+
+	body := `name: test_1
+version: v2.0
+workflow:
+  pip1:
+    pipeline: pip1`
+	req.Body = ioutil.NopCloser(strings.NewReader(body))
+	req.Header.Set("Content-Type", "application/x-yaml")
+
+	rec := httptest.NewRecorder()
+	api.Router.Mux.ServeHTTP(rec, req)
+	require.Equal(t, 200, rec.Code)
+	t.Logf(">>%s", rec.Body.String())
+
+	w, err := workflow.Load(context.TODO(), db, api.Cache, *proj, "test_1", workflow.LoadOptions{})
+	require.NoError(t, err)
+
+	// Workflow permissions should be inherited from project
+	require.Len(t, w.Groups, 3)
+	sort.Slice(w.Groups, func(i, j int) bool {
+		return w.Groups[i].Group.Name < w.Groups[j].Group.Name
+	})
+	assert.Equal(t, proj.ProjectGroups[0].Group.Name, w.Groups[0].Group.Name)
+	assert.Equal(t, sdk.PermissionReadWriteExecute, w.Groups[0].Permission)
+	assert.Equal(t, g1.Name, w.Groups[1].Group.Name)
+	assert.Equal(t, sdk.PermissionReadExecute, w.Groups[1].Permission)
+	assert.Equal(t, g2.Name, w.Groups[2].Group.Name)
+	assert.Equal(t, sdk.PermissionReadExecute, w.Groups[2].Permission)
+
+	// We want to change to permisison for g2 and remove the permission for g1
+	uri = api.Router.GetRoute("POST", api.postWorkflowImportHandler, map[string]string{
+		"permProjectKey": proj.Key,
+	})
+	req = assets.NewAuthentifiedRequest(t, u, pass, "POST", uri, nil)
+	q := req.URL.Query()
+	q.Set("force", "true")
+	req.URL.RawQuery = q.Encode()
+
+	body = `name: test_1
+version: v2.0
+workflow:
+  pip1:
+    pipeline: pip1
+permissions:
+  ` + proj.ProjectGroups[0].Group.Name + `: 7
+  ` + g2.Name + `: 4`
+	req.Body = ioutil.NopCloser(strings.NewReader(body))
+	req.Header.Set("Content-Type", "application/x-yaml")
+
+	rec = httptest.NewRecorder()
+	api.Router.Mux.ServeHTTP(rec, req)
+	require.Equal(t, 200, rec.Code)
+	t.Logf(">>%s", rec.Body.String())
+
+	w, err = workflow.Load(context.TODO(), db, api.Cache, *proj, "test_1", workflow.LoadOptions{})
+	require.NoError(t, err)
+
+	require.Len(t, w.Groups, 2)
+	sort.Slice(w.Groups, func(i, j int) bool {
+		return w.Groups[i].Group.Name < w.Groups[j].Group.Name
+	})
+	assert.Equal(t, proj.ProjectGroups[0].Group.Name, w.Groups[0].Group.Name)
+	assert.Equal(t, sdk.PermissionReadWriteExecute, w.Groups[0].Permission)
+	assert.Equal(t, g2.Name, w.Groups[1].Group.Name)
+	assert.Equal(t, sdk.PermissionRead, w.Groups[1].Permission)
+
+	// Import again the workflow without permissions should reset to project permissions
+	uri = api.Router.GetRoute("POST", api.postWorkflowImportHandler, map[string]string{
+		"permProjectKey": proj.Key,
+	})
+	req = assets.NewAuthentifiedRequest(t, u, pass, "POST", uri, nil)
+	q = req.URL.Query()
+	q.Set("force", "true")
+	req.URL.RawQuery = q.Encode()
+
+	body = `name: test_1
+version: v2.0
+workflow:
+  pip1:
+    pipeline: pip1`
+	req.Body = ioutil.NopCloser(strings.NewReader(body))
+	req.Header.Set("Content-Type", "application/x-yaml")
+
+	rec = httptest.NewRecorder()
+	api.Router.Mux.ServeHTTP(rec, req)
+	require.Equal(t, 200, rec.Code)
+	t.Logf(">>%s", rec.Body.String())
+
+	w, err = workflow.Load(context.TODO(), db, api.Cache, *proj, "test_1", workflow.LoadOptions{})
+	require.NoError(t, err)
+
+	require.Len(t, w.Groups, 3)
+	sort.Slice(w.Groups, func(i, j int) bool {
+		return w.Groups[i].Group.Name < w.Groups[j].Group.Name
+	})
+	assert.Equal(t, proj.ProjectGroups[0].Group.Name, w.Groups[0].Group.Name)
+	assert.Equal(t, sdk.PermissionReadWriteExecute, w.Groups[0].Permission)
+	assert.Equal(t, g1.Name, w.Groups[1].Group.Name)
+	assert.Equal(t, sdk.PermissionReadExecute, w.Groups[1].Permission)
+	assert.Equal(t, g2.Name, w.Groups[2].Group.Name)
+	assert.Equal(t, sdk.PermissionReadExecute, w.Groups[2].Permission)
+}
diff --git a/engine/cdn/cdn_log_test.go b/engine/cdn/cdn_log_test.go
index fcb3d8565..334808025 100644
--- a/engine/cdn/cdn_log_test.go
+++ b/engine/cdn/cdn_log_test.go
@@ -50,6 +50,7 @@ func TestWorkerLog(t *testing.T) {
 			StepOrder: 0,
 		},
 		JobID:     dbj.ID,
+		NodeRunID: jobRun.WorkflowNodeRunID,
 		Timestamp: time.Now().UnixNano(),
 	}
 	logCache.Set(fmt.Sprintf("worker-%s", signature.Worker.WorkerID), sdk.Worker{
@@ -64,7 +65,11 @@ func TestWorkerLog(t *testing.T) {
 	"host": "host", "_line":1, "_pid": 1, "_prefix": "prefix", "full_message": "this is my message", "_Signature": "%s"}`
 	message = fmt.Sprintf(message, signatureField)
 
-	require.NoError(t, s.handleLogMessage(context.TODO(), []byte(message)))
+	chanMessages := s.handleConnectionChannel(context.TODO())
+	require.NoError(t, s.handleLogMessage(context.TODO(), chanMessages, []byte(message)))
+	close(chanMessages)
+
+	time.Sleep(100 * time.Millisecond)
 
 	logs, err := workflow.LoadLogs(s.Db, dbj.ID)
 	require.NoError(t, err)
@@ -108,6 +113,7 @@ func TestServiceLog(t *testing.T) {
 			RequirementName: "service-1",
 		},
 		JobID:     dbj.ID,
+		NodeRunID: jobRun.WorkflowNodeRunID,
 		Timestamp: time.Now().UnixNano(),
 	}
 
@@ -121,9 +127,12 @@ func TestServiceLog(t *testing.T) {
 	"host": "host", "_line":1, "_pid": 1, "_prefix": "prefix", "full_message": "this is my service message", "_Signature": "%s"}`
 	message = fmt.Sprintf(message, signatureField)
 
-	require.NoError(t, s.handleLogMessage(context.TODO(), []byte(message)))
+	chanMessages := s.handleConnectionChannel(context.TODO())
+	require.NoError(t, s.handleLogMessage(context.TODO(), chanMessages, []byte(message)))
+	close(chanMessages)
 
 	logs, err := workflow.LoadServiceLog(db, dbj.ID, signature.Service.RequirementName)
 	require.NoError(t, err)
 	require.Equal(t, "this is my service message\n", logs.Val)
+
 }
diff --git a/engine/hatchery/swarm/swarm_util_create_test.go b/engine/hatchery/swarm/swarm_util_create_test.go
index 00d464dd1..823ab3320 100644
--- a/engine/hatchery/swarm/swarm_util_create_test.go
+++ b/engine/hatchery/swarm/swarm_util_create_test.go
@@ -149,9 +149,7 @@ func TestHatcherySwarm_createAndStartContainer(t *testing.T) {
 	// RegisterOnly = true, this will pull image if image is not found
 	spawnArgs := hatchery.SpawnArguments{
 		RegisterOnly: true,
-		Model: &sdk.Model{
-			Image: args.image,
-		},
+		Model:        &sdk.Model{},
 	}
 	err := h.createAndStartContainer(context.TODO(), h.dockerClients["default"], args, spawnArgs)
 	require.NoError(t, err)
@@ -195,9 +193,7 @@ func TestHatcherySwarm_createAndStartContainerWithMount(t *testing.T) {
 
 	spawnArgs := hatchery.SpawnArguments{
 		RegisterOnly: false,
-		Model: &sdk.Model{
-			Image: args.image,
-		},
+		Model:        &sdk.Model{},
 	}
 	err = h.createAndStartContainer(context.TODO(), h.dockerClients["default"], args, spawnArgs)
 	require.NoError(t, err)
@@ -230,9 +226,7 @@ func TestHatcherySwarm_createAndStartContainerWithNetwork(t *testing.T) {
 
 	spawnArgs := hatchery.SpawnArguments{
 		RegisterOnly: false,
-		Model: &sdk.Model{
-			Image: args.image,
-		},
+		Model:        &sdk.Model{},
 	}
 	err = h.createAndStartContainer(context.TODO(), h.dockerClients["default"], args, spawnArgs)
 	require.NoError(t, err)
diff --git a/engine/hooks/tasks_test.go b/engine/hooks/tasks_test.go
index 41373da7b..afd5be77b 100644
--- a/engine/hooks/tasks_test.go
+++ b/engine/hooks/tasks_test.go
@@ -78,14 +78,14 @@ func Test_dequeueTaskExecutions_ScheduledTask(t *testing.T) {
 	s, cancel := setupTestHookService(t)
 	defer cancel()
 
-	ctx, cancel := context.WithTimeout(context.TODO(), 65*time.Second)
+	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
 	defer cancel()
 
 	// Get the mock
 	m := s.Client.(*mock_cdsclient.MockInterface)
 
 	// Mock the sync of tasks
-	// It will remove all the tascks from the database
+	// It will remove all the tasks from the database
 	m.EXPECT().WorkflowAllHooksList().Return([]sdk.NodeHook{}, nil)
 	m.EXPECT().VCSConfiguration().Return(nil, nil).AnyTimes()
 	require.NoError(t, s.synchronizeTasks(ctx))
diff --git a/engine/hooks/test.go b/engine/hooks/test.go
index b9745ff46..6e523434f 100644
--- a/engine/hooks/test.go
+++ b/engine/hooks/test.go
@@ -30,11 +30,11 @@ func setupTestHookService(t *testing.T) (Service, func()) {
 	ctrl := gomock.NewController(t)
 	s.Client = mock_cdsclient.NewMockInterface(ctrl)
 
-	cancel := func() {
+	t.Cleanup(func() {
 		store.Client.Close()
 		store.Client = nil
 		ctrl.Finish()
-	}
+	})
 
-	return s, cancel
+	return s, func() {}
 }
diff --git a/engine/worker/internal/action/builtin_artifact_download_test.go b/engine/worker/internal/action/builtin_artifact_download_test.go
index 27b85a4e2..882f74aef 100644
--- a/engine/worker/internal/action/builtin_artifact_download_test.go
+++ b/engine/worker/internal/action/builtin_artifact_download_test.go
@@ -19,12 +19,12 @@ func TestRunArtifactDownload(t *testing.T) {
 	wk, ctx := SetupTest(t)
 
 	as := []sdk.WorkflowNodeRunArtifact{
-		sdk.WorkflowNodeRunArtifact{
+		{
 			ID:   1,
 			Name: "myFile.txt",
 			Tag:  "999",
 		},
-		sdk.WorkflowNodeRunArtifact{
+		{
 			ID:   2,
 			Name: "myFile.csv",
 			Tag:  "999",
@@ -90,7 +90,7 @@ func TestRunArtifactDownloadOutsideWorkspace(t *testing.T) {
 	fileName := sdk.RandomString(10)
 
 	as := []sdk.WorkflowNodeRunArtifact{
-		sdk.WorkflowNodeRunArtifact{
+		{
 			ID:   1,
 			Name: fileName,
 			Tag:  "999",
diff --git a/sdk/workflow_hook_test.go b/sdk/workflow_hook_test.go
new file mode 100644
index 000000000..6643f8f77
--- /dev/null
+++ b/sdk/workflow_hook_test.go
@@ -0,0 +1,62 @@
+package sdk
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestNodeHook_ConfigValueContainsEventsDefault(t *testing.T) {
+	var tests = []struct {
+		given    []string
+		expected bool
+	}{
+		{
+			given:    BitbucketCloudEventsDefault,
+			expected: true,
+		},
+		{
+			given:    BitbucketCloudEventsDefault,
+			expected: true,
+		},
+		{
+			given:    GitHubEventsDefault,
+			expected: true,
+		},
+		{
+			given:    GitlabEventsDefault,
+			expected: true,
+		},
+		{
+			given:    GerritEventsDefault,
+			expected: true,
+		},
+		{
+			given:    GitHubEvents,
+			expected: true,
+		},
+		{
+			given:    []string{"foo", "bar"},
+			expected: false,
+		},
+		{
+			given:    []string{"push", "bar"}, // push is the default events for github
+			expected: true,
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("test #%d with %+v", i, tt.given), func(t *testing.T) {
+			h := &NodeHook{
+				Config: WorkflowNodeHookConfig{
+					HookConfigEventFilter: WorkflowNodeHookConfigValue{
+						Value: strings.Join(tt.given, ";"),
+					},
+				},
+			}
+			require.Equal(t, tt.expected, h.ConfigValueContainsEventsDefault())
+		})
+	}
+}