Repository: ovh/cds
Pull request #4890 (2020-01-15 10:12:52)

fix(ui): remove extra pointing class from the banner element
Signed-off-by: Antoine Leblanc <antoine.leblanc@corp.ovh.com>

1. Description
A banner is displayed with a the following helper class `pointing` but
the element isn't clickable.

That's why I suggest to remove this extra helper class.
1. Related issues
None.
1. About tests
None.
1. Mentions
// @marie-j 

@ovh/cds



Patch:
diff --git a/cli/cdsctl/project.go b/cli/cdsctl/project.go
index 2dd3120b8..d71dd6ec4 100644
--- a/cli/cdsctl/project.go
+++ b/cli/cdsctl/project.go
@@ -158,9 +158,9 @@ func projectDeleteRun(v cli.Values) error {
 	projKey := v.GetString(_ProjectKey)
 	if v.GetBool("force") {
 		// Delete all workflow
-		ws, errW := client.WorkflowList(projKey)
-		if errW != nil && !sdk.ErrorIs(errW, sdk.ErrNoProject) {
-			return errW
+		ws, err := client.WorkflowList(projKey)
+		if err != nil && !sdk.ErrorIs(err, sdk.ErrNoProject) {
+			return err
 		}
 		for _, w := range ws {
 			if err := client.WorkflowDelete(projKey, w.Name); err != nil && !sdk.ErrorIs(err, sdk.ErrNoProject) {
@@ -169,9 +169,9 @@ func projectDeleteRun(v cli.Values) error {
 		}
 
 		// Delete all apps
-		apps, errA := client.ApplicationList(projKey)
-		if errA != nil && !sdk.ErrorIs(errA, sdk.ErrNoProject) {
-			return errA
+		apps, err := client.ApplicationList(projKey)
+		if err != nil && !sdk.ErrorIs(err, sdk.ErrNoProject) {
+			return err
 		}
 		for _, app := range apps {
 			if err := client.ApplicationDelete(projKey, app.Name); err != nil && !sdk.ErrorIs(err, sdk.ErrNoProject) {
@@ -180,9 +180,9 @@ func projectDeleteRun(v cli.Values) error {
 		}
 
 		// Delete all pipelines
-		pips, errP := client.PipelineList(projKey)
-		if errP != nil && !sdk.ErrorIs(errP, sdk.ErrNoProject) {
-			return errP
+		pips, err := client.PipelineList(projKey)
+		if err != nil && !sdk.ErrorIs(err, sdk.ErrNoProject) {
+			return err
 		}
 		for _, pip := range pips {
 			if err := client.PipelineDelete(projKey, pip.Name); err != nil && !sdk.ErrorIs(err, sdk.ErrNoProject) {
@@ -191,9 +191,9 @@ func projectDeleteRun(v cli.Values) error {
 		}
 
 		// Delete all environments
-		envs, errE := client.EnvironmentList(projKey)
-		if errE != nil && !sdk.ErrorIs(errE, sdk.ErrNoProject) {
-			return errE
+		envs, err := client.EnvironmentList(projKey)
+		if err != nil && !sdk.ErrorIs(err, sdk.ErrNoProject) {
+			return err
 		}
 		for _, env := range envs {
 			if err := client.EnvironmentDelete(projKey, env.Name); err != nil && !sdk.ErrorIs(err, sdk.ErrNoProject) {
diff --git a/contrib/grpcplugins/action/archive/go.sum b/contrib/grpcplugins/action/archive/go.sum
index 3cff476b0..3665e8465 100644
--- a/contrib/grpcplugins/action/archive/go.sum
+++ b/contrib/grpcplugins/action/archive/go.sum
@@ -518,6 +518,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo=
 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
diff --git a/contrib/grpcplugins/action/clair/go.sum b/contrib/grpcplugins/action/clair/go.sum
index 63861b256..bde494c9d 100644
--- a/contrib/grpcplugins/action/clair/go.sum
+++ b/contrib/grpcplugins/action/clair/go.sum
@@ -517,6 +517,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo=
 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
diff --git a/contrib/grpcplugins/action/download/go.sum b/contrib/grpcplugins/action/download/go.sum
index 7eaa366b9..ff44e856e 100644
--- a/contrib/grpcplugins/action/download/go.sum
+++ b/contrib/grpcplugins/action/download/go.sum
@@ -500,6 +500,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo=
 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
diff --git a/contrib/grpcplugins/action/group-tmpl/go.sum b/contrib/grpcplugins/action/group-tmpl/go.sum
index 7eaa366b9..ff44e856e 100644
--- a/contrib/grpcplugins/action/group-tmpl/go.sum
+++ b/contrib/grpcplugins/action/group-tmpl/go.sum
@@ -500,6 +500,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo=
 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
diff --git a/contrib/grpcplugins/action/kafka-publish/go.sum b/contrib/grpcplugins/action/kafka-publish/go.sum
index 31df55686..dfdae839f 100644
--- a/contrib/grpcplugins/action/kafka-publish/go.sum
+++ b/contrib/grpcplugins/action/kafka-publish/go.sum
@@ -649,6 +649,7 @@ golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/Le
 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
diff --git a/contrib/grpcplugins/action/marathon/go.sum b/contrib/grpcplugins/action/marathon/go.sum
index 117cf1fc6..3d7ae52c9 100644
--- a/contrib/grpcplugins/action/marathon/go.sum
+++ b/contrib/grpcplugins/action/marathon/go.sum
@@ -644,6 +644,7 @@ golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/Le
 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
diff --git a/contrib/grpcplugins/action/npm-audit-parser/go.sum b/contrib/grpcplugins/action/npm-audit-parser/go.sum
index 7eaa366b9..ff44e856e 100644
--- a/contrib/grpcplugins/action/npm-audit-parser/go.sum
+++ b/contrib/grpcplugins/action/npm-audit-parser/go.sum
@@ -500,6 +500,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo=
 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
diff --git a/contrib/grpcplugins/action/ssh-cmd/go.sum b/contrib/grpcplugins/action/ssh-cmd/go.sum
index 30f3075af..8d787a2ec 100644
--- a/contrib/grpcplugins/action/ssh-cmd/go.sum
+++ b/contrib/grpcplugins/action/ssh-cmd/go.sum
@@ -499,6 +499,7 @@ golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnf
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c h1:/nJuwDLoL/zrqY6gf57vxC+Pi+pZ8bfhpPkicO5H7W4=
 golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
diff --git a/contrib/grpcplugins/action/tmpl/go.sum b/contrib/grpcplugins/action/tmpl/go.sum
index 7eaa366b9..ff44e856e 100644
--- a/contrib/grpcplugins/action/tmpl/go.sum
+++ b/contrib/grpcplugins/action/tmpl/go.sum
@@ -500,6 +500,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo=
 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
diff --git a/contrib/grpcplugins/action/venom/go.sum b/contrib/grpcplugins/action/venom/go.sum
index cd124e1b1..82b3727c2 100644
--- a/contrib/grpcplugins/action/venom/go.sum
+++ b/contrib/grpcplugins/action/venom/go.sum
@@ -541,6 +541,7 @@ golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/Le
 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
diff --git a/engine/api/action.go b/engine/api/action.go
index 72e7159dc..6b4c415b0 100644
--- a/engine/api/action.go
+++ b/engine/api/action.go
@@ -25,7 +25,7 @@ func (api *API) getActionsHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 		var as []sdk.Action
 		var err error
-		if isMaintainer(ctx) || isAdmin(ctx) {
+		if isMaintainer(ctx) {
 			as, err = action.LoadAllByTypes(ctx, api.mustDB(),
 				[]string{sdk.DefaultAction},
 				action.LoadOptions.WithRequirements,
@@ -91,7 +91,7 @@ func (api *API) getActionsForGroupHandler() service.Handler {
 		}
 
 		// and user is part of the group
-		if !isGroupMember(ctx, g) && !isMaintainer(ctx) && !isAdmin(ctx) {
+		if !isGroupMember(ctx, g) && !isMaintainer(ctx) {
 			return sdk.WithStack(sdk.ErrForbidden)
 		}
 
@@ -119,14 +119,10 @@ func (api *API) postActionHandler() service.Handler {
 			return err
 		}
 
-		// check that the group exists and user is admin for group id
-		grp, err := group.LoadByID(ctx, api.mustDB(), *data.GroupID)
+		grp, err := group.LoadByID(ctx, api.mustDB(), *data.GroupID, group.LoadOptions.WithMembers)
 		if err != nil {
 			return err
 		}
-		if grp == nil {
-			return sdk.WithStack(sdk.ErrNotFound)
-		}
 
 		if !isGroupAdmin(ctx, grp) && !isAdmin(ctx) {
 			return sdk.WithStack(sdk.ErrInvalidGroupAdmin)
@@ -189,7 +185,7 @@ func (api *API) getActionHandler() service.Handler {
 		groupName := vars["permGroupName"]
 		actionName := vars["permActionName"]
 
-		g, err := group.LoadByName(ctx, api.mustDB(), groupName)
+		g, err := group.LoadByName(ctx, api.mustDB(), groupName, group.LoadOptions.WithMembers)
 		if err != nil {
 			return err
 		}
@@ -247,13 +243,10 @@ func (api *API) putActionHandler() service.Handler {
 		}
 		defer tx.Rollback() // nolint
 
-		grp, err := group.LoadByID(ctx, tx, *data.GroupID)
+		grp, err := group.LoadByID(ctx, tx, *data.GroupID, group.LoadOptions.WithMembers)
 		if err != nil {
 			return err
 		}
-		if grp == nil {
-			return sdk.WithStack(sdk.ErrNotFound)
-		}
 
 		if *old.GroupID != *data.GroupID || old.Name != data.Name {
 			if !isGroupAdmin(ctx, grp) && !isAdmin(ctx) {
@@ -477,7 +470,7 @@ func (api *API) postActionAuditRollbackHandler() service.Handler {
 		} else if ea.Group == grp.Name {
 			newGrp = grp
 		} else {
-			newGrp, err = group.LoadByName(ctx, tx, ea.Group)
+			newGrp, err = group.LoadByName(ctx, tx, ea.Group, group.LoadOptions.WithMembers)
 			if err != nil {
 				return err
 			}
diff --git a/engine/api/api.go b/engine/api/api.go
index 860abd059..bed08c183 100644
--- a/engine/api/api.go
+++ b/engine/api/api.go
@@ -111,13 +111,13 @@ type Configuration struct {
 			RedirectMethod string `json:"redirect_method" toml:"redirectMethod"`
 			RedirectURL    string `json:"redirect_url" toml:"redirectURL"`
 			Keys           struct {
-				RequestSigningKey  string `json:"request_signing_key" toml:"requestSigningKey"`
-				TokenSigningKey    string `json:"token_signing_key" toml:"tokenSigningKey"`
+				RequestSigningKey  string `json:"-" toml:"requestSigningKey"`
+				TokenSigningKey    string `json:"-" toml:"tokenSigningKey"`
 				TokenKeySigningKey struct {
 					KeySigningKey   string `json:"public_signing_key" toml:"keySigningKey"`
 					SigningKeyClaim string `json:"signing_key_claim" toml:"signingKeyClaim"`
-				} `json:"key_signing_key" toml:"tokenKeySigningKey"`
-			} `json:"keys" toml:"keys"`
+				} `json:"-" toml:"tokenKeySigningKey"`
+			} `json:"-" toml:"keys"`
 		} `json:"corporate_sso" toml:"corporateSSO"`
 		Github struct {
 			Enabled        bool   `toml:"enabled" default:"false" json:"enabled"`
diff --git a/engine/api/authentication/consumer.go b/engine/api/authentication/consumer.go
index db452d053..93ee22b32 100644
--- a/engine/api/authentication/consumer.go
+++ b/engine/api/authentication/consumer.go
@@ -20,6 +20,7 @@ func NewConsumerWorker(ctx context.Context, db gorp.SqlExecutor, name string, ha
 		Scopes: []sdk.AuthConsumerScope{
 			sdk.AuthConsumerScopeWorker,
 			sdk.AuthConsumerScopeWorkerModel,
+			sdk.AuthConsumerScopeProject,
 			sdk.AuthConsumerScopeRun,
 			sdk.AuthConsumerScopeRunExecution,
 		},
diff --git a/engine/api/authentication/corpsso/corpsso.go b/engine/api/authentication/corpsso/corpsso.go
index d08226fd8..fd5c3bd6f 100644
--- a/engine/api/authentication/corpsso/corpsso.go
+++ b/engine/api/authentication/corpsso/corpsso.go
@@ -89,7 +89,7 @@ func (d authDriver) GetSigninURI(state sdk.AuthSigninConsumerToken) (sdk.AuthDri
 }
 
 func (d authDriver) GetSessionDuration() time.Duration {
-	return time.Hour
+	return 24 * time.Hour
 }
 
 func (d authDriver) CheckSigninRequest(req sdk.AuthConsumerSigninRequest) error {
diff --git a/engine/api/events.go b/engine/api/events.go
index e150dc045..0b5ca529b 100644
--- a/engine/api/events.go
+++ b/engine/api/events.go
@@ -239,7 +239,7 @@ func (client *eventsBrokerSubscribe) manageEvent(db gorp.SqlExecutor, event sdk.
 
 	switch {
 	case strings.HasPrefix(event.EventType, "sdk.EventProject"):
-		if isSharedInfra || client.consumer.Maintainer() || client.consumer.Admin() {
+		if isSharedInfra || client.consumer.Maintainer() {
 			return true, nil
 		}
 
@@ -251,7 +251,7 @@ func (client *eventsBrokerSubscribe) manageEvent(db gorp.SqlExecutor, event sdk.
 		return perms.Level(event.ProjectKey) >= sdk.PermissionRead, nil
 
 	case strings.HasPrefix(event.EventType, "sdk.EventWorkflow") || strings.HasPrefix(event.EventType, "sdk.EventRunWorkflow"):
-		if isSharedInfra || client.consumer.Maintainer() || client.consumer.Admin() {
+		if isSharedInfra || client.consumer.Maintainer() {
 			return true, nil
 		}
 
@@ -267,7 +267,7 @@ func (client *eventsBrokerSubscribe) manageEvent(db gorp.SqlExecutor, event sdk.
 			return true, nil
 		}
 
-		if isSharedInfra || client.consumer.Maintainer() || client.consumer.Admin() {
+		if isSharedInfra || client.consumer.Maintainer() {
 			return true, nil
 		}
 
diff --git a/engine/api/notification/permission.go b/engine/api/notification/permission.go
index 9f2d56792..6c8751a98 100644
--- a/engine/api/notification/permission.go
+++ b/engine/api/notification/permission.go
@@ -27,7 +27,12 @@ func projectPermissionUsers(ctx context.Context, db gorp.SqlExecutor, projectID
 			AND	"group".id <> $3
 		`
 
-	rows, err := db.Query(query, projectID, access, group.SharedInfraGroup.ID)
+	var defaultGroupID int64
+	if group.DefaultGroup != nil {
+		defaultGroupID = group.DefaultGroup.ID
+	}
+
+	rows, err := db.Query(query, projectID, access, defaultGroupID)
 	if err != nil {
 		if err == sql.ErrNoRows {
 			return users, nil
diff --git a/engine/api/project.go b/engine/api/project.go
index f5e4d2dae..c08b062b9 100644
--- a/engine/api/project.go
+++ b/engine/api/project.go
@@ -291,18 +291,16 @@ func (api *API) getProjectHandler() service.Handler {
 		p.URLs.APIURL = api.Config.URL.API + api.Router.GetRoute("GET", api.getProjectHandler, map[string]string{"permProjectKey": key})
 		p.URLs.UIURL = api.Config.URL.UI + "/project/" + key
 
-		permissions, err := permission.LoadProjectMaxLevelPermission(ctx, api.mustDB(), []string{p.Key}, getAPIConsumer(ctx).GetGroupIDs())
-		if err != nil {
-			return err
-		}
-		p.Permissions = permissions.Permissions(p.Key)
-
-		if !p.Permissions.IsMaxLevel() && !p.Permissions.Readable {
-			if isMaintainer(ctx) {
-				p.Permissions = sdk.Permissions{Readable: true, Writable: false}
+		if isAdmin(ctx) {
+			p.Permissions = sdk.Permissions{Readable: true, Writable: true, Executable: true}
+		} else {
+			permissions, err := permission.LoadProjectMaxLevelPermission(ctx, api.mustDB(), []string{p.Key}, getAPIConsumer(ctx).GetGroupIDs())
+			if err != nil {
+				return err
 			}
-			if isAdmin(ctx) {
-				p.Permissions = sdk.Permissions{Readable: true, Writable: true}
+			p.Permissions = permissions.Permissions(p.Key)
+			if isMaintainer(ctx) {
+				p.Permissions.Readable = true
 			}
 		}
 
@@ -446,9 +444,9 @@ func (api *API) postProjectHandler() service.Handler {
 			var grp *sdk.Group
 			var err error
 			if gp.Group.ID != 0 {
-				grp, err = group.LoadByID(ctx, tx, gp.Group.ID)
+				grp, err = group.LoadByID(ctx, tx, gp.Group.ID, group.LoadOptions.WithMembers)
 			} else {
-				grp, err = group.LoadByName(ctx, tx, gp.Group.Name)
+				grp, err = group.LoadByName(ctx, tx, gp.Group.Name, group.LoadOptions.WithMembers)
 			}
 			if err != nil {
 				return err
diff --git a/engine/api/router_middleware_auth_permission.go b/engine/api/router_middleware_auth_permission.go
index 094b326fd..1a5be1490 100644
--- a/engine/api/router_middleware_auth_permission.go
+++ b/engine/api/router_middleware_auth_permission.go
@@ -5,6 +5,7 @@ import (
 	"strconv"
 
 	"github.com/ovh/cds/engine/api/authentication"
+	"github.com/ovh/cds/engine/api/project"
 	"github.com/ovh/cds/engine/api/cache"
 	"github.com/ovh/cds/engine/api/observability"
 	"github.com/ovh/cds/engine/api/workflow"
@@ -93,24 +94,29 @@ func (api *API) checkJobIDPermissions(ctx context.Context, jobID string, perm in
 	return nil
 }
 
-func (api *API) checkProjectPermissions(ctx context.Context, projectKey string, perm int, routeVars map[string]string) error {
+func (api *API) checkProjectPermissions(ctx context.Context, projectKey string, requiredPerm int, routeVars map[string]string) error {
 	ctx, end := observability.Span(ctx, "api.checkProjectPermissions")
 	defer end()
 
+	if _, err := project.Load(api.mustDB(), api.Cache, projectKey);  err != nil {
+		return err
+	}
+
 	perms, err := permission.LoadProjectMaxLevelPermission(ctx, api.mustDB(), []string{projectKey}, getAPIConsumer(ctx).GetGroupIDs())
 	if err != nil {
-		return sdk.NewErrorWithStack(err, sdk.WrapError(sdk.ErrForbidden, "not authorized for project %s", projectKey))
+		return sdk.WrapError(err,  "cannot get max project permissions for %s", projectKey)
 	}
 
-	maxLevelPermission := perms.Level(projectKey)
-	if maxLevelPermission < perm { // If the caller based on its group doesn have enough permission level
-		log.Debug("checkProjectPermissions> maxLevelPermission= %d ", maxLevelPermission)
+	callerPermission := perms.Level(projectKey)
+	// If the caller based on its group doesn't have enough permission level
+	if callerPermission < requiredPerm {
+		log.Debug("checkProjectPermissions> callerPermission=%d ", callerPermission)
 		// If it's about READ: we have to check if the user is a maintainer or an admin
-		if perm < sdk.PermissionReadExecute {
+		if requiredPerm == sdk.PermissionRead {
 			if !isMaintainer(ctx) {
 				// The caller doesn't enough permission level from its groups and is neither a maintainer nor an admin
 				log.Debug("checkProjectPermissions> %s(%s) is not authorized to %s", getAPIConsumer(ctx).Name, getAPIConsumer(ctx).ID, projectKey)
-				return sdk.WrapError(sdk.ErrForbidden, "not authorized for workflow %s", projectKey)
+				return sdk.WrapError(sdk.ErrNoProject, "not authorized for project %s", projectKey)
 			}
 			log.Debug("checkProjectPermissions> %s(%s) access granted to %s because is maintainer", getAPIConsumer(ctx).Name, getAPIConsumer(ctx).ID, projectKey)
 			observability.Current(ctx, observability.Tag(observability.TagPermission, "is_maintainer"))
@@ -126,9 +132,8 @@ func (api *API) checkProjectPermissions(ctx context.Context, projectKey string,
 		log.Debug("checkProjectPermissions> %s(%s) access granted to %s because is admin", getAPIConsumer(ctx).Name, getAPIConsumer(ctx).ID, projectKey)
 		observability.Current(ctx, observability.Tag(observability.TagPermission, "is_admin"))
 		return nil
-
 	}
-	log.Debug("checkWorkflowPermissions> %s(%s) access granted to %s because has permission (max permission = %d)", getAPIConsumer(ctx).Name, getAPIConsumer(ctx).ID, projectKey, maxLevelPermission)
+	log.Debug("checkWorkflowPermissions> %s(%s) access granted to %s because has permission (max permission = %d)", getAPIConsumer(ctx).Name, getAPIConsumer(ctx).ID, projectKey, callerPermission)
 	observability.Current(ctx, observability.Tag(observability.TagPermission, "is_granted"))
 	return nil
 }
diff --git a/engine/api/templates.go b/engine/api/templates.go
index 7475da327..b2ef64434 100644
--- a/engine/api/templates.go
+++ b/engine/api/templates.go
@@ -31,7 +31,7 @@ func (api *API) getTemplatesHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 		var ts []sdk.WorkflowTemplate
 		var err error
-		if isMaintainer(ctx) || isAdmin(ctx) {
+		if isMaintainer(ctx) {
 			ts, err = workflowtemplate.LoadAll(ctx, api.mustDB(),
 				workflowtemplate.LoadOptions.Default,
 				workflowtemplate.LoadOptions.WithAudits,
@@ -81,8 +81,7 @@ func (api *API) postTemplateHandler() service.Handler {
 				return sdk.NewErrorFrom(sdk.ErrWrongRequest, "missing group name")
 			}
 
-			// check that the user is admin on the given template's group
-			grp, err = group.LoadByName(ctx, api.mustDB(), data.Group.Name)
+			grp, err = group.LoadByName(ctx, api.mustDB(), data.Group.Name, group.LoadOptions.WithMembers)
 			if err != nil {
 				return sdk.NewError(sdk.ErrWrongRequest, err)
 			}
@@ -93,14 +92,10 @@ func (api *API) postTemplateHandler() service.Handler {
 				return err
 			}
 		} else {
-			// check that the group exists and user is admin for group id
-			grp, err = group.LoadByID(ctx, api.mustDB(), data.GroupID)
+			grp, err = group.LoadByID(ctx, api.mustDB(), data.GroupID, group.LoadOptions.WithMembers)
 			if err != nil {
 				return err
 			}
-			if grp == nil {
-				return sdk.WithStack(sdk.ErrNotFound)
-			}
 		}
 
 		data.Version = 0
@@ -144,7 +139,7 @@ func (api *API) getTemplateHandler() service.Handler {
 		groupName := vars["permGroupName"]
 		templateSlug := vars["permTemplateSlug"]
 
-		g, err := group.LoadByName(ctx, api.mustDB(), groupName)
+		g, err := group.LoadByName(ctx, api.mustDB(), groupName, group.LoadOptions.WithMembers)
 		if err != nil {
 			return err
 		}
@@ -169,7 +164,7 @@ func (api *API) putTemplateHandler() service.Handler {
 		groupName := vars["permGroupName"]
 		templateSlug := vars["permTemplateSlug"]
 
-		g, err := group.LoadByName(ctx, api.mustDB(), groupName)
+		g, err := group.LoadByName(ctx, api.mustDB(), groupName, group.LoadOptions.WithMembers)
 		if err != nil {
 			return err
 		}
@@ -207,7 +202,7 @@ func (api *API) putTemplateHandler() service.Handler {
 			}
 
 			// check that the user is admin on the given template's group
-			grp, err = group.LoadByName(ctx, api.mustDB(), data.Group.Name)
+			grp, err = group.LoadByName(ctx, api.mustDB(), data.Group.Name, group.LoadOptions.WithMembers)
 			if err != nil {
 				return sdk.NewError(sdk.ErrWrongRequest, err)
 			}
@@ -219,13 +214,14 @@ func (api *API) putTemplateHandler() service.Handler {
 			}
 		} else {
 			// check that the group exists and user is admin for group id
-			grp, err = group.LoadByID(ctx, api.mustDB(), data.GroupID)
+			grp, err = group.LoadByID(ctx, api.mustDB(), data.GroupID, group.LoadOptions.WithMembers)
 			if err != nil {
 				return err
 			}
-			if grp == nil {
-				return sdk.WithStack(sdk.ErrNotFound)
-			}
+		}
+
+		if !isGroupAdmin(ctx, grp) && !isAdmin(ctx) {
+			return sdk.WithStack(sdk.ErrForbidden)
 		}
 
 		// update fields from request data
@@ -402,7 +398,7 @@ func (api *API) postTemplateApplyHandler() service.Handler {
 		groupName := vars["groupName"]
 		templateSlug := vars["templateSlug"]
 
-		g, err := group.LoadByName(ctx, api.mustDB(), groupName)
+		g, err := group.LoadByName(ctx, api.mustDB(), groupName, group.LoadOptions.WithMembers)
 		if err != nil {
 			return err
 		}
@@ -489,7 +485,7 @@ func (api *API) postTemplateBulkHandler() service.Handler {
 		groupName := vars["groupName"]
 		templateSlug := vars["templateSlug"]
 
-		g, err := group.LoadByName(ctx, api.mustDB(), groupName)
+		g, err := group.LoadByName(ctx, api.mustDB(), groupName, group.LoadOptions.WithMembers)
 		if err != nil {
 			return err
 		}
@@ -641,7 +637,7 @@ func (api *API) getTemplateBulkHandler() service.Handler {
 		groupName := vars["groupName"]
 		templateSlug := vars["templateSlug"]
 
-		g, err := group.LoadByName(ctx, api.mustDB(), groupName)
+		g, err := group.LoadByName(ctx, api.mustDB(), groupName, group.LoadOptions.WithMembers)
 		if err != nil {
 			return err
 		}
@@ -676,7 +672,7 @@ func (api *API) getTemplateInstancesHandler() service.Handler {
 		groupName := vars["groupName"]
 		templateSlug := vars["templateSlug"]
 
-		g, err := group.LoadByName(ctx, api.mustDB(), groupName)
+		g, err := group.LoadByName(ctx, api.mustDB(), groupName, group.LoadOptions.WithMembers)
 		if err != nil {
 			return err
 		}
@@ -765,7 +761,7 @@ func (api *API) deleteTemplateInstanceHandler() service.Handler {
 		groupName := vars["groupName"]
 		templateSlug := vars["templateSlug"]
 
-		g, err := group.LoadByName(ctx, api.mustDB(), groupName)
+		g, err := group.LoadByName(ctx, api.mustDB(), groupName, group.LoadOptions.WithMembers)
 		if err != nil {
 			return err
 		}
@@ -816,7 +812,7 @@ func (api *API) postTemplatePullHandler() service.Handler {
 		groupName := vars["permGroupName"]
 		templateSlug := vars["permTemplateSlug"]
 
-		g, err := group.LoadByName(ctx, api.mustDB(), groupName)
+		g, err := group.LoadByName(ctx, api.mustDB(), groupName, group.LoadOptions.WithMembers)
 		if err != nil {
 			return err
 		}
@@ -859,7 +855,7 @@ func (api *API) postTemplatePushHandler() service.Handler {
 		}
 
 		// check that the user is admin on the given template's group
-		grp, err := group.LoadByName(ctx, api.mustDB(), wt.Group.Name)
+		grp, err := group.LoadByName(ctx, api.mustDB(), wt.Group.Name, group.LoadOptions.WithMembers)
 		if err != nil {
 			return sdk.NewError(sdk.ErrWrongRequest, err)
 		}
@@ -928,7 +924,7 @@ func (api *API) getTemplateUsageHandler() service.Handler {
 		groupName := vars["groupName"]
 		templateSlug := vars["templateSlug"]
 
-		g, err := group.LoadByName(ctx, api.mustDB(), groupName)
+		g, err := group.LoadByName(ctx, api.mustDB(), groupName, group.LoadOptions.WithMembers)
 		if err != nil {
 			return err
 		}
diff --git a/engine/api/worker.go b/engine/api/worker.go
index 8fb61dca4..d95637a25 100644
--- a/engine/api/worker.go
+++ b/engine/api/worker.go
@@ -113,16 +113,14 @@ func (api *API) getWorkersHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 		var workers []sdk.Worker
 		var err error
-		if !isAdmin(ctx) {
-			h, err := services.LoadByConsumerID(ctx, api.mustDB(), getAPIConsumer(ctx).ID)
-			if err != nil && !sdk.ErrorIs(err, sdk.ErrNotFound) {
-				return err
-			}
+		h, isHatchery := api.isHatchery(ctx)
+		if isHatchery && h != nil {
 			workers, err = worker.LoadByHatcheryID(ctx, api.mustDB(), h.ID)
 			if err != nil {
 				return err
 			}
-		} else {
+		} else if isMaintainer(ctx) {
+			// TODO Load worker for users
 			workers, err = worker.LoadAll(ctx, api.mustDB())
 			if err != nil {
 				return err
diff --git a/engine/api/worker_model.go b/engine/api/worker_model.go
index 8aa66a9ca..fb711f1dc 100644
--- a/engine/api/worker_model.go
+++ b/engine/api/worker_model.go
@@ -226,12 +226,17 @@ func (api *API) getWorkerModelsHandler() service.Handler {
 			filter.State = o
 		}
 
+		consumer := getAPIConsumer(ctx)
+
+		// TODO test if hatchery wildcard vs with groups (wildcard same code as a user)
 		models := []sdk.Model{}
 		var err error
-		if isMaintainer(ctx) || isAdmin(ctx) {
+		if _, ok := api.isHatchery(ctx); ok && len(consumer.GroupIDs) > 0 {
+			models, err = workermodel.LoadAllByGroupIDs(ctx, api.mustDB(), consumer.GetGroupIDs(), &filter, workermodel.LoadOptions.Default)
+		} else if isMaintainer(ctx) || isAdmin(ctx) {
 			models, err = workermodel.LoadAll(ctx, api.mustDB(), &filter, workermodel.LoadOptions.Default)
 		} else {
-			groupIDs := append(getAPIConsumer(ctx).GetGroupIDs(), group.SharedInfraGroup.ID)
+			groupIDs := append(consumer.GetGroupIDs(), group.SharedInfraGroup.ID)
 			models, err = workermodel.LoadAllByGroupIDs(ctx, api.mustDB(), groupIDs, &filter, workermodel.LoadOptions.Default)
 		}
 		if err != nil {
diff --git a/engine/api/worker_model_hatchery.go b/engine/api/worker_model_hatchery.go
index be36a7f57..89c85d5f4 100644
--- a/engine/api/worker_model_hatchery.go
+++ b/engine/api/worker_model_hatchery.go
@@ -93,8 +93,13 @@ func (api *API) getWorkerModelsEnabledHandler() service.Handler {
 		if !ok {
 			return sdk.WithStack(sdk.ErrForbidden)
 		}
-
-		var groupIDs = append(getAPIConsumer(ctx).GetGroupIDs(), group.SharedInfraGroup.ID)
+		consumer := getAPIConsumer(ctx)
+		var groupIDs []int64
+		if len(consumer.GroupIDs) > 0 {
+			groupIDs = consumer.GroupIDs
+		} else {
+			groupIDs = append(getAPIConsumer(ctx).GetGroupIDs(), group.SharedInfraGroup.ID)
+		}
 
 		// for an hatchery the token was generated by a admin for all consumer's groups.
 		models, err := workermodel.LoadAllUsableWithClearPasswordByGroupIDs(api.mustDB(), groupIDs)
diff --git a/engine/api/workflow.go b/engine/api/workflow.go
index fe29d74c3..9ece20906 100644
--- a/engine/api/workflow.go
+++ b/engine/api/workflow.go
@@ -48,14 +48,12 @@ func (api *API) getWorkflowsHandler() service.Handler {
 		}
 
 		for i := range ws {
-			ws[i].Permissions = perms.Permissions(ws[i].Name)
-
-			if !ws[i].Permissions.IsMaxLevel() && !ws[i].Permissions.Readable {
+			if isAdmin(ctx) {
+				ws[i].Permissions = sdk.Permissions{Readable: true, Writable: true, Executable: true}
+			} else {
+				ws[i].Permissions = perms.Permissions(ws[i].Name)
 				if isMaintainer(ctx) {
-					ws[i].Permissions = sdk.Permissions{Readable: true, Writable: false, Executable: false}
-				}
-				if isAdmin(ctx) {
-					ws[i].Permissions = sdk.Permissions{Readable: true, Writable: true, Executable: true}
+					ws[i].Permissions.Readable = true
 				}
 			}
 		}
@@ -128,18 +126,16 @@ func (api *API) getWorkflowHandler() service.Handler {
 			}
 		}
 
-		perms, err := permission.LoadWorkflowMaxLevelPermission(ctx, api.mustDB(), key, []string{w1.Name}, getAPIConsumer(ctx).GetGroupIDs())
-		if err != nil {
-			return err
-		}
-		w1.Permissions = perms.Permissions(w1.Name)
-
-		if !w1.Permissions.IsMaxLevel() && !w1.Permissions.Readable {
-			if isMaintainer(ctx) {
-				w1.Permissions = sdk.Permissions{Readable: true, Writable: false, Executable: false}
+		if isAdmin(ctx) {
+			w1.Permissions = sdk.Permissions{Readable: true, Writable: true, Executable: true}
+		} else {
+			perms, err := permission.LoadWorkflowMaxLevelPermission(ctx, api.mustDB(), key, []string{w1.Name}, getAPIConsumer(ctx).GetGroupIDs())
+			if err != nil {
+				return err
 			}
-			if isAdmin(ctx) {
-				w1.Permissions = sdk.Permissions{Readable: true, Writable: true, Executable: true}
+			w1.Permissions = perms.Permissions(w1.Name)
+			if isMaintainer(ctx) {
+				w1.Permissions.Readable = true
 			}
 		}
 
diff --git a/engine/api/workflow/dao_node_run_job.go b/engine/api/workflow/dao_node_run_job.go
index e4b5eefc6..f4771730f 100644
--- a/engine/api/workflow/dao_node_run_job.go
+++ b/engine/api/workflow/dao_node_run_job.go
@@ -262,8 +262,8 @@ func LoadNodeJobRun(ctx context.Context, db gorp.SqlExecutor, store cache.Store,
 //LoadDeadNodeJobRun load a NodeJobRun which is Building but without worker
 func LoadDeadNodeJobRun(ctx context.Context, db gorp.SqlExecutor, store cache.Store) ([]sdk.WorkflowNodeJobRun, error) {
 	var deadJobsDB []JobRun
-	query := `SELECT workflow_node_run_job.* FROM workflow_node_run_job WHERE status = $1 AND worker_id IS NULL`
-	if _, err := db.Select(&deadJobsDB, query, sdk.StatusBuilding); err != nil {
+	query := `SELECT workflow_node_run_job.* FROM workflow_node_run_job WHERE worker_id IS NULL`
+	if _, err := db.Select(&deadJobsDB, query); err != nil {
 		if err == sql.ErrNoRows {
 			return nil, nil
 		}
@@ -343,6 +343,13 @@ func DeleteNodeJobRuns(db gorp.SqlExecutor, nodeID int64) error {
 	return err
 }
 
+// DeleteNodeJobRun deletes the given workflow_node_run_job
+func DeleteNodeJobRun(db gorp.SqlExecutor, nodeRunJob int64) error {
+	query := `delete from workflow_node_run_job where id = $1`
+	_, err := db.Exec(query, nodeRunJob)
+	return err
+}
+
 //UpdateNodeJobRun updates a workflow_node_run_job
 func UpdateNodeJobRun(ctx context.Context, db gorp.SqlExecutor, j *sdk.WorkflowNodeJobRun) error {
 	var end func()
diff --git a/engine/api/workflow/execute_node_job_run.go b/engine/api/workflow/execute_node_job_run.go
index ae97fb51f..816fcc27d 100644
--- a/engine/api/workflow/execute_node_job_run.go
+++ b/engine/api/workflow/execute_node_job_run.go
@@ -190,8 +190,11 @@ func UpdateNodeJobRunStatus(ctx context.Context, db gorp.SqlExecutor, store cach
 	}
 	syncJobInNodeRun(nodeRun, job, stageIndex)
 
-	r, err := executeNodeRun(ctx, db, store, proj, nodeRun)
-	return report.Merge(ctx, r, err)
+	if job.Status != sdk.StatusStopped {
+		r, err := executeNodeRun(ctx, db, store, proj, nodeRun)
+		return report.Merge(ctx, r, err)
+	}
+	return nil, nil
 }
 
 // AddSpawnInfosNodeJobRun saves spawn info before starting worker
diff --git a/engine/api/workflow/execute_node_run.go b/engine/api/workflow/execute_node_run.go
index ef27009a0..270cbad54 100644
--- a/engine/api/workflow/execute_node_run.go
+++ b/engine/api/workflow/execute_node_run.go
@@ -744,10 +744,6 @@ func stopWorkflowNodePipeline(ctx context.Context, dbFunc func() *gorp.DbMap, st
 	nodeRun.Status = sdk.StatusStopped
 	nodeRun.Done = time.Now()
 
-	if err := DeleteNodeJobRuns(tx, nodeRun.ID); err != nil {
-		return nil, sdk.WrapError(err, "Unable to delete node %d job runs ", nodeRun.ID)
-	}
-
 	if errU := UpdateNodeRun(tx, nodeRun); errU != nil {
 		return report, sdk.WrapError(errU, "stopWorkflowNodePipeline> Cannot update node run")
 	}
diff --git a/engine/api/workflow/heartbeat.go b/engine/api/workflow/heartbeat.go
index 38819e511..c71c02d1a 100644
--- a/engine/api/workflow/heartbeat.go
+++ b/engine/api/workflow/heartbeat.go
@@ -12,8 +12,8 @@ import (
 
 const maxRetry = 3
 
-// restartDeadJob restart all jobs which are building but without worker
-func restartDeadJob(ctx context.Context, DBFunc func() *gorp.DbMap, store cache.Store) error {
+// manageDeadJob restart all jobs which are building but without worker
+func manageDeadJob(ctx context.Context, DBFunc func() *gorp.DbMap, store cache.Store) error {
 	db := DBFunc()
 	deadJobs, err := LoadDeadNodeJobRun(ctx, db, store)
 	if err != nil {
@@ -23,32 +23,40 @@ func restartDeadJob(ctx context.Context, DBFunc func() *gorp.DbMap, store cache.
 	for _, deadJob := range deadJobs {
 		tx, errTx := db.Begin()
 		if errTx != nil {
-			log.Error(ctx, "restartDeadJob> Cannot create transaction : %v", errTx)
+			log.Error(ctx, "manageDeadJob> Cannot create transaction : %v", errTx)
 			continue
 		}
 
-		if deadJob.Retry >= maxRetry {
-			if _, err := UpdateNodeJobRunStatus(ctx, tx, store, nil, &deadJob, sdk.StatusStopped); err != nil {
-				log.Error(ctx, "restartDeadJob> Cannot update node run job %d : %v", deadJob.ID, err)
-				_ = tx.Rollback()
-				continue
-			}
+		if deadJob.Status == sdk.StatusBuilding {
+			if deadJob.Retry >= maxRetry {
+				if _, err := UpdateNodeJobRunStatus(ctx, tx, store, nil, &deadJob, sdk.StatusStopped); err != nil {
+					log.Error(ctx, "manageDeadJob> Cannot update node run job %d : %v", deadJob.ID, err)
+					_ = tx.Rollback()
+					continue
+				}
 
-			if err := DeleteNodeJobRuns(tx, deadJob.WorkflowNodeRunID); err != nil {
-				log.Error(ctx, "restartDeadJob> Cannot delete node run job %d : %v", deadJob.ID, err)
-				_ = tx.Rollback()
-				continue
+				if err := DeleteNodeJobRun(tx, deadJob.ID); err != nil {
+					log.Error(ctx, "manageDeadJob> Cannot delete node run job %d : %v", deadJob.ID, err)
+					_ = tx.Rollback()
+					continue
+				}
+			} else {
+				if err := RestartWorkflowNodeJob(ctx, tx, deadJob); err != nil {
+					log.Warning(ctx, "manageDeadJob> Cannot restart node job run %d: %v", deadJob.ID, err)
+					_ = tx.Rollback()
+					continue
+				}
 			}
-		} else {
-			if err := RestartWorkflowNodeJob(ctx, tx, deadJob); err != nil {
-				log.Warning(ctx, "restartDeadJob> Cannot restart node job run %d: %v", deadJob.ID, err)
+		} else if sdk.StatusIsTerminated(deadJob.Status) {
+			if err := DeleteNodeJobRun(tx, deadJob.ID); err != nil {
+				log.Error(ctx, "manageDeadJob> Cannot delete node run job %d : %v", deadJob.ID, err)
 				_ = tx.Rollback()
 				continue
 			}
 		}
 
 		if err := tx.Commit(); err != nil {
-			log.Error(ctx, "restartDeadJob> Cannot commit transaction : %v", err)
+			log.Error(ctx, "manageDeadJob> Cannot commit transaction : %v", err)
 		}
 	}
 
diff --git a/engine/api/workflow/init.go b/engine/api/workflow/init.go
index 67abe0402..8811a76f0 100644
--- a/engine/api/workflow/init.go
+++ b/engine/api/workflow/init.go
@@ -31,8 +31,8 @@ func Initialize(ctx context.Context, DBFunc func() *gorp.DbMap, store cache.Stor
 				return
 			}
 		case <-tickHeart.C:
-			if err := restartDeadJob(ctx, DBFunc, store); err != nil {
-				log.Warning(ctx, "workflow.restartDeadJob> Error on restartDeadJob : %v", err)
+			if err := manageDeadJob(ctx, DBFunc, store); err != nil {
+				log.Warning(ctx, "workflow.manageDeadJob> Error on restartDeadJob : %v", err)
 			}
 		case <-tickStop.C:
 			if err := stopRunsBlocked(ctx, db); err != nil {
diff --git a/engine/api/workflow/run_workflow.go b/engine/api/workflow/run_workflow.go
index e2618cae7..0540fb25e 100644
--- a/engine/api/workflow/run_workflow.go
+++ b/engine/api/workflow/run_workflow.go
@@ -129,7 +129,7 @@ func StartWorkflowRun(ctx context.Context, db *gorp.DbMap, store cache.Store, p
 		}
 		opts.Manual.Username = u.GetUsername()
 		opts.Manual.Email = u.GetEmail()
-		opts.Manual.Username = u.GetFullname()
+		opts.Manual.Fullname = u.GetFullname()
 
 		if len(opts.FromNodeIDs) > 0 && len(wr.WorkflowNodeRuns) > 0 {
 			// MANUAL RUN FROM NODE
diff --git a/engine/api/workflow_queue.go b/engine/api/workflow_queue.go
index 77f3144b3..8ab5af514 100644
--- a/engine/api/workflow_queue.go
+++ b/engine/api/workflow_queue.go
@@ -608,7 +608,11 @@ func (api *API) postWorkflowJobStepStatusHandler() service.Handler {
 		for i := range nodeJobRun.Job.StepStatus {
 			jobStep := &nodeJobRun.Job.StepStatus[i]
 			if step.StepOrder == jobStep.StepOrder {
-				jobStep.Status = step.Status
+				if nodeJobRun.Status == sdk.StatusStopped {
+					jobStep.Status = sdk.StatusStopped
+				} else {
+					jobStep.Status = step.Status
+				}
 				if sdk.StatusIsTerminated(step.Status) {
 					jobStep.Done = step.Done
 				}
diff --git a/engine/hatchery/kubernetes/kubernetes.go b/engine/hatchery/kubernetes/kubernetes.go
index e5b648500..6cc90163d 100644
--- a/engine/hatchery/kubernetes/kubernetes.go
+++ b/engine/hatchery/kubernetes/kubernetes.go
@@ -228,6 +228,16 @@ func (h *HatcheryKubernetes) WorkerModelsEnabled() ([]sdk.Model, error) {
 // CanSpawn return wether or not hatchery can spawn model.
 // requirements are not supported
 func (h *HatcheryKubernetes) CanSpawn(ctx context.Context, model *sdk.Model, jobID int64, requirements []sdk.Requirement) bool {
+	// Service and Hostname requirement are not supported
+	for _, r := range requirements {
+		if r.Type == sdk.ServiceRequirement {
+			log.Debug("CanSpawn> Job %d has a service requirement. Kubernetes can't spawn a worker for this job", jobID)
+			return false
+		} else if r.Type == sdk.HostnameRequirement {
+			log.Debug("CanSpawn> Job %d has a hostname requirement. Kubernetes can't spawn a worker for this job", jobID)
+			return false
+		}
+	}
 	return true
 }
 
diff --git a/engine/hatchery/local/local.go b/engine/hatchery/local/local.go
index 249f2162b..a09a731c4 100644
--- a/engine/hatchery/local/local.go
+++ b/engine/hatchery/local/local.go
@@ -62,9 +62,13 @@ func (h *HatcheryLocal) ApplyConfiguration(cfg interface{}) error {
 	genname := h.Configuration().Name
 	h.Common.Common.ServiceName = genname
 	h.Common.Common.ServiceType = services.TypeHatchery
+	var err error
+	h.Config.Basedir, err = filepath.Abs(h.Config.Basedir)
+	if err != nil {
+		return fmt.Errorf("unable to get basedir absolute path: %v", err)
+	}
 	h.HTTPURL = h.Config.URL
 	h.MaxHeartbeatFailures = h.Config.API.MaxHeartbeatFailures
-	var err error
 	h.Common.Common.PrivateKey, err = jwt.ParseRSAPrivateKeyFromPEM([]byte(h.Config.RSAPrivateKey))
 	if err != nil {
 		return fmt.Errorf("unable to parse RSA private Key: %v", err)
@@ -369,6 +373,12 @@ func (h *HatcheryLocal) checkRequirement(r sdk.Requirement) (bool, error) {
 		}
 		conn.Close()
 		return true, nil
+	case sdk.HostnameRequirement:
+		h, err := os.Hostname()
+		if err != nil {
+			return false, err
+		}
+		return h == r.Value, nil
 	default:
 		log.Debug("checkRequirement> %v don't work on this hatchery", r.Type)
 		return false, nil
diff --git a/engine/hatchery/marathon/marathon.go b/engine/hatchery/marathon/marathon.go
index e6b1931b6..39d087f8e 100644
--- a/engine/hatchery/marathon/marathon.go
+++ b/engine/hatchery/marathon/marathon.go
@@ -166,11 +166,14 @@ func (h *HatcheryMarathon) WorkerModelsEnabled() ([]sdk.Model, error) {
 // CanSpawn return wether or not hatchery can spawn model
 // requirements services are not supported
 func (h *HatcheryMarathon) CanSpawn(ctx context.Context, model *sdk.Model, jobID int64, requirements []sdk.Requirement) bool {
-	//Service requirement are not supported
+	// Service and Hostname requirement are not supported
 	for _, r := range requirements {
 		if r.Type == sdk.ServiceRequirement {
 			log.Debug("CanSpawn> Job %d has a service requirement. Marathon can't spawn a worker for this job", jobID)
 			return false
+		} else if r.Type == sdk.HostnameRequirement {
+			log.Debug("CanSpawn> Job %d has a hostname requirement. Marathon can't spawn a worker for this job", jobID)
+			return false
 		}
 	}
 
@@ -309,7 +312,7 @@ func (h *HatcheryMarathon) SpawnWorker(ctx context.Context, spawnArgs hatchery.S
 			Type: "DOCKER",
 		},
 		Env:       &envsWm,
-		CPUs:      2,
+		CPUs:      h.Config.DefaultCPUs,
 		Instances: &instance,
 		Mem:       &mem,
 		Labels:    &h.marathonLabels,
diff --git a/engine/hatchery/marathon/types.go b/engine/hatchery/marathon/types.go
index ea50cd4f5..04289abc8 100644
--- a/engine/hatchery/marathon/types.go
+++ b/engine/hatchery/marathon/types.go
@@ -26,6 +26,9 @@ type HatcheryConfiguration struct {
 	// MarathonLabelsStr "marathon-labels"
 	MarathonLabels string `mapstructure:"labels" toml:"labels" default:"" commented:"false" comment:"Use this option if you want to add labels on workers spawned by this hatchery.\n Format: MarathonLabels = \"A_LABEL=value-of-label,B_LABEL=value-of-label-b\"" json:"labels"`
 
+	// DefaultCPUs
+	DefaultCPUs float64 `mapstructure:"defaultCPUs" toml:"defaultCPUs" default:"1" commented:"false" comment:"Worker default CPUs count" json:"defaultCPUs"`
+
 	// DefaultMemory Worker default memory
 	DefaultMemory int `mapstructure:"defaultMemory" toml:"defaultMemory" default:"1024" commented:"false" comment:"Worker default memory in Mo" json:"defaultMemory"`
 
diff --git a/engine/hatchery/openstack/openstack.go b/engine/hatchery/openstack/openstack.go
index 463d14091..167af86cf 100644
--- a/engine/hatchery/openstack/openstack.go
+++ b/engine/hatchery/openstack/openstack.go
@@ -166,7 +166,7 @@ func (h *HatcheryOpenstack) WorkerModelsEnabled() ([]sdk.Model, error) {
 // requirements are not supported
 func (h *HatcheryOpenstack) CanSpawn(ctx context.Context, model *sdk.Model, jobID int64, requirements []sdk.Requirement) bool {
 	for _, r := range requirements {
-		if r.Type == sdk.ServiceRequirement || r.Type == sdk.MemoryRequirement {
+		if r.Type == sdk.ServiceRequirement || r.Type == sdk.MemoryRequirement || r.Type == sdk.HostnameRequirement {
 			return false
 		}
 	}
diff --git a/engine/hatchery/swarm/swarm.go b/engine/hatchery/swarm/swarm.go
index 9b571f344..f94b1cfb3 100644
--- a/engine/hatchery/swarm/swarm.go
+++ b/engine/hatchery/swarm/swarm.go
@@ -454,6 +454,13 @@ const (
 // CanSpawn checks if the model can be spawned by this hatchery
 // it checks on every docker engine is one of the docker has availability
 func (h *HatcherySwarm) CanSpawn(ctx context.Context, model *sdk.Model, jobID int64, requirements []sdk.Requirement) bool {
+	// Hostname requirement are not supported
+	for _, r := range requirements {
+		if r.Type == sdk.HostnameRequirement {
+			log.Debug("CanSpawn> Job %d has a hostname requirement. Swarm can't spawn a worker for this job", jobID)
+			return false
+		}
+	}
 	for dockerName, dockerClient := range h.dockerClients {
 		//List all containers to check if we can spawn a new one
 		cs, errList := h.getContainers(dockerClient, types.ContainerListOptions{All: true})
@@ -626,13 +633,14 @@ func (h *HatcherySwarm) listAwolWorkers(dockerClientName string, containers []ty
 	//Checking workers
 	oldContainers := []types.Container{}
 	for _, c := range workers {
-		if !strings.Contains(c.Status, "Exited") && time.Now().Add(-1*time.Minute).Unix() < c.Created {
+		if !strings.Contains(c.Status, "Exited") && time.Now().Add(-3*time.Minute).Unix() < c.Created {
 			log.Debug("hatchery> swarm> listAwolWorkers> container %s(status=%s) is too young", c.Names[0], c.Status)
 			continue
 		}
 
 		//If there isn't any worker registered on the API. Kill the container
 		if len(apiworkers) == 0 {
+			log.Debug("hatchery> swarm> listAwolWorkers> no apiworkers returned by api container %s will be deleted", c.Names[0])
 			oldContainers = append(oldContainers, c)
 			continue
 		}
@@ -652,6 +660,7 @@ func (h *HatcherySwarm) listAwolWorkers(dockerClientName string, containers []ty
 		}
 		//If the container doesn't match any worker : Kill it.
 		if !found {
+			log.Debug("hatchery> swarm> listAwolWorkers> container %s not found on apiworkers", c.Names[0])
 			oldContainers = append(oldContainers, c)
 		}
 	}
@@ -681,12 +690,25 @@ func (h *HatcherySwarm) killAwolWorker(ctx context.Context) error {
 			}
 		}
 
+		// creating a map of containers names
+		mContainers := map[string]struct{}{}
+		for i := range containers {
+			name := strings.TrimPrefix(containers[i].Names[0], "/") // docker returns name prefixed by a /
+			mContainers[name] = struct{}{}
+		}
+
 		// Checking services
 		for _, c := range containers {
-			if c.Labels["service_worker"] == "" || c.Names[0] != c.Labels["service_worker"] {
+			// checks if the container is a service based on its labels
+			if c.Labels["service_worker"] == "" {
 				continue
 			}
-			if !strings.Contains(c.Status, "Exited") && time.Now().Add(-1*time.Minute).Unix() < c.Created {
+			// if the worker associated to this service is still alive do not kill the service
+			if _, workerStillAlive := mContainers[c.Labels["service_worker"]]; workerStillAlive {
+				continue
+			}
+
+			if !strings.Contains(c.Status, "Exited") && time.Now().Add(-3*time.Minute).Unix() < c.Created {
 				log.Debug("hatchery> swarm> killAwolWorker> container %s(status=%s) is too young - service associated to worker %s", c.Names[0], c.Status, c.Labels["service_worker"])
 				continue
 			}
diff --git a/engine/hatchery/swarm/swarm_util_create.go b/engine/hatchery/swarm/swarm_util_create.go
index 9b62b4149..b94f0f3a2 100644
--- a/engine/hatchery/swarm/swarm_util_create.go
+++ b/engine/hatchery/swarm/swarm_util_create.go
@@ -125,7 +125,7 @@ checkImage:
 	if !imageFound {
 		hatchery.SendSpawnInfo(ctx, h, spawnArgs.JobID, sdk.SpawnMsg{
 			ID:   sdk.MsgSpawnInfoHatcheryStartDockerPull.ID,
-			Args: []interface{}{h.Name, cArgs.image},
+			Args: []interface{}{h.Name(), cArgs.image},
 		})
 
 		_, next := observability.Span(ctx, "swarm.dockerClient.pullImage", observability.Tag("image", cArgs.image))
@@ -136,7 +136,7 @@ checkImage:
 			next()
 			hatchery.SendSpawnInfo(ctx, h, spawnArgs.JobID, sdk.SpawnMsg{
 				ID:   sdk.MsgSpawnInfoHatcheryEndDockerPullErr.ID,
-				Args: []interface{}{h.Name, cArgs.image, err},
+				Args: []interface{}{h.Name(), cArgs.image, err},
 			})
 			return sdk.WrapError(err, "Unable to pull image %s on %s", cArgs.image, dockerClient.name)
 		}
@@ -144,7 +144,7 @@ checkImage:
 
 		hatchery.SendSpawnInfo(ctx, h, spawnArgs.JobID, sdk.SpawnMsg{
 			ID:   sdk.MsgSpawnInfoHatcheryEndDockerPull.ID,
-			Args: []interface{}{h.Name, cArgs.image},
+			Args: []interface{}{h.Name(), cArgs.image},
 		})
 	}
 
diff --git a/engine/hatchery/vsphere/vsphere.go b/engine/hatchery/vsphere/vsphere.go
index 9fd214d5a..474a0e677 100644
--- a/engine/hatchery/vsphere/vsphere.go
+++ b/engine/hatchery/vsphere/vsphere.go
@@ -118,7 +118,7 @@ func (h *HatcheryVSphere) CheckConfiguration(cfg interface{}) error {
 // requirements are not supported
 func (h *HatcheryVSphere) CanSpawn(ctx context.Context, model *sdk.Model, jobID int64, requirements []sdk.Requirement) bool {
 	for _, r := range requirements {
-		if r.Type == sdk.ServiceRequirement || r.Type == sdk.MemoryRequirement {
+		if r.Type == sdk.ServiceRequirement || r.Type == sdk.MemoryRequirement || r.Type == sdk.HostnameRequirement {
 			return false
 		}
 	}
diff --git a/engine/hooks/outgoing_hooks.go b/engine/hooks/outgoing_hooks.go
index 9243703de..b48e507ad 100644
--- a/engine/hooks/outgoing_hooks.go
+++ b/engine/hooks/outgoing_hooks.go
@@ -235,7 +235,7 @@ func (s *Service) doOutgoingWorkflowExecution(ctx context.Context, t *sdk.TaskEx
 	}
 
 	callbackData.Log = fmt.Sprintf("Workflow %s/%s #%d.%d has been started", targetProject, targetWorkflow, targetRun.Number, targetRun.LastSubNumber)
-	callbackData.Status = sdk.StatusDisabled
+	callbackData.Status = sdk.StatusBuilding
 	callbackData.WorkflowRunNumber = &targetRun.Number
 
 	// Post the callback
diff --git a/engine/vcs/bitbucketserver/client_status.go b/engine/vcs/bitbucketserver/client_status.go
index 8d02d7521..1d8eef753 100644
--- a/engine/vcs/bitbucketserver/client_status.go
+++ b/engine/vcs/bitbucketserver/client_status.go
@@ -149,11 +149,9 @@ func processWorkflowNodeRunEvent(event sdk.Event, uiURL string) (statusData, err
 
 func getBitbucketStateFromStatus(status string) string {
 	switch status {
-	case sdk.StatusSuccess, sdk.StatusSkipped:
+	case sdk.StatusSuccess, sdk.StatusSkipped, sdk.StatusDisabled:
 		return successful
-	case sdk.StatusWaiting:
-		return inProgress
-	case sdk.StatusDisabled:
+	case sdk.StatusWaiting, sdk.StatusBuilding:
 		return inProgress
 	case sdk.StatusFail:
 		return failed
diff --git a/engine/worker/cmd_artifacts.go b/engine/worker/cmd_artifacts.go
index f80a0019f..7d40ae766 100644
--- a/engine/worker/cmd_artifacts.go
+++ b/engine/worker/cmd_artifacts.go
@@ -9,12 +9,10 @@ import (
 	"os"
 	"strconv"
 
-	"github.com/ovh/cds/engine/worker/pkg/workerruntime"
-
-	"github.com/ovh/cds/engine/worker/internal"
-
 	"github.com/spf13/cobra"
 
+	"github.com/ovh/cds/engine/worker/internal"
+	"github.com/ovh/cds/engine/worker/pkg/workerruntime"
 	"github.com/ovh/cds/sdk"
 )
 
diff --git a/engine/worker/cmd_cache.go b/engine/worker/cmd_cache.go
index 0336fdba1..2e7af012f 100644
--- a/engine/worker/cmd_cache.go
+++ b/engine/worker/cmd_cache.go
@@ -102,11 +102,7 @@ func cachePushCmd() func(cmd *cobra.Command, args []string) {
 
 		files := make([]string, len(args)-1)
 		for i, arg := range args[1:] {
-			absPath, err := filepath.Abs(arg)
-			if err != nil {
-				sdk.Exit("worker cache push > cannot have absolute path for (%s) : %s", absPath, err)
-			}
-			files[i] = absPath
+			files[i] = arg
 		}
 
 		cwd, err := os.Getwd()
diff --git a/engine/worker/cmd_upload.go b/engine/worker/cmd_upload.go
index 4bffb30c9..f7f7c94f7 100644
--- a/engine/worker/cmd_upload.go
+++ b/engine/worker/cmd_upload.go
@@ -13,6 +13,7 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/ovh/cds/engine/worker/internal"
+	"github.com/ovh/cds/engine/worker/pkg/workerruntime"
 	"github.com/ovh/cds/sdk"
 )
 
@@ -60,10 +61,12 @@ func uploadCmd() func(cmd *cobra.Command, args []string) {
 			sdk.Exit("Wrong usage: Example : worker upload --tag={{.cds.version}} filea fileb filec*")
 		}
 
+		cwd, _ := os.Getwd()
 		for _, arg := range args {
-			a := sdk.WorkflowNodeRunArtifact{
-				Name: arg,
-				Tag:  cmdUploadTag,
+			a := workerruntime.UploadArtifact{
+				Name:             arg,
+				Tag:              cmdUploadTag,
+				WorkingDirectory: cwd,
 			}
 
 			data, errMarshal := json.Marshal(a)
@@ -85,7 +88,7 @@ func uploadCmd() func(cmd *cobra.Command, args []string) {
 			}
 
 			if resp.StatusCode >= 300 {
-				sdk.Exit("cannot artefact upload HTTP %d\n", resp.StatusCode)
+				sdk.Exit("cannot artifact upload HTTP %d\n", resp.StatusCode)
 			}
 		}
 	}
diff --git a/engine/worker/internal/action/builtin_artifact_upload.go b/engine/worker/internal/action/builtin_artifact_upload.go
index 671057847..0a5624da3 100644
--- a/engine/worker/internal/action/builtin_artifact_upload.go
+++ b/engine/worker/internal/action/builtin_artifact_upload.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"path/filepath"
 	"strings"
 	"sync"
 	"time"
@@ -23,9 +24,9 @@ func RunArtifactUpload(ctx context.Context, wk workerruntime.Runtime, a sdk.Acti
 		return res, err
 	}
 
-	path := strings.TrimSpace(sdk.ParameterValue(a.Parameters, "path"))
-	if path == "" {
-		path = "."
+	artifactPath := strings.TrimSpace(sdk.ParameterValue(a.Parameters, "path"))
+	if artifactPath == "" {
+		artifactPath = "."
 	}
 
 	workdir, err := workerruntime.WorkingDirectory(ctx)
@@ -40,22 +41,23 @@ func RunArtifactUpload(ctx context.Context, wk workerruntime.Runtime, a sdk.Acti
 		abs = workdir.Name()
 	}
 
-	wkDirFS := afero.NewBasePathFs(afero.NewOsFs(), abs)
+	if !sdk.PathIsAbs(artifactPath) {
+		artifactPath = filepath.Join(abs, artifactPath)
+	}
 
 	tag := sdk.ParameterFind(a.Parameters, "tag")
 	if tag == nil {
 		return res, errors.New("tag variable is empty. aborting")
 	}
 
-	path = strings.TrimPrefix(path, abs)
 	// Global all files matching filePath
-	filesPath, err := afero.Glob(wkDirFS, path)
+	filesPath, err := afero.Glob(afero.NewOsFs(), artifactPath)
 	if err != nil {
-		return res, fmt.Errorf("cannot perform globbing of pattern '%s': %s", path, err)
+		return res, fmt.Errorf("cannot perform globbing of pattern '%s': %s", artifactPath, err)
 	}
 
 	if len(filesPath) == 0 {
-		return res, fmt.Errorf("pattern '%s' matched no file", path)
+		return res, fmt.Errorf("pattern '%s' matched no file", artifactPath)
 	}
 
 	var globalError = &sdk.MultiError{}
@@ -77,12 +79,11 @@ func RunArtifactUpload(ctx context.Context, wk workerruntime.Runtime, a sdk.Acti
 	wg.Add(len(filesPath))
 	for _, p := range filesPath {
 		go func(path string) {
-			absFile := abs + "/" + strings.TrimPrefix(path, abs)
-			log.Debug("Uploading %s projectKey:%v integrationName:%v job:%d", absFile, projectKey, integrationName, jobID)
+			log.Debug("Uploading %s projectKey:%v integrationName:%v job:%d", path, projectKey, integrationName, jobID)
 			defer wg.Done()
-			throughTempURL, duration, err := wk.Client().QueueArtifactUpload(ctx, projectKey, integrationName, jobID, tag.Value, absFile)
+			throughTempURL, duration, err := wk.Client().QueueArtifactUpload(ctx, projectKey, integrationName, jobID, tag.Value, path)
 			if err != nil {
-				chanError <- sdk.WrapError(err, "Error while uploading artifact %s", absFile)
+				chanError <- sdk.WrapError(err, "Error while uploading artifact %s", path)
 				wgErrors.Add(1)
 				return
 			}
diff --git a/engine/worker/internal/action/builtin_coverage.go b/engine/worker/internal/action/builtin_coverage.go
index 1d6e184f5..9e97c5947 100644
--- a/engine/worker/internal/action/builtin_coverage.go
+++ b/engine/worker/internal/action/builtin_coverage.go
@@ -3,9 +3,11 @@ package action
 import (
 	"context"
 	"fmt"
+	"path/filepath"
 	"strconv"
 
 	coverage "github.com/sguiheux/go-coverage"
+	"github.com/spf13/afero"
 
 	"github.com/ovh/cds/engine/worker/pkg/workerruntime"
 	"github.com/ovh/cds/sdk"
@@ -47,7 +49,27 @@ func RunParseCoverageResultAction(ctx context.Context, wk workerruntime.Runtime,
 	default:
 		return res, fmt.Errorf("coverage parser: unknown format %s", mode)
 	}
-	parser := coverage.New(p, parserMode)
+
+	workdir, err := workerruntime.WorkingDirectory(ctx)
+	if err != nil {
+		return res, err
+	}
+
+	var fpath string
+	var abs string
+	if x, ok := wk.BaseDir().(*afero.BasePathFs); ok {
+		abs, _ = x.RealPath(workdir.Name())
+	} else {
+		abs = workdir.Name()
+	}
+
+	if !sdk.PathIsAbs(p) {
+		fpath = filepath.Join(abs, p)
+	} else {
+		fpath = p
+	}
+
+	parser := coverage.New(fpath, parserMode)
 	report, errR := parser.Parse()
 	if errR != nil {
 		return res, fmt.Errorf("coverage parser: unable to parse report: %v", errR)
diff --git a/engine/worker/internal/action/builtin_deploy_application.go b/engine/worker/internal/action/builtin_deploy_application.go
index 473e205f9..6d4ff4599 100644
--- a/engine/worker/internal/action/builtin_deploy_application.go
+++ b/engine/worker/internal/action/builtin_deploy_application.go
@@ -51,7 +51,7 @@ func RunDeployApplication(ctx context.Context, wk workerruntime.Runtime, a sdk.A
 		return sdk.Result{}, fmt.Errorf("Unable to retrieve the plugin for deployment integration %s... Aborting", pf.Model.Name)
 	}
 
-	pluginSocket, err := startGRPCPlugin(context.Background(), binary.PluginName, wk, binary, startGRPCPluginOptions{})
+	pluginSocket, err := startGRPCPlugin(ctx, binary.PluginName, wk, binary, startGRPCPluginOptions{})
 	if err != nil {
 		return sdk.Result{}, fmt.Errorf("unable to start GRPCPlugin: %v", err)
 	}
diff --git a/engine/worker/internal/action/builtin_gitclone.go b/engine/worker/internal/action/builtin_gitclone.go
index f39ff48d2..52b923b1b 100644
--- a/engine/worker/internal/action/builtin_gitclone.go
+++ b/engine/worker/internal/action/builtin_gitclone.go
@@ -37,15 +37,16 @@ func RunGitClone(ctx context.Context, wk workerruntime.Runtime, a sdk.Action, se
 	if privateKey != nil && privateKey.Value != "" {
 		// The private key parameter, contains the name of the private key to use.
 		// Let's look up in the secret list to find the content of the private key
-		privateKeyContent := sdk.VariableFind(secrets, privateKey.Value)
+		privateKeyContent := sdk.VariableFind(secrets, "cds.key."+privateKey.Value+".priv")
 
 		if privateKeyContent == nil {
-			return sdk.Result{}, fmt.Errorf("unknown key \"%s\"", privateKey.Value)
+			return sdk.Result{}, fmt.Errorf("unknown key \"%s\"", privateKey.Name)
 		}
 
 		installedKey, err := wk.InstallKey(sdk.Variable{
 			Name:  privateKeyContent.Name,
 			Value: privateKeyContent.Value,
+			Type:  sdk.KeyTypeSSH,
 		}, "")
 		if err != nil {
 			return sdk.Result{}, err
diff --git a/engine/worker/internal/action/builtin_junit.go b/engine/worker/internal/action/builtin_junit.go
index 7e89086ee..a16277537 100644
--- a/engine/worker/internal/action/builtin_junit.go
+++ b/engine/worker/internal/action/builtin_junit.go
@@ -5,11 +5,13 @@ import (
 	"encoding/xml"
 	"errors"
 	"fmt"
-	"io/ioutil"
 	"path/filepath"
 
+	"github.com/spf13/afero"
+
 	"github.com/ovh/cds/engine/worker/pkg/workerruntime"
 	"github.com/ovh/cds/sdk"
+	"github.com/ovh/cds/sdk/log"
 	"github.com/ovh/venom"
 )
 
@@ -27,7 +29,30 @@ func RunParseJunitTestResultAction(ctx context.Context, wk workerruntime.Runtime
 		return res, errors.New("UnitTest parser: path not provided")
 	}
 
-	files, errg := filepath.Glob(p)
+	workdir, err := workerruntime.WorkingDirectory(ctx)
+	if err != nil {
+		return res, err
+	}
+
+	var abs string
+	if x, ok := wk.BaseDir().(*afero.BasePathFs); ok {
+		abs, _ = x.RealPath(workdir.Name())
+		log.Debug("RunParseJunitTestResultAction> workdir is %s", abs)
+	} else {
+		abs = workdir.Name()
+	}
+
+	if !sdk.PathIsAbs(p) {
+		p = filepath.Join(abs, p)
+	}
+
+	log.Debug("RunParseJunitTestResultAction> path: %v", p)
+
+	// Global all files matching filePath
+	files, errg := afero.Glob(afero.NewOsFs(), p)
+
+	log.Debug("RunParseJunitTestResultAction> files: %v", files)
+
 	if errg != nil {
 		return res, errors.New("UnitTest parser: Cannot find requested files, invalid pattern")
 	}
@@ -38,7 +63,7 @@ func RunParseJunitTestResultAction(ctx context.Context, wk workerruntime.Runtime
 	for _, f := range files {
 		var ftests venom.Tests
 
-		data, errRead := ioutil.ReadFile(f)
+		data, errRead := afero.ReadFile(afero.NewOsFs(), f)
 		if errRead != nil {
 			return res, fmt.Errorf("UnitTest parser: cannot read file %s (%s)", f, errRead)
 		}
@@ -61,7 +86,7 @@ func RunParseJunitTestResultAction(ctx context.Context, wk workerruntime.Runtime
 		wk.SendLog(ctx, workerruntime.LevelInfo, r)
 	}
 
-	if err := wk.Blur(tests); err != nil {
+	if err := wk.Blur(&tests); err != nil {
 		return res, err
 	}
 
diff --git a/engine/worker/internal/action/builtin_script.go b/engine/worker/internal/action/builtin_script.go
index 6b1e87dc6..c45546c17 100644
--- a/engine/worker/internal/action/builtin_script.go
+++ b/engine/worker/internal/action/builtin_script.go
@@ -14,9 +14,8 @@ import (
 	"runtime"
 	"strings"
 
-	"github.com/spf13/afero"
-
 	"github.com/kardianos/osext"
+	"github.com/spf13/afero"
 
 	"github.com/ovh/cds/engine/worker/pkg/workerruntime"
 	"github.com/ovh/cds/sdk"
@@ -30,7 +29,7 @@ type script struct {
 	opts    []string
 }
 
-func prepareScriptContent(parameters []sdk.Parameter) (*script, error) {
+func prepareScriptContent(parameters []sdk.Parameter, basedir afero.Fs, workdir afero.File) (*script, error) {
 	var script = script{
 		shell: "/bin/sh",
 	}
@@ -46,7 +45,7 @@ func prepareScriptContent(parameters []sdk.Parameter) (*script, error) {
 	}
 
 	// except on windows where it's powershell
-	if sdk.GOOS == "windows" {
+	if isWindows() {
 		script.shell = "PowerShell"
 		script.opts = []string{"-ExecutionPolicy", "Bypass", "-Command"}
 		// on windows, we add ErrorActionPreference just below
@@ -68,9 +67,21 @@ func prepareScriptContent(parameters []sdk.Parameter) (*script, error) {
 
 	script.content = []byte(scriptContent)
 
+	if x, ok := basedir.(*afero.BasePathFs); ok {
+		script.dir, _ = x.RealPath(workdir.Name())
+	} else {
+		script.dir = workdir.Name()
+	}
+
+	log.Debug("prepareScriptContent> script.dir is %s", script.dir)
+
 	return &script, nil
 }
 
+func isWindows() bool {
+	return sdk.GOOS == "windows" || runtime.GOOS == "windows" || os.Getenv("CDS_WORKER_PSHELL_MODE") == "true"
+}
+
 func writeScriptContent(ctx context.Context, script *script, fs afero.Fs, basedir afero.File) (func(), error) {
 	fi, err := basedir.Stat()
 	if err != nil {
@@ -89,11 +100,16 @@ func writeScriptContent(ctx context.Context, script *script, fs afero.Fs, basedi
 	}
 	tmpFileName := hex.EncodeToString(bs)[0:16]
 	log.Debug("writeScriptContent> Basedir name is %s (%T)", basedir.Name(), basedir)
-	path := filepath.Join(basedir.Name(), tmpFileName)
 
-	log.Debug("writeScriptContent> Opening file %s", path)
+	if isWindows() {
+		tmpFileName += ".PS1"
+		log.Debug("runScriptAction> renaming powershell script to %s", tmpFileName)
+	}
+
+	scriptPath := filepath.Join(path.Dir(basedir.Name()), tmpFileName)
+	log.Debug("writeScriptContent> Opening file %s", scriptPath)
 
-	tmpscript, err := fs.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0700)
+	tmpscript, err := fs.OpenFile(scriptPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0700)
 	if err != nil {
 		log.Warning(ctx, "writeScriptContent> Cannot create tmp file: %s", err)
 		return nil, fmt.Errorf("cannot create temporary file, aborting: %v", err)
@@ -104,9 +120,9 @@ func writeScriptContent(ctx context.Context, script *script, fs afero.Fs, basedi
 	n, errw := tmpscript.Write(script.content)
 	if errw != nil || n != len(script.content) {
 		if errw != nil {
-			log.Warning(ctx, "cannot write script: %s", errw)
+			log.Warning(ctx, "writeScriptContent> cannot write script: %s", errw)
 		} else {
-			log.Warning(ctx, "cannot write all script: %d/%d", n, len(script.content))
+			log.Warning(ctx, "writeScriptContent> cannot write all script: %d/%d", n, len(script.content))
 		}
 		return nil, errors.New("cannot write script in temporary file, aborting")
 	}
@@ -115,32 +131,36 @@ func writeScriptContent(ctx context.Context, script *script, fs afero.Fs, basedi
 		return nil, fmt.Errorf("unable to write script to %s", tmpscript)
 	}
 
-	if runtime.GOOS == "windows" {
-		//and add .PS1 extension
-		//newName := tmpFileName + ".PS1"
-		//if err := basedir.Rename(tmpFileName, newName); err != nil {
-		//	return nil, errors.New("cannot rename script to add powershell Extension, aborting")
-		//}
-		//tmpFileName = newName
-		////This aims to stop a the very first error and return the right exit code
-		//psCommand := fmt.Sprintf("& { $ErrorActionPreference='Stop'; & %s ;exit $LastExitCode}", tmpFileName)
-		//scriptPath = newPath
-		//script.opts = append(script.opts, psCommand)
-	} else {
-		script.opts = append(script.opts, tmpFileName)
-		switch x := fs.(type) {
-		case *afero.BasePathFs:
-			script.dir, err = x.RealPath(basedir.Name())
-			if err != nil {
-				return nil, fmt.Errorf("unable to get script working dir: %v", err)
-			}
-		default:
-			script.dir = basedir.Name()
+	var realScriptPath = scriptPath
+
+	switch x := fs.(type) {
+	case *afero.BasePathFs:
+		realScriptPath, err = x.RealPath(tmpscript.Name())
+		if err != nil {
+			return nil, fmt.Errorf("unable to get script working dir: %v", err)
+		}
+		realScriptPath, err = filepath.Abs(realScriptPath)
+		if err != nil {
+			return nil, fmt.Errorf("unable to get script working dir: %v", err)
 		}
 	}
+
+	if isWindows() {
+		//This aims to stop a the very first error and return the right exit code
+		psCommand := fmt.Sprintf("& { $ErrorActionPreference='Stop'; & %s ;exit $LastExitCode}", realScriptPath)
+		script.opts = append(script.opts, psCommand)
+	} else {
+		script.opts = append(script.opts, realScriptPath)
+	}
+
+	log.Debug("writeScriptContent> script realpath is %s", realScriptPath)
+	log.Debug("writeScriptContent> script directory is %s", script.dir)
+
 	deferFunc := func() {
-		if err := fs.Remove(tmpFileName); err != nil {
-			log.Error(ctx, "unable to remove %s: %v", tmpFileName, err)
+		filename := filepath.Join(path.Dir(basedir.Name()), tmpFileName)
+		log.Debug("writeScriptContent> removing file %s", filename)
+		if err := fs.Remove(filename); err != nil {
+			log.Error(ctx, "unable to remove %s: %v", filename, err)
 		}
 	}
 
@@ -164,17 +184,11 @@ func RunScriptAction(ctx context.Context, wk workerruntime.Runtime, a sdk.Action
 
 	go func() {
 		res := sdk.Result{Status: sdk.StatusSuccess}
-		script, err := prepareScriptContent(a.Parameters)
+		script, err := prepareScriptContent(a.Parameters, wk.BaseDir(), workdir)
 		if err != nil {
 			chanErr <- err
 		}
 
-		if x, ok := wk.BaseDir().(*afero.BasePathFs); ok {
-			script.dir, _ = x.RealPath(workdir.Name())
-		} else {
-			script.dir = workdir.Name()
-		}
-
 		deferFunc, err := writeScriptContent(ctx, script, wk.BaseDir(), workdir)
 		if deferFunc != nil {
 			defer deferFunc()
@@ -186,6 +200,7 @@ func RunScriptAction(ctx context.Context, wk workerruntime.Runtime, a sdk.Action
 		log.Info(ctx, "runScriptAction> Running command %s %s in %s", script.shell, strings.Trim(fmt.Sprint(script.opts), "[]"), script.dir)
 		cmd := exec.CommandContext(ctx, script.shell, script.opts...)
 		res.Status = sdk.StatusUnknown
+
 		cmd.Dir = script.dir
 		cmd.Env = wk.Environ()
 
diff --git a/engine/worker/internal/action/builtin_serve_static_files.go b/engine/worker/internal/action/builtin_serve_static_files.go
index aa472ffca..6a37cb609 100644
--- a/engine/worker/internal/action/builtin_serve_static_files.go
+++ b/engine/worker/internal/action/builtin_serve_static_files.go
@@ -42,10 +42,14 @@ func RunServeStaticFiles(ctx context.Context, wk workerruntime.Runtime, a sdk.Ac
 	} else {
 		abs = workdir.Name()
 	}
-	wkDirFS := afero.NewBasePathFs(afero.NewOsFs(), abs)
 
-	// Global all files matching filePath
-	filesPath, err := afero.Glob(wkDirFS, path)
+	completePath := path
+	if !filepath.IsAbs(path) {
+		completePath = filepath.Join(abs, path)
+	}
+
+	aferoFS := afero.NewOsFs()
+	filesPath, err := afero.Glob(aferoFS, completePath)
 	if err != nil {
 		return res, fmt.Errorf("cannot perform globbing of pattern '%s': %s", path, err)
 	}
@@ -61,7 +65,7 @@ func RunServeStaticFiles(ctx context.Context, wk workerruntime.Runtime, a sdk.Ac
 
 	// To set entrypoint dynamically when the path is a single file
 	if entrypoint.Value == "" && len(filesPath) == 1 {
-		fileStat, errS := wkDirFS.Stat(filesPath[0])
+		fileStat, errS := aferoFS.Stat(filesPath[0])
 		if errS != nil {
 			return res, fmt.Errorf("cannot stat file %s : %v", filesPath[0], errS)
 		}
@@ -75,7 +79,7 @@ func RunServeStaticFiles(ctx context.Context, wk workerruntime.Runtime, a sdk.Ac
 	}
 
 	wk.SendLog(ctx, workerruntime.LevelInfo, "Fetching files in progress...")
-	file, _, err := sdk.CreateTarFromPaths(wkDirFS, path, filesPath, &sdk.TarOptions{TrimDirName: filepath.Dir(path)})
+	file, _, err := sdk.CreateTarFromPaths(aferoFS, path, filesPath, &sdk.TarOptions{TrimDirName: filepath.Dir(path)})
 	if err != nil {
 		return res, fmt.Errorf("cannot tar files: %v", err)
 	}
diff --git a/engine/worker/internal/action/grpc_plugin.go b/engine/worker/internal/action/grpc_plugin.go
index 62e18e8fc..1722829bd 100644
--- a/engine/worker/internal/action/grpc_plugin.go
+++ b/engine/worker/internal/action/grpc_plugin.go
@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"io"
 	"os"
+	"path"
 	"strings"
 
 	"github.com/golang/protobuf/ptypes/empty"
@@ -73,7 +74,7 @@ func enablePluginLogger(ctx context.Context, done chan struct{}, c *pluginClient
 	}
 }
 
-func RunGRPCPlugin(ctx context.Context, actionName string, params []sdk.Parameter, w workerruntime.Runtime, chanRes chan sdk.Result, done chan struct{}) {
+func RunGRPCPlugin(ctx context.Context, actionName string, params []sdk.Parameter, action sdk.Action, w workerruntime.Runtime, chanRes chan sdk.Result, done chan struct{}) {
 	//For the moment we consider that plugin name = action name
 	pluginName := actionName
 
@@ -93,7 +94,7 @@ func RunGRPCPlugin(ctx context.Context, actionName string, params []sdk.Paramete
 		envs = append(envs, fmt.Sprintf("%s=%s", envName, p.Value))
 	}
 
-	pluginSocket, err := startGRPCPlugin(context.Background(), pluginName, w, nil, startGRPCPluginOptions{
+	pluginSocket, err := startGRPCPlugin(ctx, pluginName, w, nil, startGRPCPluginOptions{
 		envs: envs,
 	})
 	if err != nil {
@@ -144,7 +145,7 @@ func RunGRPCPlugin(ctx context.Context, actionName string, params []sdk.Paramete
 		return
 	}
 	query := actionplugin.ActionQuery{
-		Options: sdk.ParametersMapMerge(sdk.ParametersToMap(params), sdk.ParametersToMap(params), sdk.MapMergeOptions.ExcludeGitParams),
+		Options: sdk.ParametersMapMerge(sdk.ParametersToMap(params), sdk.ParametersToMap(action.Parameters), sdk.MapMergeOptions.ExcludeGitParams),
 		JobID:   jobID,
 	}
 
@@ -231,19 +232,36 @@ func startGRPCPlugin(ctx context.Context, pluginName string, w workerruntime.Run
 		}
 	}
 
+	var basedir string
+	if x, ok := w.BaseDir().(*afero.BasePathFs); ok {
+		basedir, _ = x.RealPath(".")
+	} else {
+		basedir = w.BaseDir().Name()
+	}
+
 	cmd := binary.Cmd
 	if _, err := sdk.LookPath(w.BaseDir(), cmd); err != nil {
-		return nil, sdk.WrapError(err, "plugin:%s unable to start GRPC plugin, binary command not found.", pluginName)
+		return nil, sdk.WrapError(err, "plugin:%s unable to find GRPC plugin, binary command not found.", pluginName)
+	}
+	cmd = path.Join(basedir, cmd)
+
+	for i := range binary.Entrypoints {
+		binary.Entrypoints[i] = path.Join(basedir, binary.Entrypoints[i])
 	}
 	args := append(binary.Entrypoints, binary.Args...)
 	var errstart error
 
+	workdir, err := workerruntime.WorkingDirectory(ctx)
+	if err != nil {
+		return nil, err
+	}
 	var dir string
 	if x, ok := w.BaseDir().(*afero.BasePathFs); ok {
-		dir, _ = x.RealPath(".")
+		dir, _ = x.RealPath(workdir.Name())
 	} else {
-		dir = w.BaseDir().Name()
+		dir = workdir.Name()
 	}
+
 	if c.StdPipe, c.Socket, errstart = grpcplugin.StartPlugin(ctx, pluginName, dir, cmd, args, envs); errstart != nil {
 		return nil, sdk.WrapError(errstart, "plugin:%s unable to start GRPC plugin... Aborting", pluginName)
 	}
diff --git a/engine/worker/internal/action/vcs_strategy.go b/engine/worker/internal/action/vcs_strategy.go
index 3cf33114c..802d9f161 100644
--- a/engine/worker/internal/action/vcs_strategy.go
+++ b/engine/worker/internal/action/vcs_strategy.go
@@ -33,7 +33,7 @@ func vcsStrategy(ctx context.Context, wk workerruntime.Runtime, params []sdk.Par
 		}
 		privateKeyVar := sdk.Variable{
 			Name:  "cds.key." + keyName.Value + ".priv",
-			Type:  "string",
+			Type:  sdk.KeyTypeSSH,
 			Value: privateKey.Value,
 		}
 
diff --git a/engine/worker/internal/builtin.go b/engine/worker/internal/builtin.go
index cc0dba460..82a12587c 100644
--- a/engine/worker/internal/builtin.go
+++ b/engine/worker/internal/builtin.go
@@ -54,7 +54,7 @@ func (w *CurrentWorker) runGRPCPlugin(ctx context.Context, a sdk.Action) sdk.Res
 	chanRes := make(chan sdk.Result, 1)
 	done := make(chan struct{})
 	sdk.GoRoutine(ctx, "runGRPCPlugin", func(ctx context.Context) {
-		action.RunGRPCPlugin(ctx, a.Name, w.currentJob.params, w, chanRes, done)
+		action.RunGRPCPlugin(ctx, a.Name, w.currentJob.params, a, w, chanRes, done)
 	})
 
 	select {
diff --git a/engine/worker/internal/handler_cache.go b/engine/worker/internal/handler_cache.go
index 2aee919d0..4524f9398 100644
--- a/engine/worker/internal/handler_cache.go
+++ b/engine/worker/internal/handler_cache.go
@@ -10,13 +10,11 @@ import (
 	"net/http"
 	"os"
 	"path/filepath"
-	"strings"
 	"time"
 
 	"github.com/gorilla/mux"
 	"github.com/spf13/afero"
 
-	"github.com/ovh/cds/engine/worker/pkg/workerruntime"
 	"github.com/ovh/cds/sdk"
 	"github.com/ovh/cds/sdk/log"
 )
@@ -47,29 +45,7 @@ func cachePushHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFunc {
 			return
 		}
 
-		workdir, errWk := workerruntime.WorkingDirectory(wk.currentJob.context)
-		if errWk != nil {
-			err := sdk.Error{
-				Message: "worker cache push > Cannot get worker working directory : " + errWk.Error(),
-				Status:  http.StatusInternalServerError,
-			}
-			log.Error(ctx, "%v", err)
-			writeError(w, r, err)
-			return
-		}
-		var abs string
-		if x, ok := wk.BaseDir().(*afero.BasePathFs); ok {
-			abs, _ = x.RealPath(workdir.Name())
-		} else {
-			abs = workdir.Name()
-		}
-		wkDirFS := afero.NewBasePathFs(afero.NewOsFs(), abs)
-
-		for i := range c.Files {
-			c.Files[i] = strings.TrimPrefix(c.Files[i], abs)
-		}
-
-		res, size, errTar := sdk.CreateTarFromPaths(wkDirFS, c.WorkingDirectory, c.Files, nil)
+		res, size, errTar := sdk.CreateTarFromPaths(afero.NewOsFs(), c.WorkingDirectory, c.Files, nil)
 		if errTar != nil {
 			errTar = sdk.Error{
 				Message: fmt.Sprintf("worker cache push > Cannot tar (%+v) : %v", c.Files, errTar.Error()),
diff --git a/engine/worker/internal/handler_upload.go b/engine/worker/internal/handler_upload.go
index 28b72cded..8db4e24e9 100644
--- a/engine/worker/internal/handler_upload.go
+++ b/engine/worker/internal/handler_upload.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"io/ioutil"
 	"net/http"
+	"path/filepath"
 
 	"github.com/ovh/cds/engine/worker/internal/action"
 	"github.com/ovh/cds/engine/worker/pkg/workerruntime"
@@ -22,18 +23,23 @@ func uploadHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFunc {
 			return
 		}
 
-		var art sdk.WorkflowNodeRunArtifact
+		var art workerruntime.UploadArtifact
 		if err := json.Unmarshal(data, &art); err != nil {
 			w.WriteHeader(http.StatusBadRequest)
 			return
 		}
 
+		artifactPath := art.Name
+		if !sdk.PathIsAbs(artifactPath) {
+			artifactPath = filepath.Join(art.WorkingDirectory, art.Name)
+		}
+
 		a := sdk.Action{
 			Parameters: []sdk.Parameter{
 				{
 					Name:  "path",
 					Type:  sdk.StringParameter,
-					Value: art.Name,
+					Value: artifactPath,
 				},
 				{
 					Name:  "tag",
@@ -57,6 +63,7 @@ func uploadHandler(ctx context.Context, wk *CurrentWorker) http.HandlerFunc {
 			return
 		}
 		ctx = workerruntime.SetWorkingDirectory(ctx, workingDir)
+
 		result, err := action.RunArtifactUpload(ctx, wk, a, wk.currentJob.secrets)
 		if err != nil {
 			wk.SendLog(ctx, workerruntime.LevelError, fmt.Sprintf("Artifact upload failed: %v", err))
diff --git a/engine/worker/internal/logger.go b/engine/worker/internal/logger.go
index c3692a3b8..af77575ad 100644
--- a/engine/worker/internal/logger.go
+++ b/engine/worker/internal/logger.go
@@ -76,7 +76,9 @@ func (wk *CurrentWorker) sendHTTPLog(ctx context.Context, jobID int64) {
 
 	for _, l := range logs {
 		log.Debug("LOG: %v", l.Val)
-		if err := wk.Client().QueueSendLogs(wk.currentJob.context, jobID, *l); err != nil {
+		// TODO: stop the worker a nice way,
+		// for the moment we are using context.Background and not the job context
+		if err := wk.Client().QueueSendLogs(context.Background(), jobID, *l); err != nil {
 			log.Error(ctx, "error: cannot send logs: %s", err)
 			continue
 		}
diff --git a/engine/worker/internal/requirement.go b/engine/worker/internal/requirement.go
index 43032e84f..de7df2f73 100644
--- a/engine/worker/internal/requirement.go
+++ b/engine/worker/internal/requirement.go
@@ -6,7 +6,6 @@ import (
 	"net"
 	"os"
 	"os/exec"
-	"path"
 	"strconv"
 	"strings"
 	"time"
@@ -254,11 +253,11 @@ func checkPluginDeployment(ctx context.Context, w *CurrentWorker, job sdk.Workfl
 	}
 
 	// then try to download the plugin
-	integrationPluginBinary := path.Join(w.BaseDir().Name(), binary.Name)
-	if _, err := w.BaseDir().Stat(integrationPluginBinary); os.IsNotExist(err) {
+	//integrationPluginBinary := path.Join(w.BaseDir().Name(), binary.Name)
+	if _, err := w.BaseDir().Stat(binary.Name); os.IsNotExist(err) {
 		log.Debug("Downloading the plugin %s", binary.PluginName)
 		//If the file doesn't exist. Download it.
-		fi, err := os.OpenFile(integrationPluginBinary, os.O_CREATE|os.O_RDWR, os.FileMode(binary.Perm))
+		fi, err := w.BaseDir().OpenFile(binary.Name, os.O_CREATE|os.O_RDWR, os.FileMode(binary.Perm))
 		if err != nil {
 			return false, err
 		}
diff --git a/engine/worker/internal/run.go b/engine/worker/internal/run.go
index 5e88c27f7..c58976a6f 100644
--- a/engine/worker/internal/run.go
+++ b/engine/worker/internal/run.go
@@ -123,6 +123,17 @@ func (w *CurrentWorker) runJob(ctx context.Context, a *sdk.Action, jobID int64,
 		if nCriticalFailed == 0 || step.AlwaysExecuted {
 			stepResult = w.runAction(ctx, step, jobID, secrets, step.Name)
 
+			// Check if all newVariables are in currentJob.params
+			// variable can be add in w.currentJob.newVariables by worker command export
+			for _, newVariableFromHandler := range w.currentJob.newVariables {
+				p := sdk.ParameterFind(w.currentJob.params, newVariableFromHandler.Name)
+				if p == nil {
+					w.currentJob.params = append(w.currentJob.params, newVariableFromHandler.ToParameter(""))
+				} else {
+					p.Value = newVariableFromHandler.Value
+				}
+			}
+
 			for _, newVariable := range stepResult.NewVariables {
 				// append the new variable from a step to the following steps
 				w.currentJob.params = append(w.currentJob.params, newVariable.ToParameter("cds.build"))
@@ -259,6 +270,17 @@ func (w *CurrentWorker) runSteps(ctx context.Context, steps []sdk.Action, a sdk.
 			r.Status = sdk.StatusNeverBuilt
 		}
 
+		// Check if all newVariables are in currentJob.params
+		// variable can be add in w.currentJob.newVariables by worker command export
+		for _, newVariableFromHandler := range w.currentJob.newVariables {
+			p := sdk.ParameterFind(w.currentJob.params, newVariableFromHandler.Name)
+			if p == nil {
+				w.currentJob.params = append(w.currentJob.params, newVariableFromHandler.ToParameter(""))
+			} else {
+				p.Value = newVariableFromHandler.Value
+			}
+		}
+
 		for _, newVariable := range r.NewVariables {
 			// append the new variable from a chile to the following children
 			w.currentJob.params = append(w.currentJob.params, newVariable.ToParameter("cds.build"))
@@ -318,7 +340,13 @@ func setupWorkingDirectory(ctx context.Context, fs afero.Fs, wd string) (afero.F
 		}
 	}
 
-	if err := os.Setenv("HOME", wd); err != nil {
+	var absWD string
+	if x, ok := fs.(*afero.BasePathFs); ok {
+		absWD, _ = x.RealPath(wd)
+	} else {
+		absWD = wd
+	}
+	if err := os.Setenv("HOME", absWD); err != nil {
 		return nil, err
 	}
 
@@ -329,8 +357,8 @@ func setupWorkingDirectory(ctx context.Context, fs afero.Fs, wd string) (afero.F
 	return fi, nil
 }
 
-func teardownDirectory(fs afero.Fs, wd string) error {
-	return fs.RemoveAll(wd)
+func teardownDirectory(fs afero.Fs, dir string) error {
+	return fs.RemoveAll(dir)
 }
 
 func workingDirectory(ctx context.Context, fs afero.Fs, jobInfo sdk.WorkflowNodeJobRunData, suffixes ...string) (string, error) {
@@ -516,11 +544,18 @@ func (w *CurrentWorker) ProcessJob(jobInfo sdk.WorkflowNodeJobRunData) (sdk.Resu
 		log.Debug("processJob> new variables: %v", res.NewVariables)
 	}
 
+	// Delete working directory
 	if err := teardownDirectory(w.basedir, wdFile.Name()); err != nil {
 		log.Error(ctx, "Cannot remove build directory: %s", err)
 	}
-	if err := teardownDirectory(w.basedir, wdFile.Name()); err != nil {
+	// Delelete key directory
+	if err := teardownDirectory(w.basedir, kdFile.Name()); err != nil {
 		log.Error(ctx, "Cannot remove keys directory: %s", err)
 	}
+	// Delete all plugins
+	if err := teardownDirectory(w.basedir, ""); err != nil {
+		log.Error(ctx, "Cannot remove basedir content: %s", err)
+	}
+
 	return res, err
 }
diff --git a/engine/worker/internal/take.go b/engine/worker/internal/take.go
index ab7cf27fb..46e43b004 100644
--- a/engine/worker/internal/take.go
+++ b/engine/worker/internal/take.go
@@ -20,6 +20,8 @@ func (w *CurrentWorker) Take(ctx context.Context, job sdk.WorkflowNodeJobRun) er
 	t := ""
 	log.Info(ctx, "takeWorkflowJob> Job %d taken%s", job.ID, t)
 
+	ctx, cancel := context.WithCancel(ctx)
+
 	w.currentJob.context = workerruntime.SetJobID(ctx, job.ID)
 	w.currentJob.context = ctx
 
@@ -32,7 +34,6 @@ func (w *CurrentWorker) Take(ctx context.Context, job sdk.WorkflowNodeJobRun) er
 	start := time.Now()
 
 	//This goroutine try to get the job every 5 seconds, if it fails, it cancel the build.
-	ctx, cancel := context.WithCancel(ctx)
 	tick := time.NewTicker(5 * time.Second)
 	go func(cancel context.CancelFunc, jobID int64, tick *time.Ticker) {
 		var nbConnrefused int
@@ -106,7 +107,7 @@ func (w *CurrentWorker) Take(ctx context.Context, job sdk.WorkflowNodeJobRun) er
 			log.Info(ctx, "takeWorkflowJob> Cannot send build result: HTTP %v - worker cancelled - giving up", lasterr)
 			return nil
 		}
-		log.Warning(ctx, "takeWorkflowJob> Cannot send build result: HTTP %v - try: %d - new try in 15s", lasterr, try)
+		log.Warning(ctx, "takeWorkflowJob> Cannot send build result for job id %d: HTTP %v - try: %d - new try in 15s", job.ID, lasterr, try)
 		time.Sleep(15 * time.Second)
 	}
 	log.Error(ctx, "takeWorkflowJob> Could not send built result 10 times, giving up. job: %d", job.ID)
diff --git a/engine/worker/pkg/workerruntime/types.go b/engine/worker/pkg/workerruntime/types.go
index 0de4a77d1..ab560aadb 100644
--- a/engine/worker/pkg/workerruntime/types.go
+++ b/engine/worker/pkg/workerruntime/types.go
@@ -20,6 +20,12 @@ type DownloadArtifact struct {
 	Destination string `json:"destination"`
 }
 
+type UploadArtifact struct {
+	Name             string `json:"name"`
+	Tag              string `json:"tag"`
+	WorkingDirectory string `json:"working_directory"`
+}
+
 type FilePath struct {
 	Path string `json:"path"`
 }
diff --git a/sdk/cache.go b/sdk/cache.go
index c43fd2311..d1be77ca3 100644
--- a/sdk/cache.go
+++ b/sdk/cache.go
@@ -53,13 +53,20 @@ func CreateTarFromPaths(fs afero.Fs, cwd string, paths []string, opts *TarOption
 	// Create a new tar archive.
 	tw := tar.NewWriter(buf)
 
-	for _, path := range paths {
+	for _, p := range paths {
 		// ensure the src actually exists before trying to tar it
-		if _, err := fs.Stat(path); err != nil {
-			return nil, 0, fmt.Errorf("Unable to tar files - %v", err.Error())
+
+		completePath := p
+		if !PathIsAbs(p) {
+			completePath = filepath.Join(cwd, p)
+		}
+
+		if _, err := fs.Stat(completePath); err != nil {
+			return nil, 0, fmt.Errorf("unable to tar files - %v", err.Error())
 		}
+
 		// walk path
-		errWalk := afero.Walk(fs, path, func(file string, fi os.FileInfo, err error) error {
+		errWalk := afero.Walk(fs, completePath, func(file string, fi os.FileInfo, err error) error {
 			if err != nil {
 				return err
 			}
@@ -70,21 +77,21 @@ func CreateTarFromPaths(fs afero.Fs, cwd string, paths []string, opts *TarOption
 				return err
 			}
 
-			header.Name = strings.TrimPrefix(strings.Replace(file, cwd, "", -1), string(filepath.Separator))
+			header.Name = strings.TrimPrefix(strings.TrimPrefix(file, cwd), string(filepath.Separator))
 
 			if opts != nil && opts.TrimDirName != "" {
 				opts.TrimDirName = strings.TrimPrefix(opts.TrimDirName, string(filepath.Separator))
 				header.Name = strings.TrimPrefix(strings.TrimPrefix(header.Name, opts.TrimDirName), string(filepath.Separator))
 			}
 			if fi.Mode()&os.ModeSymlink != 0 {
+
 				symlink, errEval := filepath.EvalSymlinks(file)
 				if errEval != nil {
 					return errEval
 				}
-				abs, errAbs := filepath.Abs(filepath.Dir(header.Name))
-				if errAbs != nil {
-					return errAbs
-				}
+
+				abs := filepath.Dir(filepath.Join(cwd, header.Name))
+
 				symlinkRel, errRel := filepath.Rel(abs, symlink)
 				if errRel != nil {
 					return errRel
diff --git a/sdk/common.go b/sdk/common.go
index dfe3fc5da..02f9e31ef 100644
--- a/sdk/common.go
+++ b/sdk/common.go
@@ -11,8 +11,10 @@ import (
 	"fmt"
 	"io"
 	"os"
+	"path"
 	"reflect"
 	"regexp"
+	"runtime"
 	"strings"
 	"unicode"
 
@@ -269,3 +271,12 @@ func RemoveNotPrintableChar(in string) string {
 	}
 	return strings.Map(m, in)
 }
+
+var windowsPathRegex = regexp.MustCompile(`^[a-zA-Z]:\\[\\\S|*\S]?.*$`)
+
+func PathIsAbs(s string) bool {
+	if GOOS == "windows" || runtime.GOOS == "windows" {
+		return windowsPathRegex.MatchString(s)
+	}
+	return path.IsAbs(s)
+}
diff --git a/sdk/hatchery/hatchery.go b/sdk/hatchery/hatchery.go
index 9a621f928..e68b6c882 100644
--- a/sdk/hatchery/hatchery.go
+++ b/sdk/hatchery/hatchery.go
@@ -389,7 +389,7 @@ func canRunJobWithModel(ctx context.Context, h InterfaceWithModels, j workerStar
 
 // SendSpawnInfo sends a spawnInfo
 func SendSpawnInfo(ctx context.Context, h Interface, jobID int64, spawnMsg sdk.SpawnMsg) {
-	if h.CDSClient() == nil {
+	if h.CDSClient() == nil || jobID == 0 {
 		return
 	}
 	infos := []sdk.SpawnInfo{{RemoteTime: time.Now(), Message: spawnMsg}}
diff --git a/sdk/hatchery/worker_token.go b/sdk/hatchery/worker_token.go
index b89257369..866fcb025 100644
--- a/sdk/hatchery/worker_token.go
+++ b/sdk/hatchery/worker_token.go
@@ -21,6 +21,13 @@ func NewWorkerToken(hatcheryName string, privateKey *rsa.PrivateKey, expiration
 		},
 	}
 
+	// FIXME create dedicated struct with only required fields for the token
+	if claims.Worker.Model != nil {
+		claims.Worker.Model = &sdk.Model{
+			ID: claims.Worker.Model.ID,
+		}
+	}
+
 	jwtoken := jwt.NewWithClaims(jwt.SigningMethodRS512, claims)
 	signedJWToken, err := jwtoken.SignedString(privateKey)
 	if err != nil {
diff --git a/sdk/variable.go b/sdk/variable.go
index b75670655..f4c52e317 100644
--- a/sdk/variable.go
+++ b/sdk/variable.go
@@ -15,8 +15,12 @@ type Variable struct {
 }
 
 func (v Variable) ToParameter(prefix string) Parameter {
+	name := v.Name
+	if prefix != "" {
+		name = fmt.Sprintf("%s.%s", prefix, v.Name)
+	}
 	return Parameter{
-		Name:  "." + prefix + "." + v.Name,
+		Name:  name,
 		Value: v.Value,
 		Type:  v.Type,
 	}
diff --git a/sdk/vcs/git/git_clone.go b/sdk/vcs/git/git_clone.go
index df46fc907..5c3409676 100644
--- a/sdk/vcs/git/git_clone.go
+++ b/sdk/vcs/git/git_clone.go
@@ -112,6 +112,8 @@ func prepareGitCloneCommands(repo, workdirPath, path string, opts *CloneOpts) (s
 			if path == "" {
 				t := strings.Split(repo, "/")
 				fetchCmd.workdir = filepath.Join(workdirPath, strings.TrimSuffix(t[len(t)-1], ".git"))
+			} else if strings.HasPrefix(path, "/") {
+				fetchCmd.workdir = path
 			} else {
 				fetchCmd.workdir = filepath.Join(workdirPath, path)
 			}
@@ -129,6 +131,8 @@ func prepareGitCloneCommands(repo, workdirPath, path string, opts *CloneOpts) (s
 		if path == "" {
 			t := strings.Split(repo, "/")
 			resetCmd.workdir = filepath.Join(workdirPath, strings.TrimSuffix(t[len(t)-1], ".git"))
+		} else if strings.HasPrefix(path, "/") {
+			resetCmd.workdir = path
 		} else {
 			resetCmd.workdir = filepath.Join(workdirPath, path)
 		}
diff --git a/sdk/workflow_data.go b/sdk/workflow_data.go
index 193fc3b97..3254e4193 100644
--- a/sdk/workflow_data.go
+++ b/sdk/workflow_data.go
@@ -63,6 +63,9 @@ func (w *WorkflowData) Array() []*Node {
 
 func (w *WorkflowData) Maps() map[int64]*Node {
 	nodes := make(map[int64]*Node, 0)
+	if w == nil {
+		return nodes
+	}
 	nodes = w.Node.maps(nodes)
 	for i := range w.Joins {
 		nodes = w.Joins[i].maps(nodes)
@@ -71,6 +74,9 @@ func (w *WorkflowData) Maps() map[int64]*Node {
 }
 
 func (w *WorkflowData) NodeByRef(ref string) *Node {
+	if w == nil {
+		return nil
+	}
 	n := (&w.Node).nodeByRef(ref)
 	if n != nil {
 		return n
@@ -85,6 +91,9 @@ func (w *WorkflowData) NodeByRef(ref string) *Node {
 }
 
 func (w *WorkflowData) NodeByID(ID int64) *Node {
+	if w == nil {
+		return nil
+	}
 	n := (&w.Node).nodeByID(ID)
 	if n != nil {
 		return n
diff --git a/tools/smtpmock/.gitignore b/tools/smtpmock/.gitignore
new file mode 100644
index 000000000..6e021aa29
--- /dev/null
+++ b/tools/smtpmock/.gitignore
@@ -0,0 +1,1 @@
+server/server
\ No newline at end of file
diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html
index b0f58b5b4..8c2df9bb3 100644
--- a/ui/src/app/app.component.html
+++ b/ui/src/app/app.component.html
@@ -3,7 +3,7 @@
         <div id="navbar" *ngIf="isConnected && !hideNavBar" [class.connected]="isConnected">
             <app-navbar></app-navbar>
         </div>
-        <div class="maintenance banner pointing" [class.mt50]="isConnected" *ngIf="maintenance && (!isConnected || user?.ring == 'ADMIN')">
+        <div class="maintenance banner" [class.mt50]="isConnected" *ngIf="maintenance && (!isConnected || user?.ring == 'ADMIN')">
             <span>{{ 'maintenance_title' | translate }}</span>
             <img src="assets/images/maintenance.svg"/>
         </div>
diff --git a/ui/src/app/shared/table/data-table.html b/ui/src/app/shared/table/data-table.html
index 58d70c296..50de03ba4 100644
--- a/ui/src/app/shared/table/data-table.html
+++ b/ui/src/app/shared/table/data-table.html
@@ -76,8 +76,11 @@
                     </ng-container>
                     <ng-container *ngSwitchCase="'time-ago'"> {{c.selector | amTimeAgo}} </ng-container>
                     <ng-container *ngSwitchCase="'date'"> {{c.selector | amCalendar}} </ng-container>
-                    <button class="ui primary button small" *ngSwitchCase="'button'" (click)="c.selector.click()">{{c.selector.title
-                        | translate}}</button>
+                    <button class="ui button" [ngClass]="c.selector.class" *ngSwitchCase="'button'"
+                        (click)="c.selector.click()">
+                        {{c.selector.title | translate}}
+                        <i class="icon" [ngClass]="c.selector.icon" *ngIf="c.selector.icon"></i>
+                    </button>
                     <app-confirm-button *ngSwitchCase="'confirm-button'" [class]="'small'"
                         [color]="c.selector.color ? c.selector.color : 'primary'" (event)="c.selector.click()"
                         [title]="c.selector.title">
@@ -131,4 +134,4 @@
             </td>
         </tr>
     </tfoot>
-</table>
\ No newline at end of file
+</table>
diff --git a/ui/src/app/store/applications.state.spec.ts b/ui/src/app/store/applications.state.spec.ts
index aa0ec4110..49491fee6 100644
--- a/ui/src/app/store/applications.state.spec.ts
+++ b/ui/src/app/store/applications.state.spec.ts
@@ -295,7 +295,7 @@ describe('Applications', () => {
         let overview = new Overview();
         overview.git_url = 'git+ssh://thisisatest';
         http.expectOne(((req: HttpRequest<any>) => {
-            return req.url === '/project/test1/application/app1/overview';
+            return req.url === '/ui/project/test1/application/app1/overview';
         })).flush(overview);
         store.selectOnce(ApplicationsState).subscribe(state => {
             expect(Object.keys(state.overviews).length).toEqual(1);
diff --git a/ui/src/app/store/applications.state.ts b/ui/src/app/store/applications.state.ts
index fa98374eb..19976adaf 100644
--- a/ui/src/app/store/applications.state.ts
+++ b/ui/src/app/store/applications.state.ts
@@ -195,7 +195,7 @@ export class ApplicationsState {
         const appKey = action.payload.projectKey + '/' + action.payload.applicationName;
 
         return this._http.get<Overview>(
-            `/project/${action.payload.projectKey}/application/${action.payload.applicationName}/overview`
+            `/ui/project/${action.payload.projectKey}/application/${action.payload.applicationName}/overview`
         ).pipe(tap((overview) => {
             ctx.setState({
                 ...state,
diff --git a/ui/src/app/views/workflow/run/node/artifact/artifact.list.component.ts b/ui/src/app/views/workflow/run/node/artifact/artifact.list.component.ts
index 0c34db8a5..affee72ab 100644
--- a/ui/src/app/views/workflow/run/node/artifact/artifact.list.component.ts
+++ b/ui/src/app/views/workflow/run/node/artifact/artifact.list.component.ts
@@ -31,7 +31,7 @@ export class WorkflowRunArtifactListComponent {
                 selector: (a: WorkflowNodeRunArtifact) => {
                     let size = this.getHumainFileSize(a.size);
                     return {
-                        link: `/cdsapi/workflow/artifact/${a.download_hash}`,
+                        link: `./cdsapi/workflow/artifact/${a.download_hash}`,
                         value: `${a.name} (${size})`
                     };
                 }
diff --git a/ui/src/app/views/workflow/show/workflow.html b/ui/src/app/views/workflow/show/workflow.html
index 43ce9046f..d8dc9921a 100644
--- a/ui/src/app/views/workflow/show/workflow.html
+++ b/ui/src/app/views/workflow/show/workflow.html
@@ -69,7 +69,7 @@
                                         </div>
                                         <div class="right aligned five wide column" title="{{'btn_run_workflow' | translate}}">
                                             <div class="ui green buttons">
-                                                <div class="ui button" [class.disabled]="!detailedWorkflow.permissions.writable"
+                                                <div class="ui button" [class.disabled]="!detailedWorkflow.permissions.executable"
                                                     (click)="runWithParameter()" [class.loading]="loading">
                                                     {{'btn_run_workflow' | translate }}
                                                 </div>


Test patch:
diff --git a/engine/api/ascode_test.go b/engine/api/ascode_test.go
index 1ea17c0e8..a6486d857 100644
--- a/engine/api/ascode_test.go
+++ b/engine/api/ascode_test.go
@@ -124,6 +124,7 @@ func Test_getImportAsCodeHandler(t *testing.T) {
 	api, db, _, end := newTestAPI(t)
 	defer end()
 
+	p := assets.InsertTestProject(t, db, api.Cache, sdk.RandomString(10), sdk.RandomString(10))
 	u, pass := assets.InsertAdminUser(t, db)
 
 	_, _ = assets.InsertService(t, db, "Test_getImportAsCodeHandler", services.TypeRepositories)
@@ -164,7 +165,8 @@ func Test_getImportAsCodeHandler(t *testing.T) {
 	)
 
 	uri := api.Router.GetRoute("GET", api.getImportAsCodeHandler, map[string]string{
-		"uuid": UUID,
+		"permProjectKey": p.Key,
+		"uuid":           UUID,
 	})
 	req, err := http.NewRequest("GET", uri, nil)
 	test.NoError(t, err)
diff --git a/engine/hatchery/marathon/helper_test.go b/engine/hatchery/marathon/helper_test.go
index 627c79b48..6fa2473e7 100644
--- a/engine/hatchery/marathon/helper_test.go
+++ b/engine/hatchery/marathon/helper_test.go
@@ -28,6 +28,7 @@ func InitMarathonMarathonTest(opts marathonJDD) *HatcheryMarathon {
 	h.marathonClient, _ = marathon.NewClient(config)
 	h.Config.Provision.MaxConcurrentProvisioning = opts.MaxProvision
 	h.Config.Provision.MaxWorker = opts.MaxWorker
+	h.Config.DefaultCPUs = 1
 	if opts.Prefix != "" {
 		h.Config.MarathonIDPrefix = opts.Prefix
 	}
diff --git a/engine/hatchery/marathon/marathon_test.go b/engine/hatchery/marathon/marathon_test.go
index 9f42c4708..d963824b6 100644
--- a/engine/hatchery/marathon/marathon_test.go
+++ b/engine/hatchery/marathon/marathon_test.go
@@ -296,7 +296,7 @@ func TestSpawnWorkerTimeout(t *testing.T) {
 				assert.NoError(t, json.Unmarshal(bodyContent, &a))
 				assert.Equal(t, "DOCKER", a.Container.Type)
 				assert.Equal(t, "BRIDGE", a.Container.Docker.Network)
-				assert.Equal(t, float64(2), a.CPUs)
+				assert.Equal(t, float64(1), a.CPUs)
 				assert.Equal(t, 1, *a.Instances)
 				assert.Equal(t, "1", (*a.Env)["CDS_BOOKED_WORKFLOW_JOB_ID"])
 				assert.Equal(t, "GroupModel/fake", (*a.Env)["CDS_MODEL_PATH"])
diff --git a/engine/hatchery/swarm/swarm_test.go b/engine/hatchery/swarm/swarm_test.go
index a30cba9be..6570964c9 100644
--- a/engine/hatchery/swarm/swarm_test.go
+++ b/engine/hatchery/swarm/swarm_test.go
@@ -85,7 +85,7 @@ func TestHatcherySwarm_ListAwolWorker(t *testing.T) {
 	h.Config.Name = "swarmy"
 
 	now := time.Now()
-	d1h := now.Add(-2 * time.Minute)
+	d1h := now.Add(-5 * time.Minute)
 	containers := []types.Container{
 		{
 			ID:    "swarmy-model1-w1",
diff --git a/engine/worker/internal/action/builtin_coverage_test.go b/engine/worker/internal/action/builtin_coverage_test.go
index dedf9af2c..1f5781c58 100644
--- a/engine/worker/internal/action/builtin_coverage_test.go
+++ b/engine/worker/internal/action/builtin_coverage_test.go
@@ -3,13 +3,16 @@ package action
 import (
 	"bytes"
 	"encoding/json"
-	"github.com/ovh/cds/sdk/cdsclient"
-	"github.com/stretchr/testify/require"
 	"io/ioutil"
 	"net/http"
 	"os"
+	"path/filepath"
 	"testing"
 
+	"github.com/ovh/cds/sdk/cdsclient"
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/require"
+
 	"github.com/sguiheux/go-coverage"
 	"gopkg.in/h2non/gock.v1"
 
@@ -41,12 +44,17 @@ func TestRunCoverageWrongFormat(t *testing.T) {
 	assert.Equal(t, sdk.StatusFail, res.Status)
 }
 
-func TestRunCoverage(t *testing.T) {
+func TestRunCoverage_Absolute(t *testing.T) {
 	defer gock.Off()
 
 	wk, ctx := setupTest(t)
 	assert.NoError(t, ioutil.WriteFile("results.xml", []byte(cobertura_result), os.ModePerm))
 
+	fi, err := os.Open("results.xml")
+	require.NoError(t, err)
+	fiPath, err := filepath.Abs(fi.Name())
+	require.NoError(t, err)
+
 	gock.New("http://lolcat.host").Post("/queue/workflows/666/coverage").
 		Reply(200)
 
@@ -78,7 +86,61 @@ func TestRunCoverage(t *testing.T) {
 			Parameters: []sdk.Parameter{
 				{
 					Name:  "path",
-					Value: "./results.xml",
+					Value: fiPath,
+				},
+				{
+					Name:  "format",
+					Value: "cobertura",
+				},
+				{
+					Name:  "minimum",
+					Value: "10",
+				},
+			},
+		}, nil)
+	assert.NoError(t, err)
+	assert.Equal(t, sdk.StatusSuccess, res.Status)
+}
+
+func TestRunCoverage_Relative(t *testing.T) {
+	defer gock.Off()
+
+	wk, ctx := setupTest(t)
+	fname := filepath.Join(wk.workingDirectory.Name(), "results.xml")
+	require.NoError(t, afero.WriteFile(wk.BaseDir(), fname, []byte(cobertura_result), os.ModePerm))
+
+	gock.New("http://lolcat.host").Post("/queue/workflows/666/coverage").
+		Reply(200)
+
+	var checkRequest gock.ObserverFunc = func(request *http.Request, mock gock.Mock) {
+		bodyContent, err := ioutil.ReadAll(request.Body)
+		assert.NoError(t, err)
+		request.Body = ioutil.NopCloser(bytes.NewReader(bodyContent))
+		if mock != nil {
+			t.Logf("%s %s - Body: %s", mock.Request().Method, mock.Request().URLStruct.String(), string(bodyContent))
+			switch mock.Request().URLStruct.String() {
+			case "http://lolcat.host/queue/workflows/666/coverage":
+				var report coverage.Report
+				err := json.Unmarshal(bodyContent, &report)
+				assert.NoError(t, err)
+				require.Equal(t, 8, report.TotalLines)
+				require.Equal(t, 6, report.CoveredLines)
+				require.Equal(t, 4, report.TotalBranches)
+				require.Equal(t, 2, report.CoveredBranches)
+			}
+		}
+	}
+
+	gock.Observe(checkRequest)
+
+	gock.InterceptClient(wk.Client().(cdsclient.Raw).HTTPClient())
+	gock.InterceptClient(wk.Client().(cdsclient.Raw).HTTPSSEClient())
+	res, err := RunParseCoverageResultAction(ctx, wk,
+		sdk.Action{
+			Parameters: []sdk.Parameter{
+				{
+					Name:  "path",
+					Value: "results.xml",
 				},
 				{
 					Name:  "format",
@@ -100,6 +162,11 @@ func TestRunCoverageMinimumFail(t *testing.T) {
 	wk, ctx := setupTest(t)
 	assert.NoError(t, ioutil.WriteFile("results.xml", []byte(cobertura_result), os.ModePerm))
 
+	fi, err := os.Open("results.xml")
+	require.NoError(t, err)
+	fiPath, err := filepath.Abs(fi.Name())
+	require.NoError(t, err)
+
 	gock.New("http://lolcat.host").Post("/queue/workflows/666/coverage").
 		Reply(200)
 
@@ -131,7 +198,7 @@ func TestRunCoverageMinimumFail(t *testing.T) {
 			Parameters: []sdk.Parameter{
 				{
 					Name:  "path",
-					Value: "./results.xml",
+					Value: fiPath,
 				},
 				{
 					Name:  "format",
diff --git a/engine/worker/internal/action/builtin_gitclone_test.go b/engine/worker/internal/action/builtin_gitclone_test.go
index 7d6ac833f..7fa401dff 100644
--- a/engine/worker/internal/action/builtin_gitclone_test.go
+++ b/engine/worker/internal/action/builtin_gitclone_test.go
@@ -55,7 +55,7 @@ func TestRunGitCloneInSSHWithPrivateKey(t *testing.T) {
 		},
 		[]sdk.Variable{
 			{
-				Name:  "proj-ssh-key",
+				Name:  "cds.key.proj-ssh-key.priv",
 				Value: string(test.TestKey),
 			},
 		})
@@ -83,7 +83,7 @@ func TestRunGitCloneInSSHWithTheWrongPrivateKeyShouldFail(t *testing.T) {
 		},
 		[]sdk.Variable{
 			{
-				Name:  "proj-ssh-key",
+				Name:  "cds.key.proj-ssh-key.priv",
 				Value: "this not a private key",
 			},
 		})
@@ -111,7 +111,7 @@ func TestRunGitCloneInSSHWithPrivateKeyWithTargetDirectory(t *testing.T) {
 		},
 		[]sdk.Variable{
 			{
-				Name:  "proj-ssh-key",
+				Name:  "cds.key.proj-ssh-key.priv",
 				Value: string(test.TestKey),
 			},
 		})
@@ -152,7 +152,7 @@ func TestRunGitCloneInSSHWithPrivateKeyAndExtractInfo(t *testing.T) {
 		},
 		[]sdk.Variable{
 			{
-				Name:  "proj-ssh-key",
+				Name:  "cds.key.proj-ssh-key.priv",
 				Value: string(test.TestKey),
 			},
 		})
@@ -256,6 +256,33 @@ func TestRunGitCloneInHTTPSWithoutAuth(t *testing.T) {
 	assert.DirExists(t, filepath.Join(wk.workingDirectory.File.Name(), "dummy-empty-repo", ".git"))
 }
 
-func TestRunGitCloneInHTTPSWithAuth(t *testing.T) {
+func TestRunGitCloneWithSecret(t *testing.T) {
+	wk, ctx := setupTest(t)
+	res, err := RunGitClone(ctx, wk,
+		sdk.Action{
+			Parameters: []sdk.Parameter{
+				{
+					Name:  "url",
+					Value: "git@github.com:fsamin/dummy-empty-repo.git",
+				},
+				{
+					Name:  "privateKey",
+					Value: "proj-ssh-key",
+				},
+				{
+					Name:  "directory",
+					Value: ".",
+				},
+			},
+		}, []sdk.Variable{
+			{
+				Name:  "cds.key.proj-ssh-key.priv",
+				Value: string(test.TestKey),
+				Type:  sdk.KeyTypeSSH,
+			},
+		})
+	assert.NoError(t, err)
+	assert.Equal(t, sdk.StatusSuccess, res.Status)
 
+	assert.DirExists(t, filepath.Join(wk.workingDirectory.File.Name(), ".git"))
 }
diff --git a/engine/worker/internal/action/builtin_junit_test.go b/engine/worker/internal/action/builtin_junit_test.go
index 0ffd0d952..8c6e537d3 100644
--- a/engine/worker/internal/action/builtin_junit_test.go
+++ b/engine/worker/internal/action/builtin_junit_test.go
@@ -1,14 +1,159 @@
 package action
 
 import (
+	"bytes"
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"path/filepath"
 	"reflect"
 	"testing"
 
 	"github.com/ovh/venom"
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"gopkg.in/h2non/gock.v1"
 
 	"github.com/ovh/cds/sdk"
+	"github.com/ovh/cds/sdk/cdsclient"
 )
 
+func TestRunParseJunitTestResultAction_Absolute(t *testing.T) {
+	fileContent := `<?xml version="1.0" encoding="UTF-8"?>
+	<testsuites>
+	   <testsuite name="JUnitXmlReporter" errors="0" tests="0" failures="0" time="0" timestamp="2013-05-24T10:23:58" />
+	   <testsuite name="JUnitXmlReporter.constructor" errors="0" skipped="1" tests="3" failures="1" time="0.006" timestamp="2013-05-24T10:23:58">
+		  <properties>
+			 <property name="java.vendor" value="Sun Microsystems Inc." />
+			 <property name="compiler.debug" value="on" />
+			 <property name="project.jdk.classpath" value="jdk.classpath.1.6" />
+		  </properties>
+		  <testcase classname="JUnitXmlReporter.constructor" name="should default path to an empty string" time="0.006">
+			 <failure message="test failure">Assertion failed</failure>
+		  </testcase>
+		  <testcase classname="JUnitXmlReporter.constructor" name="should default consolidate to true" time="0">
+			 <skipped />
+		  </testcase>
+		  <testcase classname="JUnitXmlReporter.constructor" name="should default useDotNotation to true" time="0" />
+	   </testsuite>
+	</testsuites>`
+
+	defer gock.Off()
+
+	wk, ctx := setupTest(t)
+	assert.NoError(t, ioutil.WriteFile("results.xml", []byte(fileContent), os.ModePerm))
+
+	fi, err := os.Open("results.xml")
+	require.NoError(t, err)
+	fiPath, err := filepath.Abs(fi.Name())
+	require.NoError(t, err)
+
+	gock.New("http://lolcat.host").Post("/queue/workflows/666/test").
+		Reply(200)
+
+	var checkRequest gock.ObserverFunc = func(request *http.Request, mock gock.Mock) {
+		bodyContent, err := ioutil.ReadAll(request.Body)
+		assert.NoError(t, err)
+		request.Body = ioutil.NopCloser(bytes.NewReader(bodyContent))
+		if mock != nil {
+			t.Logf("%s %s - Body: %s", mock.Request().Method, mock.Request().URLStruct.String(), string(bodyContent))
+			switch mock.Request().URLStruct.String() {
+			case "http://lolcat.host/queue/workflows/666/test":
+				var report venom.Tests
+				err := json.Unmarshal(bodyContent, &report)
+				assert.NoError(t, err)
+				assert.Equal(t, 3, report.Total)
+				assert.Equal(t, 1, report.TotalKO)
+				assert.Equal(t, 1, report.TotalSkipped)
+			}
+		}
+	}
+
+	gock.Observe(checkRequest)
+
+	gock.InterceptClient(wk.Client().(cdsclient.Raw).HTTPClient())
+	gock.InterceptClient(wk.Client().(cdsclient.Raw).HTTPSSEClient())
+	res, err := RunParseJunitTestResultAction(ctx, wk,
+		sdk.Action{
+			Parameters: []sdk.Parameter{
+				{
+					Name:  "path",
+					Value: fiPath,
+				},
+			},
+		}, nil)
+	assert.NoError(t, err)
+	assert.Equal(t, sdk.StatusFail, res.Status)
+
+}
+
+func TestRunParseJunitTestResultAction_Relative(t *testing.T) {
+	fileContent := `<?xml version="1.0" encoding="UTF-8"?>
+	<testsuites>
+	   <testsuite name="JUnitXmlReporter" errors="0" tests="0" failures="0" time="0" timestamp="2013-05-24T10:23:58" />
+	   <testsuite name="JUnitXmlReporter.constructor" errors="0" skipped="1" tests="3" failures="1" time="0.006" timestamp="2013-05-24T10:23:58">
+		  <properties>
+			 <property name="java.vendor" value="Sun Microsystems Inc." />
+			 <property name="compiler.debug" value="on" />
+			 <property name="project.jdk.classpath" value="jdk.classpath.1.6" />
+		  </properties>
+		  <testcase classname="JUnitXmlReporter.constructor" name="should default path to an empty string" time="0.006">
+			 <failure message="test failure">Assertion failed</failure>
+		  </testcase>
+		  <testcase classname="JUnitXmlReporter.constructor" name="should default consolidate to true" time="0">
+			 <skipped />
+		  </testcase>
+		  <testcase classname="JUnitXmlReporter.constructor" name="should default useDotNotation to true" time="0" />
+	   </testsuite>
+	</testsuites>`
+
+	defer gock.Off()
+
+	wk, ctx := setupTest(t)
+	fname := filepath.Join(wk.workingDirectory.Name(), "results.xml")
+	require.NoError(t, afero.WriteFile(wk.BaseDir(), fname, []byte(fileContent), os.ModePerm))
+
+	gock.New("http://lolcat.host").Post("/queue/workflows/666/test").
+		Reply(200)
+
+	var checkRequest gock.ObserverFunc = func(request *http.Request, mock gock.Mock) {
+		bodyContent, err := ioutil.ReadAll(request.Body)
+		assert.NoError(t, err)
+		request.Body = ioutil.NopCloser(bytes.NewReader(bodyContent))
+		if mock != nil {
+			t.Logf("%s %s - Body: %s", mock.Request().Method, mock.Request().URLStruct.String(), string(bodyContent))
+			switch mock.Request().URLStruct.String() {
+			case "http://lolcat.host/queue/workflows/666/test":
+				var report venom.Tests
+				err := json.Unmarshal(bodyContent, &report)
+				assert.NoError(t, err)
+				assert.Equal(t, 3, report.Total)
+				assert.Equal(t, 1, report.TotalKO)
+				assert.Equal(t, 1, report.TotalSkipped)
+			}
+		}
+	}
+
+	gock.Observe(checkRequest)
+
+	gock.InterceptClient(wk.Client().(cdsclient.Raw).HTTPClient())
+	gock.InterceptClient(wk.Client().(cdsclient.Raw).HTTPSSEClient())
+	res, err := RunParseJunitTestResultAction(ctx, wk,
+		sdk.Action{
+			Parameters: []sdk.Parameter{
+				{
+					Name:  "path",
+					Value: "results.xml",
+				},
+			},
+		}, nil)
+	assert.NoError(t, err)
+	assert.Equal(t, sdk.StatusFail, res.Status)
+
+}
+
 func Test_ComputeStats(t *testing.T) {
 	type args struct {
 		res *sdk.Result
diff --git a/engine/worker/internal/action/builtin_script_test.go b/engine/worker/internal/action/builtin_script_test.go
index 4b222d501..1dd60b2d3 100644
--- a/engine/worker/internal/action/builtin_script_test.go
+++ b/engine/worker/internal/action/builtin_script_test.go
@@ -7,6 +7,7 @@ import (
 	"github.com/ovh/cds/sdk/log"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func init() {
@@ -71,7 +72,8 @@ echo "lol"`,
 
 	for _, tst := range tests {
 		t.Run(tst.name, func(t *testing.T) {
-			script, err := prepareScriptContent(tst.parameters)
+			wk, _ := setupTest(t)
+			script, err := prepareScriptContent(tst.parameters, wk.BaseDir(), wk.workingDirectory)
 			if tst.shouldHaveError {
 				assert.Error(t, err)
 			} else {
@@ -82,7 +84,6 @@ echo "lol"`,
 			}
 		})
 	}
-
 }
 
 func TestRunScriptAction(t *testing.T) {
@@ -105,3 +106,29 @@ func TestRunScriptAction(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Equal(t, sdk.StatusSuccess, res.Status)
 }
+
+func Test_writeScriptContent_windows(t *testing.T) {
+	sdk.GOOS = "windows"
+	defer func() {
+		sdk.GOOS = ""
+	}()
+
+	wk, ctx := setupTest(t)
+
+	script, err := prepareScriptContent([]sdk.Parameter{
+		{
+			Name:  "script",
+			Value: "sleep 1\necho this is a test from %HOME%\nsleep 1",
+		},
+	}, wk.BaseDir(), wk.workingDirectory)
+	require.NoError(t, err)
+	require.NotNil(t, script)
+
+	deferFunc, err := writeScriptContent(ctx, script, wk.BaseDir(), wk.workingDirectory)
+	if deferFunc != nil {
+		defer deferFunc()
+	}
+	require.NoError(t, err)
+
+	t.Logf("script: %+v", script)
+}
diff --git a/sdk/common_test.go b/sdk/common_test.go
index 595b67a4a..acd62275e 100644
--- a/sdk/common_test.go
+++ b/sdk/common_test.go
@@ -32,3 +32,12 @@ func TestRemoveNotPrintableChar(t *testing.T) {
 		})
 	}
 }
+
+func TestPathIsAbs(t *testing.T) {
+	GOOS = "windows"
+	assert.True(t, PathIsAbs(`C:\Program Files (x86)\Foo`))
+	assert.False(t, PathIsAbs(`Program Files (x86)\Foo`))
+	GOOS = "linux"
+	assert.True(t, PathIsAbs(`/tmp`))
+	assert.False(t, PathIsAbs(`tmp`))
+}
diff --git a/tests/fixtures/04SCWorkflowRunWorkerCmd/pipeline.yml b/tests/fixtures/04SCWorkflowRunWorkerCmd/pipeline.yml
index 9960a9485..d6fcd5273 100644
--- a/tests/fixtures/04SCWorkflowRunWorkerCmd/pipeline.yml
+++ b/tests/fixtures/04SCWorkflowRunWorkerCmd/pipeline.yml
@@ -15,6 +15,8 @@ jobs:
       - this a a line in the file, with a CDS variable of type password {{.cds.app.password}}
       - EOF
       - worker check-secret {{.cds.workspace}}/myFile
+    - script:
+      - sleep 5
   requirements:
   - os-architecture: linux/amd64
 - job: EXIT
@@ -62,6 +64,8 @@ jobs:
       - EOF
       - worker tmpl {{.cds.workspace}}/myFile {{.cds.workspace}}/outputFile
       - cat {{.cds.workspace}}/outputFile
+    - script:
+        - sleep 5
   requirements:
   - os-architecture: linux/amd64
 - job: UPLOAD
@@ -82,6 +86,8 @@ jobs:
       - set -ex
       - worker key install --env proj-ssh-04scworkflowrunworkercmd --file id_rsa
       - cat id_rsa
+    - script:
+        - sleep 5
   requirements:
   - os-architecture: linux/amd64
 - job: ARTIFACTLIST
@@ -90,6 +96,7 @@ jobs:
     - script:
       - set -ex
       - worker artifacts
+    - script:
       - sleep 5
   requirements:
   - os-architecture: linux/amd64
@@ -100,6 +107,7 @@ jobs:
       - set -ex
       - worker cache pull latest
       - cat fileCache
+    - script:
       - sleep 5
   requirements:
   - os-architecture: linux/amd64
@@ -110,6 +118,7 @@ jobs:
       - set -ex
       - worker download
       - cat myFileUploaded
+    - script:
       - sleep 5
   requirements:
   - os-architecture: linux/amd64
@@ -119,6 +128,7 @@ jobs:
     - script:
       - set -ex
       - echo "Export value {{.cds.build.foo}}"
+    - script:
       - sleep 5
   requirements:
   - os-architecture: linux/amd64
diff --git a/tests/fixtures/ITSCWRKFLW6/clone.pip.yml b/tests/fixtures/ITSCWRKFLW6/clone.pip.yml
index 25ba00137..d9fa34dc6 100644
--- a/tests/fixtures/ITSCWRKFLW6/clone.pip.yml
+++ b/tests/fixtures/ITSCWRKFLW6/clone.pip.yml
@@ -10,6 +10,7 @@ jobs:
       tag: '{{.git.tag}}'
       url: https://github.com/ovh/venom.git
   - script:
+    - ls
     - git log
     - cat README.md
   requirements:
diff --git a/ui/src/app/views/workflow/run/node/test/table/test.table.component.ts b/ui/src/app/views/workflow/run/node/test/table/test.table.component.ts
index be6a764e6..7cb981e1c 100644
--- a/ui/src/app/views/workflow/run/node/test/table/test.table.component.ts
+++ b/ui/src/app/views/workflow/run/node/test/table/test.table.component.ts
@@ -1,10 +1,10 @@
 import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit, ViewChild } from '@angular/core';
-import { Failure, TestCase, Tests } from '../../../../../../model/pipeline.model';
-
 import { ThemeStore } from 'app/service/services.module';
 import { Column, ColumnType, Filter } from 'app/shared/table/data-table.component';
 import { Subscription } from 'rxjs';
 import { finalize } from 'rxjs/operators';
+import { Failure, TestCase, Tests } from '../../../../../../model/pipeline.model';
+
 
 @Component({
     selector: 'app-workflow-test-table',
@@ -46,7 +46,7 @@ export class WorkflowRunTestTableComponent implements OnInit {
                     tc.fullname = ts.name + ' / ' + tc.name;
                     if (!tc.errors && !tc.failures) {
                         tc.status = 'success';
-                    } else if ( (tc.errors && tc.errors.length > 0) || (tc.failures && tc.failures.length > 0)) {
+                    } else if ((tc.errors && tc.errors.length > 0) || (tc.failures && tc.failures.length > 0)) {
                         tc.status = 'failed';
                     } else {
                         tc.status = 'skipped';