Repository: ovh/cds
Pull request #6133 (2022-04-05 07:42:43)

refactor(ui): remove sm-menu
1. Description
1. Related issues
1. About tests
1. Mentions

@ovh/cds


ovh-cds: 
CDS Report build-all-cds#17280.0 ✘  
* Build
  * Build and Package All ✔ 
  * Build Debpacker ✔ 
  * Build OS-Ansible-Inventory ✔ 
  * Build SMTPMock ✔ 
  * Lint UI Translate ✔ 
  * Test CLI ✔ 
  * Test Engine ✔ 
  * Test SDK ✔ 
  * Test SDK Rust ✔ 
  * Test UI ✘  


Patch:
diff --git a/Makefile b/Makefile
index be2b82e08..47a25903b 100644
--- a/Makefile
+++ b/Makefile
@@ -115,3 +115,5 @@ tidy:
 	done;
 	@echo "Running tidy on tests/fixtures/04SCWorkflowRunSimplePlugin"
 	@(cd tests/fixtures/04SCWorkflowRunSimplePlugin && go mod tidy)
+	@(cd sdk/interpolate && go mod tidy)
+	@(cd tools/smtpmock && go mod tidy)
diff --git a/cli/cdsctl/experimental.go b/cli/cdsctl/experimental.go
new file mode 100644
index 000000000..a615d152a
--- /dev/null
+++ b/cli/cdsctl/experimental.go
@@ -0,0 +1,24 @@
+package main
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/ovh/cds/cli"
+)
+
+var experimentalCmd = cli.Command{
+	Name:    "experimental",
+	Aliases: []string{"exp"},
+	Short:   "CDS Experimental commands",
+	Hidden:  true,
+}
+
+func experimentalCommands() []*cobra.Command {
+	return []*cobra.Command{
+		experimentalProject(),
+	}
+}
+
+func experimental() *cobra.Command {
+	return cli.NewCommand(experimentalCmd, nil, experimentalCommands())
+}
diff --git a/cli/cdsctl/experimental_project.go b/cli/cdsctl/experimental_project.go
new file mode 100644
index 000000000..b080d9a0c
--- /dev/null
+++ b/cli/cdsctl/experimental_project.go
@@ -0,0 +1,18 @@
+package main
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/ovh/cds/cli"
+)
+
+var experimentalProjectCmd = cli.Command{
+	Name:  "project",
+	Short: "CDS Experimental project commands",
+}
+
+func experimentalProject() *cobra.Command {
+	return cli.NewCommand(experimentalProjectCmd, nil, []*cobra.Command{
+		projectVCS(),
+	})
+}
diff --git a/cli/cdsctl/experimental_project_vcs.go b/cli/cdsctl/experimental_project_vcs.go
new file mode 100644
index 000000000..e9b58a233
--- /dev/null
+++ b/cli/cdsctl/experimental_project_vcs.go
@@ -0,0 +1,114 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"os"
+
+	"github.com/spf13/cobra"
+
+	yaml "github.com/ghodss/yaml"
+	"github.com/ovh/cds/cli"
+	"github.com/ovh/cds/sdk/cdsclient"
+)
+
+var projectVCSCmd = cli.Command{
+	Name:    "vcs",
+	Aliases: []string{"vcs"},
+	Short:   "Manage VCS on a CDS project",
+}
+
+func projectVCS() *cobra.Command {
+	return cli.NewCommand(projectVCSCmd, nil, []*cobra.Command{
+		cli.NewListCommand(projectVCSListCmd, projectVCSListFunc, nil, withAllCommandModifiers()...),
+		cli.NewDeleteCommand(projectVCSDeleteCmd, projectVCSDeleteFunc, nil, withAllCommandModifiers()...),
+		cli.NewCommand(projectVCSImportCmd, projectVCSImportFunc, nil, withAllCommandModifiers()...),
+		cli.NewCommand(projectVCSExportCmd, projectVCSExportFunc, nil, withAllCommandModifiers()...),
+	})
+}
+
+var projectVCSListCmd = cli.Command{
+	Name:  "list",
+	Short: "List VCS available on a project",
+	Ctx: []cli.Arg{
+		{Name: _ProjectKey},
+	},
+}
+
+func projectVCSListFunc(v cli.Values) (cli.ListResult, error) {
+	pfs, err := client.ProjectVCSList(context.Background(), v.GetString(_ProjectKey))
+	return cli.AsListResult(pfs), err
+}
+
+var projectVCSDeleteCmd = cli.Command{
+	Name:  "delete",
+	Short: "Delete a VCS configuration on a project",
+	Ctx: []cli.Arg{
+		{Name: _ProjectKey},
+	},
+	Args: []cli.Arg{
+		{Name: "name"},
+	},
+}
+
+func projectVCSDeleteFunc(v cli.Values) error {
+	return client.ProjectVCSDelete(context.Background(), v.GetString(_ProjectKey), v.GetString("name"))
+}
+
+var projectVCSImportCmd = cli.Command{
+	Name:    "import",
+	Short:   "Import a VCS configuration on a project from a yaml file",
+	Example: "cdsctl project vcs import MY-PROJECT file.yml",
+	Ctx: []cli.Arg{
+		{Name: _ProjectKey},
+	},
+	Args: []cli.Arg{
+		{Name: "filename"},
+	},
+	Flags: []cli.Flag{
+		{Name: "force", Type: cli.FlagBool},
+	},
+}
+
+func projectVCSImportFunc(v cli.Values) error {
+	f, err := os.Open(v.GetString("filename"))
+	if err != nil {
+		return cli.WrapError(err, "unable to open file %s", v.GetString("filename"))
+	}
+	defer f.Close()
+
+	var mods []cdsclient.RequestModifier
+	if v.GetBool("force") {
+		mods = append(mods, cdsclient.Force())
+	}
+
+	_, err = client.ProjectVCSImport(context.Background(), v.GetString(_ProjectKey), f, mods...)
+	return err
+}
+
+var projectVCSExportCmd = cli.Command{
+	Name:    "export",
+	Short:   "Export a VCS configuration from a project to stdout",
+	Example: "cdsctl vcs export MY-PROJECT MY-VCS-SERVER-NAME > file.yaml",
+	Ctx: []cli.Arg{
+		{Name: _ProjectKey},
+	},
+	Args: []cli.Arg{
+		{Name: "name"},
+	},
+}
+
+func projectVCSExportFunc(v cli.Values) error {
+	pf, err := client.ProjectVCSGet(context.Background(), v.GetString(_ProjectKey), v.GetString("name"))
+	if err != nil {
+		return err
+	}
+
+	btes, err := yaml.Marshal(pf)
+	if err != nil {
+		return err
+	}
+
+	fmt.Println(string(btes))
+	return nil
+}
diff --git a/cli/cdsctl/main.go b/cli/cdsctl/main.go
index 29313c74e..419fd0762 100644
--- a/cli/cdsctl/main.go
+++ b/cli/cdsctl/main.go
@@ -28,6 +28,7 @@ func main() {
 		encrypt(),
 		contexts(),
 		environment(),
+		experimental(),
 		events(),
 		group(),
 		health(),
diff --git a/cli/cdsctl/project_integration.go b/cli/cdsctl/project_integration.go
index 2339336d0..9807b2342 100644
--- a/cli/cdsctl/project_integration.go
+++ b/cli/cdsctl/project_integration.go
@@ -62,7 +62,7 @@ func projectIntegrationDeleteFunc(v cli.Values) error {
 var projectIntegrationImportCmd = cli.Command{
 	Name:    "import",
 	Short:   "Import a integration configuration on a project from a yaml file",
-	Example: "cdsctl integration import MY-PROJECT file.yml",
+	Example: "cdsctl project integration import MY-PROJECT file.yml",
 	Ctx: []cli.Arg{
 		{Name: _ProjectKey},
 	},
@@ -93,7 +93,7 @@ func projectIntegrationImportFunc(v cli.Values) error {
 var projectIntegrationExportCmd = cli.Command{
 	Name:    "export",
 	Short:   "Export a integration configuration from a project to stdout",
-	Example: "cdsctl integration export MY-PROJECT MY-INTEGRATION-NAME > file.yaml",
+	Example: "cdsctl project integration export MY-PROJECT MY-INTEGRATION-NAME > file.yaml",
 	Ctx: []cli.Arg{
 		{Name: _ProjectKey},
 	},
diff --git a/contrib/grpcplugins/action/plugin-archive/go.sum b/contrib/grpcplugins/action/plugin-archive/go.sum
index 77e04ce48..da476fafa 100644
--- a/contrib/grpcplugins/action/plugin-archive/go.sum
+++ b/contrib/grpcplugins/action/plugin-archive/go.sum
@@ -112,7 +112,6 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
-github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
 github.com/buger/goterm v0.0.0-20170918171949-d443b9114f9c/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U=
 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
diff --git a/contrib/grpcplugins/action/plugin-artifactory-release-bundle-create/go.sum b/contrib/grpcplugins/action/plugin-artifactory-release-bundle-create/go.sum
index 20b933f5f..16879e530 100644
--- a/contrib/grpcplugins/action/plugin-artifactory-release-bundle-create/go.sum
+++ b/contrib/grpcplugins/action/plugin-artifactory-release-bundle-create/go.sum
@@ -113,7 +113,6 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm
 github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
-github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
 github.com/buger/goterm v0.0.0-20170918171949-d443b9114f9c/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U=
 github.com/buger/jsonparser v0.0.0-20180910192245-6acdf747ae99/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
 github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
diff --git a/contrib/grpcplugins/action/plugin-artifactory-release-bundle-distribute/go.sum b/contrib/grpcplugins/action/plugin-artifactory-release-bundle-distribute/go.sum
index b5843c7d0..3270d5a93 100644
--- a/contrib/grpcplugins/action/plugin-artifactory-release-bundle-distribute/go.sum
+++ b/contrib/grpcplugins/action/plugin-artifactory-release-bundle-distribute/go.sum
@@ -111,7 +111,6 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
-github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
 github.com/buger/goterm v0.0.0-20170918171949-d443b9114f9c/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U=
 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
diff --git a/contrib/grpcplugins/action/plugin-artifactory-release-bundle-distribute/main.go b/contrib/grpcplugins/action/plugin-artifactory-release-bundle-distribute/main.go
index d46771ae7..deebfd713 100644
--- a/contrib/grpcplugins/action/plugin-artifactory-release-bundle-distribute/main.go
+++ b/contrib/grpcplugins/action/plugin-artifactory-release-bundle-distribute/main.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"os"
+	"time"
 
 	"github.com/golang/protobuf/ptypes/empty"
 	"github.com/jfrog/jfrog-client-go/distribution/services"
@@ -34,7 +35,7 @@ func (actPlugin *artifactoryReleaseBundleDistributePlugin) Manifest(_ context.Co
 	}, nil
 }
 
-func (actPlugin *artifactoryReleaseBundleDistributePlugin) Run(_ context.Context, q *actionplugin.ActionQuery) (*actionplugin.ActionResult, error) {
+func (actPlugin *artifactoryReleaseBundleDistributePlugin) Run(ctx context.Context, q *actionplugin.ActionQuery) (*actionplugin.ActionResult, error) {
 	name := q.GetOptions()["name"]
 	version := q.GetOptions()["version"]
 	url := q.GetOptions()["url"]
@@ -63,8 +64,11 @@ func (actPlugin *artifactoryReleaseBundleDistributePlugin) Run(_ context.Context
 		return actionplugin.Fail("missing Artifactory Distribution Token")
 	}
 
+	ctx, cancel := context.WithTimeout(ctx, 15*time.Minute)
+	defer cancel()
+
 	log.SetLogger(log.NewLogger(log.INFO, os.Stdout))
-	distriClient, err := art.CreateDistributionClient(url, token)
+	distriClient, err := art.CreateDistributionClient(ctx, url, token)
 	if err != nil {
 		return actionplugin.Fail("unable to create distribution client: %v", err)
 	}
diff --git a/contrib/grpcplugins/action/plugin-download/go.sum b/contrib/grpcplugins/action/plugin-download/go.sum
index 13f877387..2f2f4b6ec 100644
--- a/contrib/grpcplugins/action/plugin-download/go.sum
+++ b/contrib/grpcplugins/action/plugin-download/go.sum
@@ -111,7 +111,6 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
-github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
 github.com/buger/goterm v0.0.0-20170918171949-d443b9114f9c/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U=
 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
diff --git a/contrib/grpcplugins/action/plugin-group-tmpl/go.sum b/contrib/grpcplugins/action/plugin-group-tmpl/go.sum
index 13f877387..2f2f4b6ec 100644
--- a/contrib/grpcplugins/action/plugin-group-tmpl/go.sum
+++ b/contrib/grpcplugins/action/plugin-group-tmpl/go.sum
@@ -111,7 +111,6 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
-github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
 github.com/buger/goterm v0.0.0-20170918171949-d443b9114f9c/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U=
 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
diff --git a/contrib/grpcplugins/action/plugin-kafka-publish/go.sum b/contrib/grpcplugins/action/plugin-kafka-publish/go.sum
index 618a3e80e..6c7bd9045 100644
--- a/contrib/grpcplugins/action/plugin-kafka-publish/go.sum
+++ b/contrib/grpcplugins/action/plugin-kafka-publish/go.sum
@@ -122,7 +122,6 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
-github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
 github.com/buger/goterm v0.0.0-20170918171949-d443b9114f9c/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U=
 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
diff --git a/contrib/grpcplugins/action/plugin-marathon/go.mod b/contrib/grpcplugins/action/plugin-marathon/go.mod
index 43392d2e2..0a4e7ead2 100644
--- a/contrib/grpcplugins/action/plugin-marathon/go.mod
+++ b/contrib/grpcplugins/action/plugin-marathon/go.mod
@@ -32,6 +32,7 @@ require (
 	github.com/emirpasic/gods v1.12.0 // indirect
 	github.com/fatih/color v1.13.0 // indirect
 	github.com/fsamin/go-dump v1.0.9 // indirect
+	github.com/ghodss/yaml v1.0.0 // indirect
 	github.com/go-git/gcfg v1.5.0 // indirect
 	github.com/go-git/go-billy/v5 v5.3.1 // indirect
 	github.com/go-git/go-git/v5 v5.4.2 // indirect
diff --git a/contrib/grpcplugins/action/plugin-marathon/go.sum b/contrib/grpcplugins/action/plugin-marathon/go.sum
index 4457a36e8..849fc684e 100644
--- a/contrib/grpcplugins/action/plugin-marathon/go.sum
+++ b/contrib/grpcplugins/action/plugin-marathon/go.sum
@@ -116,7 +116,6 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
-github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
 github.com/buger/goterm v0.0.0-20170918171949-d443b9114f9c/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U=
 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -213,6 +212,7 @@ github.com/fujiwara/shapeio v0.0.0-20170602072123-c073257dd745/go.mod h1:/WpqsrS
 github.com/gambol99/go-marathon v0.0.0-20170922093320-ec4a50170df7/go.mod h1:GLyXJD41gBO/NPKVPGQbhyyC06eugGy15QEZyUkE2/s=
 github.com/gambol99/go-marathon v0.7.1 h1:/dnwXQ0W0UDScpvmcdjzRz3ssnJ/5ieX/q4Xi/QHOn4=
 github.com/gambol99/go-marathon v0.7.1/go.mod h1:GLyXJD41gBO/NPKVPGQbhyyC06eugGy15QEZyUkE2/s=
+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
 github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
diff --git a/contrib/grpcplugins/action/plugin-npm-audit-parser/go.sum b/contrib/grpcplugins/action/plugin-npm-audit-parser/go.sum
index 13f877387..2f2f4b6ec 100644
--- a/contrib/grpcplugins/action/plugin-npm-audit-parser/go.sum
+++ b/contrib/grpcplugins/action/plugin-npm-audit-parser/go.sum
@@ -111,7 +111,6 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
-github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
 github.com/buger/goterm v0.0.0-20170918171949-d443b9114f9c/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U=
 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
diff --git a/contrib/grpcplugins/action/plugin-ssh-cmd/go.sum b/contrib/grpcplugins/action/plugin-ssh-cmd/go.sum
index 13f877387..2f2f4b6ec 100644
--- a/contrib/grpcplugins/action/plugin-ssh-cmd/go.sum
+++ b/contrib/grpcplugins/action/plugin-ssh-cmd/go.sum
@@ -111,7 +111,6 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
-github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
 github.com/buger/goterm v0.0.0-20170918171949-d443b9114f9c/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U=
 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
diff --git a/contrib/grpcplugins/action/plugin-tmpl/go.sum b/contrib/grpcplugins/action/plugin-tmpl/go.sum
index 13f877387..2f2f4b6ec 100644
--- a/contrib/grpcplugins/action/plugin-tmpl/go.sum
+++ b/contrib/grpcplugins/action/plugin-tmpl/go.sum
@@ -111,7 +111,6 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
-github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
 github.com/buger/goterm v0.0.0-20170918171949-d443b9114f9c/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U=
 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
diff --git a/contrib/grpcplugins/action/plugin-venom/go.sum b/contrib/grpcplugins/action/plugin-venom/go.sum
index de2264eb1..dee68fee5 100644
--- a/contrib/grpcplugins/action/plugin-venom/go.sum
+++ b/contrib/grpcplugins/action/plugin-venom/go.sum
@@ -116,7 +116,6 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
-github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
 github.com/bsm/sarama-cluster v2.1.15+incompatible h1:RkV6WiNRnqEEbp81druK8zYhmnIgdOjqSVi0+9Cnl2A=
 github.com/bsm/sarama-cluster v2.1.15+incompatible/go.mod h1:r7ao+4tTNXvWm+VRpRJchr2kQhqxgmAp2iEX5W96gMM=
 github.com/buger/goterm v0.0.0-20170918171949-d443b9114f9c/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U=
diff --git a/contrib/integrations/artifactory/artifactory.go b/contrib/integrations/artifactory/artifactory.go
index 80e302c92..5c7df992e 100644
--- a/contrib/integrations/artifactory/artifactory.go
+++ b/contrib/integrations/artifactory/artifactory.go
@@ -1,6 +1,7 @@
 package art
 
 import (
+	"context"
 	"fmt"
 	"net/url"
 	"strings"
@@ -50,7 +51,7 @@ type DistribClient struct {
 	ServiceConfig config.Config
 }
 
-func CreateDistributionClient(url, token string) (DistribClient, error) {
+func CreateDistributionClient(ctx context.Context, url, token string) (DistribClient, error) {
 	dtb := authdistrib.NewDistributionDetails()
 	dtb.SetUrl(strings.Replace(url, "/artifactory/", "/distribution/", -1))
 	dtb.SetAccessToken(token)
@@ -58,6 +59,9 @@ func CreateDistributionClient(url, token string) (DistribClient, error) {
 		SetServiceDetails(dtb).
 		SetThreads(1).
 		SetDryRun(false).
+		SetContext(ctx).
+		SetHttpTimeout(60 * time.Second).
+		SetHttpRetries(5).
 		Build()
 	if err != nil {
 		return DistribClient{}, fmt.Errorf("unable to create service config: %v", err)
@@ -69,7 +73,7 @@ func CreateDistributionClient(url, token string) (DistribClient, error) {
 	return DistribClient{Dsm: dsm, ServiceConfig: serviceConfig}, nil
 }
 
-func CreateArtifactoryClient(url, token string) (artifactory.ArtifactoryServicesManager, error) {
+func CreateArtifactoryClient(ctx context.Context, url, token string) (artifactory.ArtifactoryServicesManager, error) {
 	rtDetails := auth.NewArtifactoryDetails()
 	rtDetails.SetUrl(strings.TrimSuffix(url, "/") + "/") // ensure having '/' at the end
 	rtDetails.SetAccessToken(token)
@@ -77,6 +81,9 @@ func CreateArtifactoryClient(url, token string) (artifactory.ArtifactoryServices
 		SetServiceDetails(rtDetails).
 		SetThreads(1).
 		SetDryRun(false).
+		SetContext(ctx).
+		SetHttpTimeout(60 * time.Second).
+		SetHttpRetries(5).
 		Build()
 	if err != nil {
 		return nil, fmt.Errorf("unable to create service config: %v", err)
diff --git a/contrib/integrations/artifactory/plugin-artifactory-build-info/main.go b/contrib/integrations/artifactory/plugin-artifactory-build-info/main.go
index 012183c0f..5b909bd57 100644
--- a/contrib/integrations/artifactory/plugin-artifactory-build-info/main.go
+++ b/contrib/integrations/artifactory/plugin-artifactory-build-info/main.go
@@ -15,6 +15,7 @@ import (
 	"github.com/golang/protobuf/ptypes/empty"
 	"github.com/jfrog/jfrog-client-go/artifactory"
 	"github.com/jfrog/jfrog-client-go/artifactory/buildinfo"
+	"github.com/jfrog/jfrog-client-go/artifactory/services"
 	"github.com/jfrog/jfrog-client-go/artifactory/services/utils"
 	"github.com/jfrog/jfrog-client-go/utils/log"
 
@@ -64,7 +65,7 @@ func (e *artifactoryBuildInfoPlugin) Manifest(_ context.Context, _ *empty.Empty)
 	}, nil
 }
 
-func (e *artifactoryBuildInfoPlugin) Run(_ context.Context, opts *integrationplugin.RunQuery) (*integrationplugin.RunResult, error) {
+func (e *artifactoryBuildInfoPlugin) Run(ctx context.Context, opts *integrationplugin.RunQuery) (*integrationplugin.RunResult, error) {
 	artifactoryURL := opts.GetOptions()[fmt.Sprintf("cds.integration.artifact_manager.%s", sdk.ArtifactoryConfigURL)]
 	token := opts.GetOptions()[fmt.Sprintf("cds.integration.artifact_manager.%s", sdk.ArtifactoryConfigToken)]
 	tokenName := opts.GetOptions()[fmt.Sprintf("cds.integration.artifact_manager.%s", sdk.ArtifactoryConfigTokenName)]
@@ -76,11 +77,14 @@ func (e *artifactoryBuildInfoPlugin) Run(_ context.Context, opts *integrationplu
 	projectKey := opts.GetOptions()["cds.project"]
 	workflowName := opts.GetOptions()["cds.workflow"]
 
-	artiClient, err := art.CreateArtifactoryClient(artifactoryURL, token)
+	ctx, cancel := context.WithTimeout(ctx, 15*time.Minute)
+	defer cancel()
+
+	artiClient, err := art.CreateArtifactoryClient(ctx, artifactoryURL, token)
 	if err != nil {
 		return fail("unable to create artifactory client: %v", err)
 	}
-	log.SetLogger(log.NewLogger(log.ERROR, os.Stdout))
+	log.SetLogger(log.NewLogger(log.INFO, os.Stdout))
 
 	buildInfoName := fmt.Sprintf("%s/%s/%s", buildInfo, projectKey, workflowName)
 
@@ -92,6 +96,14 @@ func (e *artifactoryBuildInfoPlugin) Run(_ context.Context, opts *integrationplu
 	nodeRunURL := opts.GetOptions()["cds.ui.pipeline.run"]
 	runURL := nodeRunURL[0:strings.Index(nodeRunURL, "/node/")]
 
+	fmt.Printf("Creating Artifactory Build %s %s on project %s...\n", buildInfoName, version, artifactoryProjectKey)
+
+	// Get the build agent from env variable set by worker
+	workerName := os.Getenv("CDS_WORKER")
+	if workerName == "" {
+		workerName = "CDS"
+	}
+
 	buildInfoRequest := &buildinfo.BuildInfo{
 		Properties: map[string]string{},
 		Name:       buildInfoName,
@@ -100,7 +112,7 @@ func (e *artifactoryBuildInfoPlugin) Run(_ context.Context, opts *integrationplu
 			Version: sdk.VERSION,
 		},
 		BuildAgent: &buildinfo.Agent{
-			Name:    "CDS",
+			Name:    workerName,
 			Version: sdk.VERSION,
 		},
 		ArtifactoryPrincipal:     fmt.Sprintf("token:%s", tokenName),
@@ -111,10 +123,16 @@ func (e *artifactoryBuildInfoPlugin) Run(_ context.Context, opts *integrationplu
 		Modules:                  []buildinfo.Module{},
 		VcsList:                  make([]buildinfo.Vcs, 0),
 	}
+
+	gitUrl := opts.GetOptions()["git.url"]
+	if gitUrl == "" {
+		gitUrl = opts.GetOptions()["git.http_url"]
+	}
+
 	buildInfoRequest.VcsList = append(buildInfoRequest.VcsList, buildinfo.Vcs{
 		Branch:   opts.GetOptions()["git.branch"],
 		Message:  opts.GetOptions()["git.message"],
-		Url:      opts.GetOptions()["git.http_url"],
+		Url:      gitUrl,
 		Revision: opts.GetOptions()["git.hash"],
 	})
 
@@ -133,6 +151,24 @@ func (e *artifactoryBuildInfoPlugin) Run(_ context.Context, opts *integrationplu
 	if _, err := artiClient.PublishBuildInfo(buildInfoRequest, artifactoryProjectKey); err != nil {
 		return fail("unable to push build info: %v", err)
 	}
+
+	// Temporary code
+	if opts.GetOptions()["cds.proj.xray.enabled"] == "true" {
+		fmt.Printf("Triggering XRay Build %s %s scan...\n", buildInfoName, version)
+
+		// Scan build info
+		scanBuildRequest := services.NewXrayScanParams()
+		scanBuildRequest.BuildName = buildInfoRequest.Name
+		scanBuildRequest.BuildNumber = buildInfoRequest.Number
+		scanBuildRequest.ProjectKey = artifactoryProjectKey
+		scanBuildResponseBtes, err := artiClient.XrayScanBuild(scanBuildRequest)
+		if err != nil {
+			fmt.Println(err.Error())
+		} else {
+			fmt.Println(string(scanBuildResponseBtes))
+		}
+	}
+
 	return &integrationplugin.RunResult{
 		Status: sdk.StatusSuccess,
 	}, nil
diff --git a/contrib/integrations/artifactory/plugin-artifactory-download-artifact/main.go b/contrib/integrations/artifactory/plugin-artifactory-download-artifact/main.go
index 37ee0e8e6..48bd7b149 100644
--- a/contrib/integrations/artifactory/plugin-artifactory-download-artifact/main.go
+++ b/contrib/integrations/artifactory/plugin-artifactory-download-artifact/main.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"os"
 	"strconv"
+	"time"
 
 	"github.com/golang/protobuf/ptypes/empty"
 	"github.com/jfrog/jfrog-client-go/artifactory/services"
@@ -60,7 +61,7 @@ func (e *artifactoryDownloadArtifactPlugin) Manifest(_ context.Context, _ *empty
 	}, nil
 }
 
-func (e *artifactoryDownloadArtifactPlugin) Run(_ context.Context, opts *integrationplugin.RunQuery) (*integrationplugin.RunResult, error) {
+func (e *artifactoryDownloadArtifactPlugin) Run(ctx context.Context, opts *integrationplugin.RunQuery) (*integrationplugin.RunResult, error) {
 	cdsRepo := opts.GetOptions()[fmt.Sprintf("cds.integration.artifact_manager.%s", sdk.ArtifactoryConfigCdsRepository)]
 	artifactoryURL := opts.GetOptions()[fmt.Sprintf("cds.integration.artifact_manager.%s", sdk.ArtifactoryConfigURL)]
 	token := opts.GetOptions()[fmt.Sprintf("cds.integration.artifact_manager.%s", sdk.ArtifactoryConfigToken)]
@@ -75,11 +76,14 @@ func (e *artifactoryDownloadArtifactPlugin) Run(_ context.Context, opts *integra
 		return fail("unable to read file permission %s: %v", permS, err)
 	}
 
-	artiClient, err := art.CreateArtifactoryClient(artifactoryURL, token)
+	ctx, cancel := context.WithTimeout(ctx, 15*time.Minute)
+	defer cancel()
+
+	artiClient, err := art.CreateArtifactoryClient(ctx, artifactoryURL, token)
 	if err != nil {
 		return fail("unable to create artifactory client: %v", err)
 	}
-	log.SetLogger(log.NewLogger(log.ERROR, os.Stdout))
+	log.SetLogger(log.NewLogger(log.INFO, os.Stdout))
 	fileutils.SetTempDirBase(opts.GetOptions()["cds.workspace"])
 
 	params := services.NewDownloadParams()
diff --git a/contrib/integrations/artifactory/plugin-artifactory-promote/main.go b/contrib/integrations/artifactory/plugin-artifactory-promote/main.go
index d486b6f29..31ba7b5d5 100644
--- a/contrib/integrations/artifactory/plugin-artifactory-promote/main.go
+++ b/contrib/integrations/artifactory/plugin-artifactory-promote/main.go
@@ -6,6 +6,7 @@ import (
 	"os"
 	"regexp"
 	"strings"
+	"time"
 
 	"github.com/golang/protobuf/ptypes/empty"
 	"github.com/jfrog/jfrog-client-go/utils/log"
@@ -48,7 +49,7 @@ func (e *artifactoryPromotePlugin) Manifest(_ context.Context, _ *empty.Empty) (
 	}, nil
 }
 
-func (e *artifactoryPromotePlugin) Run(_ context.Context, opts *integrationplugin.RunQuery) (*integrationplugin.RunResult, error) {
+func (e *artifactoryPromotePlugin) Run(ctx context.Context, opts *integrationplugin.RunQuery) (*integrationplugin.RunResult, error) {
 	artifactoryURL := opts.GetOptions()[fmt.Sprintf("cds.integration.artifact_manager.%s", sdk.ArtifactoryConfigURL)]
 	token := opts.GetOptions()[fmt.Sprintf("cds.integration.artifact_manager.%s", sdk.ArtifactoryConfigToken)]
 
@@ -68,9 +69,12 @@ func (e *artifactoryPromotePlugin) Run(_ context.Context, opts *integrationplugi
 		return fail("unable to list run results: %v", err)
 	}
 
-	log.SetLogger(log.NewLogger(log.ERROR, os.Stdout))
+	log.SetLogger(log.NewLogger(log.INFO, os.Stdout))
 
-	artiClient, err := art.CreateArtifactoryClient(artifactoryURL, token)
+	ctx, cancel := context.WithTimeout(ctx, 15*time.Minute)
+	defer cancel()
+
+	artiClient, err := art.CreateArtifactoryClient(ctx, artifactoryURL, token)
 	if err != nil {
 		return fail("unable to create artifactory client: %v", err)
 	}
diff --git a/contrib/integrations/artifactory/plugin-artifactory-release/main.go b/contrib/integrations/artifactory/plugin-artifactory-release/main.go
index 20528c173..f8a9f5918 100644
--- a/contrib/integrations/artifactory/plugin-artifactory-release/main.go
+++ b/contrib/integrations/artifactory/plugin-artifactory-release/main.go
@@ -6,6 +6,7 @@ import (
 	"os"
 	"regexp"
 	"strings"
+	"time"
 
 	"github.com/golang/protobuf/ptypes/empty"
 	"github.com/jfrog/jfrog-client-go/artifactory/services/utils"
@@ -56,7 +57,7 @@ func (e *artifactoryReleasePlugin) Manifest(_ context.Context, _ *empty.Empty) (
 	}, nil
 }
 
-func (e *artifactoryReleasePlugin) Run(_ context.Context, opts *integrationplugin.RunQuery) (*integrationplugin.RunResult, error) {
+func (e *artifactoryReleasePlugin) Run(ctx context.Context, opts *integrationplugin.RunQuery) (*integrationplugin.RunResult, error) {
 	artifactoryURL := opts.GetOptions()[fmt.Sprintf("cds.integration.artifact_manager.%s", sdk.ArtifactoryConfigURL)]
 	distributionURL := opts.GetOptions()[fmt.Sprintf("cds.integration.artifact_manager.%s", sdk.ArtifactoryConfigDistributionURL)]
 	token := opts.GetOptions()[fmt.Sprintf("cds.integration.artifact_manager.%s", sdk.ArtifactoryConfigToken)]
@@ -85,7 +86,7 @@ func (e *artifactoryReleasePlugin) Run(_ context.Context, opts *integrationplugi
 		return fail("unable to list run results: %v", err)
 	}
 
-	log.SetLogger(log.NewLogger(log.ERROR, os.Stdout))
+	log.SetLogger(log.NewLogger(log.INFO, os.Stdout))
 	if distributionURL == "" {
 		fmt.Printf("Using %s to release\n", artifactoryURL)
 		distributionURL = artifactoryURL
@@ -94,13 +95,17 @@ func (e *artifactoryReleasePlugin) Run(_ context.Context, opts *integrationplugi
 		fmt.Println("Using distribution token to release")
 		releaseToken = token
 	}
-	distriClient, err := art.CreateDistributionClient(distributionURL, releaseToken)
+
+	ctx, cancel := context.WithTimeout(ctx, 15*time.Minute)
+	defer cancel()
+
+	distriClient, err := art.CreateDistributionClient(ctx, distributionURL, releaseToken)
 	if err != nil {
 		return fail("unable to create distribution client: %v", err)
 	}
 
 	// Promotion
-	artiClient, err := art.CreateArtifactoryClient(artifactoryURL, token)
+	artiClient, err := art.CreateArtifactoryClient(ctx, artifactoryURL, token)
 	if err != nil {
 		return fail("unable to create artifactory client: %v", err)
 	}
diff --git a/contrib/integrations/artifactory/plugin-artifactory-upload-artifact/main.go b/contrib/integrations/artifactory/plugin-artifactory-upload-artifact/main.go
index 92cb47981..b5bc74e8d 100644
--- a/contrib/integrations/artifactory/plugin-artifactory-upload-artifact/main.go
+++ b/contrib/integrations/artifactory/plugin-artifactory-upload-artifact/main.go
@@ -62,7 +62,7 @@ func (e *artifactoryUploadArtifactPlugin) Manifest(_ context.Context, _ *empty.E
 	}, nil
 }
 
-func (e *artifactoryUploadArtifactPlugin) Run(_ context.Context, opts *integrationplugin.RunQuery) (*integrationplugin.RunResult, error) {
+func (e *artifactoryUploadArtifactPlugin) Run(ctx context.Context, opts *integrationplugin.RunQuery) (*integrationplugin.RunResult, error) {
 	prefix := "cds.integration.artifact_manager"
 	cdsRepo := opts.GetOptions()[fmt.Sprintf("%s.%s", prefix, sdk.ArtifactoryConfigCdsRepository)]
 	artifactoryURL := opts.GetOptions()[fmt.Sprintf("%s.%s", prefix, sdk.ArtifactoryConfigURL)]
@@ -73,7 +73,10 @@ func (e *artifactoryUploadArtifactPlugin) Run(_ context.Context, opts *integrati
 	version := opts.GetOptions()["cds.version"]
 	buildInfo := opts.GetOptions()[fmt.Sprintf("%s.%s", prefix, sdk.ArtifactoryConfigBuildInfoPrefix)]
 
-	artiClient, err := art.CreateArtifactoryClient(artifactoryURL, token)
+	ctx, cancel := context.WithTimeout(ctx, 15*time.Minute)
+	defer cancel()
+
+	artiClient, err := art.CreateArtifactoryClient(ctx, artifactoryURL, token)
 	if err != nil {
 		return fail("unable to create artifactory client: %v", err)
 	}
diff --git a/docs/content/docs/concepts/files/workflow-syntax.md b/docs/content/docs/concepts/files/workflow-syntax.md
index 2ab4d9a10..8c57dde48 100644
--- a/docs/content/docs/concepts/files/workflow-syntax.md
+++ b/docs/content/docs/concepts/files/workflow-syntax.md
@@ -27,6 +27,9 @@ workflow:
 hooks:
   build:
   - type: RepositoryWebHook
+integrations:
+  my-artifactory-integration-name:
+    type: artifact_manager
 notifications:
 - type: email
   pipelines:
@@ -81,6 +84,10 @@ Example with using LUA syntax as advanced condition:
       == "master" and git_repository == "ovh/cds")
 ```
 
+## Integrations
+
+[Artifactory Integration]({{<relref "/docs/integrations/artifact-manager.md">}})
+
 ## Notifications
 
 [Notifications documentation]({{<relref "/docs/concepts/workflow/notifications.md">}})
diff --git a/engine/api/action.go b/engine/api/action.go
index 3610ecbcd..b0f630175 100644
--- a/engine/api/action.go
+++ b/engine/api/action.go
@@ -214,6 +214,10 @@ func (api *API) getActionHandler() service.Handler {
 
 func (api *API) putActionHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+		if isService(ctx) {
+			return sdk.WithStack(sdk.ErrForbidden)
+		}
+
 		vars := mux.Vars(r)
 
 		groupName := vars["permGroupName"]
@@ -307,6 +311,10 @@ func (api *API) putActionHandler() service.Handler {
 
 func (api *API) deleteActionHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+		if isService(ctx) {
+			return sdk.WithStack(sdk.ErrForbidden)
+		}
+
 		vars := mux.Vars(r)
 
 		groupName := vars["permGroupName"]
@@ -425,6 +433,10 @@ func (api *API) getActionAuditHandler() service.Handler {
 
 func (api *API) postActionAuditRollbackHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+		if isService(ctx) {
+			return sdk.WithStack(sdk.ErrForbidden)
+		}
+
 		vars := mux.Vars(r)
 
 		groupName := vars["permGroupName"]
diff --git a/engine/api/api_helper.go b/engine/api/api_helper.go
index dca1a18b1..3f149b385 100644
--- a/engine/api/api_helper.go
+++ b/engine/api/api_helper.go
@@ -85,6 +85,14 @@ func isHatchery(ctx context.Context) bool {
 	return c.Service != nil && c.Service.Type == sdk.TypeHatchery
 }
 
+func isHatcheryShared(ctx context.Context) bool {
+	c := getAPIConsumer(ctx)
+	if c == nil {
+		return false
+	}
+	return isHatchery(ctx) && c.GroupIDs.Contains(group.SharedInfraGroup.ID)
+}
+
 func isCDN(ctx context.Context) bool {
 	c := getAPIConsumer(ctx)
 	if c == nil {
diff --git a/engine/api/api_routes.go b/engine/api/api_routes.go
index 5851165d5..cea0fac14 100644
--- a/engine/api/api_routes.go
+++ b/engine/api/api_routes.go
@@ -168,7 +168,7 @@ func (api *API) InitRouter() {
 	r.Handle("/project/{permProjectKey}/variable/{name}/audit", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getVariableAuditInProjectHandler))
 	r.Handle("/project/{permProjectKey}/applications", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getApplicationsHandler), r.POST(api.addApplicationHandler))
 	r.Handle("/project/{permProjectKey}/integrations", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getProjectIntegrationsHandler), r.POST(api.postProjectIntegrationHandler))
-	r.Handle("/project/{permProjectKey}/integrations/{integrationName}", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getProjectIntegrationHandler), r.PUT(api.putProjectIntegrationHandler), r.DELETE(api.deleteProjectIntegrationHandler))
+	r.Handle("/project/{permProjectKeyWithHooksAllowed}/integrations/{integrationName}", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getProjectIntegrationHandler), r.PUT(api.putProjectIntegrationHandler), r.DELETE(api.deleteProjectIntegrationHandler))
 	r.Handle("/project/{permProjectKey}/integrations/{integrationName}/workerhooks", Scopes(sdk.AuthConsumerScopeProject, sdk.AuthConsumerScopeRunExecution), r.GET(api.getProjectIntegrationWorkerHookHandler), r.POST(api.postProjectIntegrationWorkerHookHandler))
 	r.Handle("/project/{permProjectKey}/notifications", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getProjectNotificationsHandler, DEPRECATED))
 	r.Handle("/project/{permProjectKey}/keys", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getKeysInProjectHandler), r.POST(api.addKeyInProjectHandler))
@@ -223,7 +223,7 @@ func (api *API) InitRouter() {
 
 	r.Handle("/project/{key}/type/{type}/access", Scope(sdk.AuthConsumerScopeService), r.GET(api.getProjectAccessHandler))
 	r.Handle("/project/{permProjectKey}/workflows", Scope(sdk.AuthConsumerScopeProject), r.POST(api.postWorkflowHandler), r.GET(api.getWorkflowsHandler))
-	r.Handle("/project/{key}/workflows/{permWorkflowName}", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getWorkflowHandler), r.PUT(api.putWorkflowHandler), r.DELETE(api.deleteWorkflowHandler))
+	r.Handle("/project/{key}/workflows/{permWorkflowNameAdvanced}", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getWorkflowHandler), r.PUT(api.putWorkflowHandler), r.DELETE(api.deleteWorkflowHandler))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/delete/dependencies", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getWorkflowDependenciesHandler))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/retention/maxruns", Scope(sdk.AuthConsumerScopeProject), r.POST(api.postWorkflowMaxRunHandler, service.OverrideAuth(api.authAdminMiddleware)))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/retention/dryrun", Scope(sdk.AuthConsumerScopeProject), r.POST(api.postWorkflowRetentionPolicyDryRun))
@@ -265,12 +265,12 @@ func (api *API) InitRouter() {
 
 	// Workflows run
 	r.Handle("/project/{permProjectKey}/runs", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getWorkflowAllRunsHandler))
-	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowRunsHandler), r.POSTEXECUTE(api.postWorkflowRunHandler))
+	r.Handle("/project/{key}/workflows/{permWorkflowNameAdvanced}/runs", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowRunsHandler), r.POSTEXECUTE(api.postWorkflowRunHandler))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/branch/{branch}", Scope(sdk.AuthConsumerScopeRun), r.DELETE(api.deleteWorkflowRunsBranchHandler))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/latest", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getLatestWorkflowRunHandler))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/tags", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowRunTagsHandler))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/num", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowRunNumHandler), r.POST(api.postWorkflowRunNumHandler))
-	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/{number}", Scopes(sdk.AuthConsumerScopeRun, sdk.AuthConsumerScopeRunExecution), r.GET(api.getWorkflowRunHandler), r.DELETE(api.deleteWorkflowRunHandler))
+	r.Handle("/project/{key}/workflows/{permWorkflowNameAdvanced}/runs/{number}", Scopes(sdk.AuthConsumerScopeRun, sdk.AuthConsumerScopeRunExecution), r.GET(api.getWorkflowRunHandler), r.DELETE(api.deleteWorkflowRunHandler))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/{number}/stop", Scope(sdk.AuthConsumerScopeRun), r.POSTEXECUTE(api.stopWorkflowRunHandler, MaintenanceAware()))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/{number}/vcs/resync", Scope(sdk.AuthConsumerScopeRun), r.POSTEXECUTE(api.postResyncVCSWorkflowRunHandler))
 	r.Handle("/project/{key}/workflows/{permWorkflowNameAdvanced}/runs/{number}/artifacts/links", Scopes(sdk.AuthConsumerScopeRun, sdk.AuthConsumerScopeRunExecution), r.GET(api.getWorkflowRunArtifactLinksHandler))
@@ -289,7 +289,7 @@ func (api *API) InitRouter() {
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/hook/triggers/condition", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowTriggerHookConditionHandler))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/triggers/condition", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowTriggerConditionHandler))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/{number}/nodes/{nodeRunID}/release", Scope(sdk.AuthConsumerScopeRunExecution), r.POSTEXECUTE(api.releaseApplicationWorkflowHandler, MaintenanceAware()))
-	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/{number}/hooks/{hookRunID}/callback", Scope(sdk.AuthConsumerScopeRun), r.POST(api.postWorkflowJobHookCallbackHandler, MaintenanceAware()))
+	r.Handle("/project/{key}/workflows/{permWorkflowNameAdvanced}/runs/{number}/hooks/{hookRunID}/callback", Scope(sdk.AuthConsumerScopeRun), r.POST(api.postWorkflowJobHookCallbackHandler, MaintenanceAware()))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/{number}/hooks/{hookRunID}/details", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowJobHookDetailsHandler))
 
 	// Environment
diff --git a/engine/api/application/dao.go b/engine/api/application/dao.go
index 5c7adc32d..e82d5a0a2 100644
--- a/engine/api/application/dao.go
+++ b/engine/api/application/dao.go
@@ -27,7 +27,7 @@ func (e dbApplication) Canonical() gorpmapper.CanonicalForms {
 }
 
 // LoadOptionFunc is a type for all options in LoadOptions
-type LoadOptionFunc *func(gorp.SqlExecutor, *sdk.Application) error
+type LoadOptionFunc *func(context.Context, gorp.SqlExecutor, *sdk.Application) error
 
 // LoadOptions provides all options on project loads functions
 var LoadOptions = struct {
@@ -137,14 +137,14 @@ func getWithClearVCSStrategyPassword(ctx context.Context, db gorp.SqlExecutor, k
 		return nil, err
 	}
 	if !isValid {
-		log.Error(context.Background(), "application.get> application %d data corrupted", dbApp.ID)
+		log.Error(ctx, "application.get> application %d data corrupted", dbApp.ID)
 		return nil, sdk.WithStack(sdk.ErrNotFound)
 	}
 	dbApp.ProjectKey = key
-	return unwrap(db, opts, &dbApp)
+	return unwrap(ctx, db, opts, &dbApp)
 }
 
-func unwrap(db gorp.SqlExecutor, opts []LoadOptionFunc, dbApp *dbApplication) (*sdk.Application, error) {
+func unwrap(ctx context.Context, db gorp.SqlExecutor, opts []LoadOptionFunc, dbApp *dbApplication) (*sdk.Application, error) {
 	app := &dbApp.Application
 	if app.ProjectKey == "" {
 		pkey, errP := db.SelectStr("SELECT projectkey FROM project WHERE id = $1", app.ProjectID)
@@ -155,7 +155,7 @@ func unwrap(db gorp.SqlExecutor, opts []LoadOptionFunc, dbApp *dbApplication) (*
 	}
 
 	for _, f := range opts {
-		if err := (*f)(db, app); err != nil && sdk.Cause(err) != sql.ErrNoRows {
+		if err := (*f)(ctx, db, app); err != nil && sdk.Cause(err) != sql.ErrNoRows {
 			return nil, sdk.WrapError(err, "application.unwrap")
 		}
 	}
@@ -283,7 +283,7 @@ func getAllWithClearVCS(ctx context.Context, db gorp.SqlExecutor, opts []LoadOpt
 			continue
 		}
 		a := &res[i]
-		app, err := unwrap(db, opts, a)
+		app, err := unwrap(ctx, db, opts, a)
 		if err != nil {
 			return nil, sdk.WrapError(err, "application.getAllWithClearVCS")
 		}
@@ -310,7 +310,7 @@ func getAll(ctx context.Context, db gorp.SqlExecutor, opts []LoadOptionFunc, que
 		}
 
 		a := &res[i]
-		app, err := unwrap(db, opts, a)
+		app, err := unwrap(ctx, db, opts, a)
 		if err != nil {
 			return nil, sdk.WrapError(err, "application.getAll")
 		}
diff --git a/engine/api/application/dao_application_integration.go b/engine/api/application/dao_application_integration.go
index cad4b3ca5..e4c2f163d 100644
--- a/engine/api/application/dao_application_integration.go
+++ b/engine/api/application/dao_application_integration.go
@@ -8,6 +8,7 @@ import (
 	"github.com/rockbears/log"
 
 	"github.com/ovh/cds/engine/api/database/gorpmapping"
+	"github.com/ovh/cds/engine/api/integration"
 	"github.com/ovh/cds/engine/gorpmapper"
 	"github.com/ovh/cds/sdk"
 )
@@ -46,7 +47,7 @@ func (e *dbApplicationDeploymentStrategy) IntegrationConfig() sdk.IntegrationCon
 }
 
 // LoadDeploymentStrategies loads the deployment strategies for an application
-func LoadDeploymentStrategies(db gorp.SqlExecutor, appID int64, withClearPassword bool) (map[string]sdk.IntegrationConfig, error) {
+func LoadDeploymentStrategies(ctx context.Context, db gorp.SqlExecutor, appID int64, withClearPassword bool) (map[string]sdk.IntegrationConfig, error) {
 	query := gorpmapping.NewQuery(`
     SELECT *
 	  FROM application_deployment_strategy
@@ -54,7 +55,7 @@ func LoadDeploymentStrategies(db gorp.SqlExecutor, appID int64, withClearPasswor
   `).Args(appID)
 
 	var res []dbApplicationDeploymentStrategy
-	if err := gorpmapping.GetAll(context.Background(), db, query, &res, gorpmapping.GetOptions.WithDecryption); err != nil {
+	if err := gorpmapping.GetAll(ctx, db, query, &res, gorpmapping.GetOptions.WithDecryption); err != nil {
 		return nil, sdk.WrapError(err, "unable to load deployment strategies")
 	}
 
@@ -65,7 +66,7 @@ func LoadDeploymentStrategies(db gorp.SqlExecutor, appID int64, withClearPasswor
 			return nil, err
 		}
 		if !isValid {
-			log.Error(context.Background(), "application.LoadDeploymentStrategies> application_deployment_strategy %d data corrupted", appID)
+			log.Error(ctx, "application.LoadDeploymentStrategies> application_deployment_strategy %d data corrupted", appID)
 			continue
 		}
 
@@ -85,12 +86,16 @@ func LoadDeploymentStrategies(db gorp.SqlExecutor, appID int64, withClearPasswor
 				newCfg[k] = v
 			}
 		}
-		// Sorry about that :(
-		projectIntegrationName, err := db.SelectStr("SELECT name FROM project_integration WHERE id = $1 ", r.ProjectIntegrationID)
+		projectIntegration, err := integration.LoadProjectIntegrationByID(ctx, db, r.ProjectIntegrationID)
 		if err != nil {
 			return nil, sdk.WrapError(err, "unable to find project integration name for ID=%d", r.ProjectIntegrationID)
 		}
-		deps[projectIntegrationName] = newCfg
+		for name, val := range projectIntegration.Model.AdditionalDefaultConfig {
+			if _, ok := newCfg[name]; !ok {
+				newCfg[name] = val
+			}
+		}
+		deps[projectIntegration.Name] = newCfg
 	}
 
 	return deps, nil
diff --git a/engine/api/application/dao_dependencies.go b/engine/api/application/dao_dependencies.go
index b2e436aa4..9a3dab8fe 100644
--- a/engine/api/application/dao_dependencies.go
+++ b/engine/api/application/dao_dependencies.go
@@ -1,6 +1,7 @@
 package application
 
 import (
+	"context"
 	"database/sql"
 
 	"github.com/go-gorp/gorp"
@@ -9,14 +10,14 @@ import (
 )
 
 var (
-	loadDefaultDependencies = func(db gorp.SqlExecutor, app *sdk.Application) error {
-		if err := loadVariables(db, app); err != nil && sdk.Cause(err) != sql.ErrNoRows {
+	loadDefaultDependencies = func(ctx context.Context, db gorp.SqlExecutor, app *sdk.Application) error {
+		if err := loadVariables(ctx, db, app); err != nil && sdk.Cause(err) != sql.ErrNoRows {
 			return sdk.WrapError(err, "application.loadDefaultDependencies %s", app.Name)
 		}
 		return nil
 	}
 
-	loadVariables = func(db gorp.SqlExecutor, app *sdk.Application) error {
+	loadVariables = func(ctx context.Context, db gorp.SqlExecutor, app *sdk.Application) error {
 		variables, err := LoadAllVariables(db, app.ID)
 		if err != nil && sdk.Cause(err) != sql.ErrNoRows {
 			return sdk.WrapError(err, "Unable to load variables for application %d", app.ID)
@@ -25,7 +26,7 @@ var (
 		return nil
 	}
 
-	loadVariablesWithClearPassword = func(db gorp.SqlExecutor, app *sdk.Application) error {
+	loadVariablesWithClearPassword = func(ctx context.Context, db gorp.SqlExecutor, app *sdk.Application) error {
 		variables, err := LoadAllVariablesWithDecrytion(db, app.ID)
 		if err != nil && sdk.Cause(err) != sql.ErrNoRows {
 			return sdk.WrapError(err, "Unable to load variables for application %d", app.ID)
@@ -34,7 +35,7 @@ var (
 		return nil
 	}
 
-	loadKeys = func(db gorp.SqlExecutor, app *sdk.Application) error {
+	loadKeys = func(ctx context.Context, db gorp.SqlExecutor, app *sdk.Application) error {
 		keys, err := LoadAllKeys(db, app.ID)
 		if err != nil {
 			return err
@@ -43,7 +44,7 @@ var (
 		return nil
 	}
 
-	loadClearKeys = func(db gorp.SqlExecutor, app *sdk.Application) error {
+	loadClearKeys = func(ctx context.Context, db gorp.SqlExecutor, app *sdk.Application) error {
 		keys, err := LoadAllKeysWithPrivateContent(db, app.ID)
 		if err != nil {
 			return err
@@ -52,16 +53,16 @@ var (
 		return nil
 	}
 
-	loadDeploymentStrategies = func(db gorp.SqlExecutor, app *sdk.Application) error {
+	loadDeploymentStrategies = func(ctx context.Context, db gorp.SqlExecutor, app *sdk.Application) error {
 		var err error
-		app.DeploymentStrategies, err = LoadDeploymentStrategies(db, app.ID, false)
+		app.DeploymentStrategies, err = LoadDeploymentStrategies(ctx, db, app.ID, false)
 		if err != nil && sdk.Cause(err) != sql.ErrNoRows {
 			return sdk.WrapError(err, "Unable to load deployment strategies for application %d", app.ID)
 		}
 		return nil
 	}
 
-	loadIcon = func(db gorp.SqlExecutor, app *sdk.Application) error {
+	loadIcon = func(ctx context.Context, db gorp.SqlExecutor, app *sdk.Application) error {
 		var err error
 		app.Icon, err = LoadIcon(db, app.ID)
 		if err != nil && sdk.Cause(err) != sql.ErrNoRows {
@@ -70,7 +71,7 @@ var (
 		return nil
 	}
 
-	loadVulnerabilities = func(db gorp.SqlExecutor, app *sdk.Application) error {
+	loadVulnerabilities = func(ctx context.Context, db gorp.SqlExecutor, app *sdk.Application) error {
 		var err error
 		app.Vulnerabilities, err = LoadVulnerabilities(db, app.ID)
 		if err != nil {
@@ -79,9 +80,9 @@ var (
 		return nil
 	}
 
-	loadDeploymentStrategiesWithClearPassword = func(db gorp.SqlExecutor, app *sdk.Application) error {
+	loadDeploymentStrategiesWithClearPassword = func(ctx context.Context, db gorp.SqlExecutor, app *sdk.Application) error {
 		var err error
-		app.DeploymentStrategies, err = LoadDeploymentStrategies(db, app.ID, true)
+		app.DeploymentStrategies, err = LoadDeploymentStrategies(ctx, db, app.ID, true)
 		if err != nil && sdk.Cause(err) != sql.ErrNoRows {
 			return sdk.WrapError(err, "Unable to load deployment strategies for application %d", app.ID)
 		}
diff --git a/engine/api/auth_builtin.go b/engine/api/auth_builtin.go
index 6e90546fe..bab533b61 100644
--- a/engine/api/auth_builtin.go
+++ b/engine/api/auth_builtin.go
@@ -133,6 +133,8 @@ func (api *API) postAuthBuiltinSigninHandler() service.Handler {
 		if hasService {
 			ctx = context.WithValue(ctx, cdslog.AuthServiceName, srv.Name)
 			SetTracker(w, cdslog.AuthServiceName, srv.Name)
+			ctx = context.WithValue(ctx, cdslog.AuthServiceType, srv.Type)
+			SetTracker(w, cdslog.AuthServiceType, srv.Type)
 
 			if err := api.serviceRegister(ctx, tx, &srv); err != nil {
 				return err
diff --git a/engine/api/download.go b/engine/api/download.go
index 641bdd84f..33a5997df 100644
--- a/engine/api/download.go
+++ b/engine/api/download.go
@@ -24,11 +24,15 @@ func (api *API) downloadsHandler() service.Handler {
 func (api *API) downloadHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 		vars := mux.Vars(r)
-		name := vars["name"]
-		os := vars["os"]
-		arch := vars["arch"]
+		name := sdk.NoPath(vars["name"])
+		os := sdk.NoPath(vars["os"])
+		arch := sdk.NoPath(vars["arch"])
 		r.ParseForm() // nolint
-		variant := r.Form.Get("variant")
+		variant := sdk.NoPath(r.Form.Get("variant"))
+
+		if !sdk.IsInArray(variant, []string{"", "nokeychain", "keychain"}) {
+			return sdk.NewErrorFrom(sdk.ErrWrongRequest, "invalid variant: %s", variant)
+		}
 
 		if err := download.CheckBinary(ctx, api.getDownloadConf(), name, os, arch, variant); err != nil {
 			return err
diff --git a/engine/api/group.go b/engine/api/group.go
index 86899be9a..2d3ed6606 100644
--- a/engine/api/group.go
+++ b/engine/api/group.go
@@ -119,6 +119,10 @@ func (api *API) postGroupHandler() service.Handler {
 
 func (api *API) putGroupHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+		if isService(ctx) {
+			return sdk.WithStack(sdk.ErrForbidden)
+		}
+
 		vars := mux.Vars(r)
 		groupName := vars["permGroupName"]
 
diff --git a/engine/api/project/loader.go b/engine/api/project/loader.go
index 134a1f375..f12f79b54 100644
--- a/engine/api/project/loader.go
+++ b/engine/api/project/loader.go
@@ -101,7 +101,7 @@ func loadApplicationWithDeploymentStrategies(ctx context.Context, db gorp.SqlExe
 	}
 	for i := range proj.Applications {
 		a := &proj.Applications[i]
-		if err := (*application.LoadOptions.WithDeploymentStrategies)(db, a); err != nil {
+		if err := (*application.LoadOptions.WithDeploymentStrategies)(ctx, db, a); err != nil {
 			return sdk.WithStack(err)
 		}
 	}
@@ -134,7 +134,7 @@ func loadApplicationVariables(ctx context.Context, db gorp.SqlExecutor, proj *sd
 	}
 
 	for _, a := range proj.Applications {
-		if err := (*application.LoadOptions.WithVariables)(db, &a); err != nil {
+		if err := (*application.LoadOptions.WithVariables)(ctx, db, &a); err != nil {
 			return sdk.WithStack(err)
 		}
 	}
@@ -150,7 +150,7 @@ func loadApplicationKeys(ctx context.Context, db gorp.SqlExecutor, proj *sdk.Pro
 	}
 
 	for _, a := range proj.Applications {
-		if err := (*application.LoadOptions.WithKeys)(db, &a); err != nil {
+		if err := (*application.LoadOptions.WithKeys)(ctx, db, &a); err != nil {
 			return sdk.WithStack(err)
 		}
 	}
diff --git a/engine/api/project_integration.go b/engine/api/project_integration.go
index 60b8a9846..10962223e 100644
--- a/engine/api/project_integration.go
+++ b/engine/api/project_integration.go
@@ -17,7 +17,7 @@ import (
 func (api *API) getProjectIntegrationHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 		vars := mux.Vars(r)
-		projectKey := vars[permProjectKey]
+		projectKey := vars["permProjectKeyWithHooksAllowed"]
 		integrationName := vars["integrationName"]
 
 		var integ sdk.ProjectIntegration
@@ -25,7 +25,7 @@ func (api *API) getProjectIntegrationHandler() service.Handler {
 
 		clearPassword := service.FormBool(r, "clearPassword")
 		if clearPassword {
-			if !isService(ctx) && !isWorker(ctx) {
+			if !isHooks(ctx) && !isWorker(ctx) {
 				return sdk.WithStack(sdk.ErrForbidden)
 			}
 			integ, err = integration.LoadProjectIntegrationByNameWithClearPassword(ctx, api.mustDB(), projectKey, integrationName)
@@ -52,7 +52,7 @@ func (api *API) getProjectIntegrationHandler() service.Handler {
 func (api *API) putProjectIntegrationHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 		vars := mux.Vars(r)
-		projectKey := vars[permProjectKey]
+		projectKey := vars["permProjectKeyWithHooksAllowed"]
 		integrationName := vars["integrationName"]
 
 		var projectIntegration sdk.ProjectIntegration
@@ -141,7 +141,7 @@ func (api *API) putProjectIntegrationHandler() service.Handler {
 func (api *API) deleteProjectIntegrationHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 		vars := mux.Vars(r)
-		projectKey := vars[permProjectKey]
+		projectKey := vars["permProjectKeyWithHooksAllowed"]
 		integrationName := vars["integrationName"]
 
 		p, err := project.Load(ctx, api.mustDB(), projectKey, project.LoadOptions.WithIntegrations)
diff --git a/engine/api/router_middleware_auth.go b/engine/api/router_middleware_auth.go
index 28e7a93df..6b5528342 100644
--- a/engine/api/router_middleware_auth.go
+++ b/engine/api/router_middleware_auth.go
@@ -157,6 +157,8 @@ func (api *API) authOptionalMiddleware(ctx context.Context, w http.ResponseWrite
 	if consumer.Service != nil {
 		ctx = context.WithValue(ctx, cdslog.AuthServiceName, consumer.Service.Name)
 		SetTracker(w, cdslog.AuthServiceName, consumer.Service.Name)
+		ctx = context.WithValue(ctx, cdslog.AuthServiceType, consumer.Service.Type)
+		SetTracker(w, cdslog.AuthServiceType, consumer.Service.Type)
 	}
 
 	// Add worker for consumer if exists
diff --git a/engine/api/router_middleware_auth_permission.go b/engine/api/router_middleware_auth_permission.go
index 4f7453132..5ea5c1fbd 100644
--- a/engine/api/router_middleware_auth_permission.go
+++ b/engine/api/router_middleware_auth_permission.go
@@ -27,19 +27,20 @@ type PermCheckFunc func(ctx context.Context, w http.ResponseWriter, key string,
 
 func permissionFunc(api *API) map[string]PermCheckFunc {
 	return map[string]PermCheckFunc{
-		"permProjectKey":           api.checkProjectPermissions,
-		"permWorkflowName":         api.checkWorkflowPermissions,
-		"permWorkflowNameAdvanced": api.checkWorkflowAdvancedPermissions,
-		"permGroupName":            api.checkGroupPermissions,
-		"permModelName":            api.checkWorkerModelPermissions,
-		"permActionName":           api.checkActionPermissions,
-		"permActionBuiltinName":    api.checkActionBuiltinPermissions,
-		"permTemplateSlug":         api.checkTemplateSlugPermissions,
-		"permUsernamePublic":       api.checkUserPublicPermissions,
-		"permUsername":             api.checkUserPermissions,
-		"permConsumerID":           api.checkConsumerPermissions,
-		"permSessionID":            api.checkSessionPermissions,
-		"permJobID":                api.checkJobIDPermissions,
+		"permProjectKey":                 api.checkProjectPermissions,
+		"permProjectKeyWithHooksAllowed": api.checkProjectAdvancedWithHooksAllowed,
+		"permWorkflowName":               api.checkWorkflowPermissions,
+		"permWorkflowNameAdvanced":       api.checkWorkflowAdvancedPermissions,
+		"permGroupName":                  api.checkGroupPermissions,
+		"permModelName":                  api.checkWorkerModelPermissions,
+		"permActionName":                 api.checkActionPermissions,
+		"permActionBuiltinName":          api.checkActionBuiltinPermissions,
+		"permTemplateSlug":               api.checkTemplateSlugPermissions,
+		"permUsernamePublic":             api.checkUserPublicPermissions,
+		"permUsername":                   api.checkUserPermissions,
+		"permConsumerID":                 api.checkConsumerPermissions,
+		"permSessionID":                  api.checkSessionPermissions,
+		"permJobID":                      api.checkJobIDPermissions,
 	}
 }
 
@@ -87,7 +88,7 @@ func (api *API) checkJobIDPermissions(ctx context.Context, w http.ResponseWriter
 		return nil
 	}
 	if perm == sdk.PermissionRead {
-		if isMaintainer(ctx) {
+		if isHatcheryShared(ctx) || isMaintainer(ctx) {
 			return nil
 		}
 	} else {
@@ -100,8 +101,16 @@ func (api *API) checkJobIDPermissions(ctx context.Context, w http.ResponseWriter
 	return sdk.WrapError(sdk.ErrForbidden, "not authorized for job %s", jobID)
 }
 
-func (api *API) checkProjectPermissions(ctx context.Context, w http.ResponseWriter, projectKey string, requiredPerm int, routeVars map[string]string) error {
-	ctx, end := telemetry.Span(ctx, "api.checkProjectPermissions")
+type checkProjectPermissionsOpts struct {
+	AllowHooks bool
+}
+
+func (api *API) checkProjectAdvancedWithHooksAllowed(ctx context.Context, w http.ResponseWriter, projectKey string, requiredPerm int, routeVars map[string]string) error {
+	return api.checkProjectAdvancedPermissionsWithOpts(ctx, w, projectKey, requiredPerm, routeVars, checkProjectPermissionsOpts{AllowHooks: true})
+}
+
+func (api *API) checkProjectAdvancedPermissionsWithOpts(ctx context.Context, w http.ResponseWriter, projectKey string, requiredPerm int, routeVars map[string]string, opts checkProjectPermissionsOpts) error {
+	ctx, end := telemetry.Span(ctx, "api.checkProjectAdvancedPermissions")
 	defer end()
 
 	if supportMFA(ctx) && !isMFA(ctx) {
@@ -148,14 +157,19 @@ func (api *API) checkProjectPermissions(ctx context.Context, w http.ResponseWrit
 		log.Debug(ctx, "checkProjectPermissions> callerPermission=%d ", callerPermission)
 		// If it's about READ: we have to check if the user is a maintainer or an admin
 		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(ctx, "checkProjectPermissions> %s(%s) is not authorized to %s", getAPIConsumer(ctx).Name, getAPIConsumer(ctx).ID, projectKey)
-				return sdk.WrapError(sdk.ErrNoProject, "not authorized for project %s", projectKey)
+			if isMaintainer(ctx) {
+				log.Debug(ctx, "checkProjectPermissions> %s(%s) access granted to %s because is maintainer", getAPIConsumer(ctx).Name, getAPIConsumer(ctx).ID, projectKey)
+				telemetry.Current(ctx, telemetry.Tag(telemetry.TagPermission, "is_maintainer"))
+				return nil
 			}
-			log.Debug(ctx, "checkProjectPermissions> %s(%s) access granted to %s because is maintainer", getAPIConsumer(ctx).Name, getAPIConsumer(ctx).ID, projectKey)
-			telemetry.Current(ctx, telemetry.Tag(telemetry.TagPermission, "is_maintainer"))
-			return nil
+			if opts.AllowHooks && isHooks(ctx) {
+				log.Debug(ctx, "checkProjectPermissions> %s(%s) access granted to %s because is hooks", getAPIConsumer(ctx).Name, getAPIConsumer(ctx).ID, projectKey)
+				telemetry.Current(ctx, telemetry.Tag(telemetry.TagPermission, "is_hooks"))
+				return nil
+			}
+			// The caller doesn't enough permission level from its groups and is neither a maintainer nor an admin
+			log.Debug(ctx, "checkProjectPermissions> %s(%s) is not authorized to %s", getAPIConsumer(ctx).Name, getAPIConsumer(ctx).ID, projectKey)
+			return sdk.WrapError(sdk.ErrNoProject, "not authorized for project %s", projectKey)
 		}
 
 		// If it's about Execute of Write: we have to check if the user is an admin
@@ -174,6 +188,10 @@ func (api *API) checkProjectPermissions(ctx context.Context, w http.ResponseWrit
 	return nil
 }
 
+func (api *API) checkProjectPermissions(ctx context.Context, w http.ResponseWriter, projectKey string, requiredPerm int, routeVars map[string]string) error {
+	return api.checkProjectAdvancedPermissionsWithOpts(ctx, w, projectKey, requiredPerm, routeVars, checkProjectPermissionsOpts{})
+}
+
 func (api *API) checkWorkflowPermissions(ctx context.Context, w http.ResponseWriter, workflowName string, perm int, routeVars map[string]string) error {
 	return api.checkWorkflowPermissionsWithOpts(CheckWorkflowPermissionsOpts{})(ctx, w, workflowName, perm, routeVars)
 }
@@ -181,13 +199,16 @@ func (api *API) checkWorkflowPermissions(ctx context.Context, w http.ResponseWri
 // Same as checkWorkflowPermissions but also allows GET for workers on same project's workflows.
 // This is needed as artifact download is allowed from a workflow to another in the same project.
 func (api *API) checkWorkflowAdvancedPermissions(ctx context.Context, w http.ResponseWriter, workflowName string, perm int, routeVars map[string]string) error {
-	return api.checkWorkflowPermissionsWithOpts(CheckWorkflowPermissionsOpts{
-		AllowGETForWorkerOnSameProject: true,
-	})(ctx, w, workflowName, perm, routeVars)
+	return api.checkWorkflowPermissionsWithOpts(
+		CheckWorkflowPermissionsOpts{
+			AllowGETForWorkerOnSameProject: true,
+			AllowHooks:                     true,
+		})(ctx, w, workflowName, perm, routeVars)
 }
 
 type CheckWorkflowPermissionsOpts struct {
 	AllowGETForWorkerOnSameProject bool
+	AllowHooks                     bool
 }
 
 func (api *API) checkWorkflowPermissionsWithOpts(opts CheckWorkflowPermissionsOpts) PermCheckFunc {
@@ -277,26 +298,37 @@ func (api *API) checkWorkflowPermissionsWithOpts(opts CheckWorkflowPermissionsOp
 		if maxLevelPermission < perm { // If the caller based on its group doesn have enough permission level
 			// If it's about READ: we have to check if the user is a maintainer or an admin
 			if perm < sdk.PermissionReadExecute {
-				if !isMaintainer(ctx) {
-					// The caller doesn't enough permission level from its groups and is neither a maintainer nor an admin
-					log.Debug(ctx, "checkWorkflowPermissions> %s is not authorized to %s/%s", getAPIConsumer(ctx).ID, projectKey, workflowName)
-					return sdk.WrapError(sdk.ErrForbidden, "not authorized for workflow %s/%s", projectKey, workflowName)
+				if isMaintainer(ctx) || (isHooks(ctx) && opts.AllowHooks) {
+					if isHooks(ctx) {
+						log.Debug(ctx, "checkWorkflowPermissions> %s access granted to %s/%s because is hooks service", getAPIConsumer(ctx).ID, projectKey, workflowName)
+						telemetry.Current(ctx, telemetry.Tag(telemetry.TagPermission, "is_hooks"))
+					} else if isMaintainer(ctx) {
+						log.Debug(ctx, "checkWorkflowPermissions> %s access granted to %s/%s because is maintainer", getAPIConsumer(ctx).ID, projectKey, workflowName)
+						telemetry.Current(ctx, telemetry.Tag(telemetry.TagPermission, "is_maintainer"))
+					}
+					return nil
 				}
-				log.Debug(ctx, "checkWorkflowPermissions> %s access granted to %s/%s because is maintainer", getAPIConsumer(ctx).ID, projectKey, workflowName)
-				telemetry.Current(ctx, telemetry.Tag(telemetry.TagPermission, "is_maintainer"))
-				return nil
-			}
-
-			// If it's about Execute of Write: we have to check if the user is an admin
-			if !isAdmin(ctx) {
-				// The caller doesn't enough permission level from its groups and is not an admin
+				// The caller doesn't enough permission level from its groups and is neither a maintainer nor an admin
 				log.Debug(ctx, "checkWorkflowPermissions> %s is not authorized to %s/%s", getAPIConsumer(ctx).ID, projectKey, workflowName)
 				return sdk.WrapError(sdk.ErrForbidden, "not authorized for workflow %s/%s", projectKey, workflowName)
 			}
-			log.Debug(ctx, "checkWorkflowPermissions> %s access granted to %s/%s because is admin", getAPIConsumer(ctx).ID, projectKey, workflowName)
-			telemetry.Current(ctx, telemetry.Tag(telemetry.TagPermission, "is_admin"))
-			trackSudo(ctx, w)
-			return nil
+
+			// If it's about Execute of Write: we have to check if the user is an admin or if it hooks service
+			if isAdmin(ctx) || (isHooks(ctx) && opts.AllowHooks) {
+				if isHooks(ctx) {
+					log.Debug(ctx, "checkWorkflowPermissions> %s access granted to %s/%s because is hooks service", getAPIConsumer(ctx).ID, projectKey, workflowName)
+					telemetry.Current(ctx, telemetry.Tag(telemetry.TagPermission, "is_hooks"))
+				} else if isAdmin(ctx) {
+					log.Debug(ctx, "checkWorkflowPermissions> %s access granted to %s/%s because is admin", getAPIConsumer(ctx).ID, projectKey, workflowName)
+					telemetry.Current(ctx, telemetry.Tag(telemetry.TagPermission, "is_admin"))
+					trackSudo(ctx, w)
+				}
+				return nil
+			}
+
+			// The caller doesn't enough permission level from its groups and is not an admin
+			log.Debug(ctx, "checkWorkflowPermissions> %s is not authorized to %s/%s", getAPIConsumer(ctx).ID, projectKey, workflowName)
+			return sdk.WrapError(sdk.ErrForbidden, "not authorized for workflow %s/%s", projectKey, workflowName)
 		}
 		log.Debug(ctx, "checkWorkflowPermissions> %s access granted to %s/%s because has permission (max permission = %d)", getAPIConsumer(ctx).ID, projectKey, workflowName, maxLevelPermission)
 		telemetry.Current(ctx, telemetry.Tag(telemetry.TagPermission, "is_granted"))
@@ -321,8 +353,13 @@ func (api *API) checkGroupPermissions(ctx context.Context, w http.ResponseWriter
 		return sdk.WithStack(sdk.ErrForbidden)
 	}
 
-	if permissionValue > sdk.PermissionRead { // Only group administror or CDS administrator can update a group or its dependencies
-		if !isGroupAdmin(ctx, g) || g.ID == group.SharedInfraGroup.ID {
+	if permissionValue > sdk.PermissionRead {
+		// Hatcheries started for "shared.infra" group are granted for group "shared.infra"
+		if isHatcheryShared(ctx) {
+			return nil
+		}
+		// Only group administror or CDS administrator can update a group or its dependencies
+		if !isGroupAdmin(ctx, g) {
 			if isAdmin(ctx) {
 				trackSudo(ctx, w)
 			} else {
@@ -330,6 +367,10 @@ func (api *API) checkGroupPermissions(ctx context.Context, w http.ResponseWriter
 			}
 		}
 	} else {
+		// Hatcheries started for "shared.infra" group are granted for group "shared.infra"
+		if isHatcheryShared(ctx) {
+			return nil
+		}
 		if !isGroupMember(ctx, g) && !isMaintainer(ctx) { // Only group member or CDS maintainer can get a group or its dependencies
 			return sdk.WithStack(sdk.ErrForbidden)
 		}
diff --git a/engine/api/templates.go b/engine/api/templates.go
index 0c1278096..a83737a5f 100644
--- a/engine/api/templates.go
+++ b/engine/api/templates.go
@@ -174,6 +174,10 @@ func (api *API) getTemplateHandler() service.Handler {
 
 func (api *API) putTemplateHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+		if isService(ctx) {
+			return sdk.WithStack(sdk.ErrForbidden)
+		}
+
 		vars := mux.Vars(r)
 
 		groupName := vars["permGroupName"]
@@ -288,6 +292,10 @@ func (api *API) putTemplateHandler() service.Handler {
 
 func (api *API) deleteTemplateHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+		if isService(ctx) {
+			return sdk.WithStack(sdk.ErrForbidden)
+		}
+
 		vars := mux.Vars(r)
 
 		groupName := vars["permGroupName"]
@@ -917,6 +925,10 @@ func (api *API) deleteTemplateInstanceHandler() service.Handler {
 
 func (api *API) postTemplatePullHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+		if isService(ctx) {
+			return sdk.WithStack(sdk.ErrForbidden)
+		}
+
 		vars := mux.Vars(r)
 
 		groupName := vars["permGroupName"]
diff --git a/engine/api/user.go b/engine/api/user.go
index 5e9319374..88a218c56 100644
--- a/engine/api/user.go
+++ b/engine/api/user.go
@@ -83,7 +83,6 @@ func (api *API) putUserHandler() service.Handler {
 		}
 
 		newUser := *oldUser
-		newUser.Username = data.Username
 		newUser.Fullname = data.Fullname
 
 		// Only an admin can change the ring of a user
diff --git a/engine/api/v2_project.go b/engine/api/v2_project.go
index 80ccedbb3..cc0ca8785 100644
--- a/engine/api/v2_project.go
+++ b/engine/api/v2_project.go
@@ -80,6 +80,8 @@ func (api *API) putVCSProjectHandler() ([]service.RbacChecker, service.Handler)
 			}
 
 			vcsProject.ID = vcsOld.ID
+			vcsProject.Created = vcsOld.Created
+			vcsProject.CreatedBy = vcsOld.CreatedBy
 
 			if err := vcs.Update(ctx, tx, &vcsProject); err != nil {
 				return err
diff --git a/engine/api/worker_model.go b/engine/api/worker_model.go
index 50b6aa81a..ed7831d65 100644
--- a/engine/api/worker_model.go
+++ b/engine/api/worker_model.go
@@ -79,6 +79,10 @@ func (api *API) postWorkerModelHandler() service.Handler {
 
 func (api *API) putWorkerModelHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+		if isService(ctx) {
+			return sdk.WithStack(sdk.ErrForbidden)
+		}
+
 		vars := mux.Vars(r)
 
 		groupName := vars["permGroupName"]
@@ -156,6 +160,10 @@ func (api *API) putWorkerModelHandler() service.Handler {
 
 func (api *API) deleteWorkerModelHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+		if isService(ctx) {
+			return sdk.WithStack(sdk.ErrForbidden)
+		}
+
 		vars := mux.Vars(r)
 
 		groupName := vars["permGroupName"]
diff --git a/engine/api/worker_model_hatchery.go b/engine/api/worker_model_hatchery.go
index b4dfe6825..f0ab7d23c 100644
--- a/engine/api/worker_model_hatchery.go
+++ b/engine/api/worker_model_hatchery.go
@@ -14,8 +14,7 @@ import (
 
 func (api *API) putBookWorkerModelHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
-		// this handler should only answer to an hatchery
-		if ok := isHatchery(ctx); !ok {
+		if !isHatchery(ctx) {
 			return sdk.WithStack(sdk.ErrForbidden)
 		}
 
diff --git a/engine/api/workflow.go b/engine/api/workflow.go
index 454a76bd5..8e19012f6 100644
--- a/engine/api/workflow.go
+++ b/engine/api/workflow.go
@@ -84,7 +84,7 @@ func (api *API) getWorkflowsHandler() service.Handler {
 }
 
 func (api *API) setWorkflowURLs(w1 *sdk.Workflow) {
-	w1.URLs.APIURL = api.Config.URL.API + api.Router.GetRoute("GET", api.getWorkflowHandler, map[string]string{"key": w1.ProjectKey, "permWorkflowName": w1.Name})
+	w1.URLs.APIURL = api.Config.URL.API + api.Router.GetRoute("GET", api.getWorkflowHandler, map[string]string{"key": w1.ProjectKey, "permWorkflowNameAdvanced": w1.Name})
 	w1.URLs.UIURL = api.Config.URL.UI + "/project/" + w1.ProjectKey + "/workflow/" + w1.Name
 
 	for j := range w1.Runs {
@@ -94,7 +94,7 @@ func (api *API) setWorkflowURLs(w1 *sdk.Workflow) {
 }
 
 func (api *API) setWorkflowRunURLs(r1 *sdk.WorkflowRun) {
-	r1.URLs.APIURL = api.Config.URL.API + api.Router.GetRoute("GET", api.getWorkflowRunHandler, map[string]string{"key": r1.Workflow.ProjectKey, "permWorkflowName": r1.Workflow.Name, "number": strconv.FormatInt(r1.Number, 10)})
+	r1.URLs.APIURL = api.Config.URL.API + api.Router.GetRoute("GET", api.getWorkflowRunHandler, map[string]string{"key": r1.Workflow.ProjectKey, "permWorkflowNameAdvanced": r1.Workflow.Name, "number": strconv.FormatInt(r1.Number, 10)})
 	r1.URLs.UIURL = api.Config.URL.UI + "/project/" + r1.Workflow.ProjectKey + "/workflow/" + r1.Workflow.Name + "/run/" + strconv.FormatInt(r1.Number, 10)
 }
 
@@ -218,7 +218,7 @@ func (api *API) getWorkflowHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 		vars := mux.Vars(r)
 		key := vars["key"]
-		name := vars["permWorkflowName"]
+		name := vars["permWorkflowNameAdvanced"]
 		withUsage := service.FormBool(r, "withUsage")
 		withAudits := service.FormBool(r, "withAudits")
 		withLabels := service.FormBool(r, "withLabels")
@@ -531,7 +531,11 @@ func (api *API) putWorkflowHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 		vars := mux.Vars(r)
 		key := vars["key"]
-		name := vars["permWorkflowName"]
+		name := vars["permWorkflowNameAdvanced"]
+
+		if isHooks(ctx) {
+			return sdk.WithStack(sdk.ErrForbidden)
+		}
 
 		p, err := project.Load(ctx, api.mustDB(), key, project.LoadOptions.WithIntegrations)
 		if err != nil {
@@ -687,7 +691,11 @@ func (api *API) deleteWorkflowHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 		vars := mux.Vars(r)
 		key := vars["key"]
-		name := vars["permWorkflowName"]
+		name := vars["permWorkflowNameAdvanced"]
+
+		if isHooks(ctx) {
+			return sdk.WithStack(sdk.ErrForbidden)
+		}
 
 		p, errP := project.Load(ctx, api.mustDB(), key, project.LoadOptions.WithIntegrations)
 		if errP != nil {
diff --git a/engine/api/workflow/dao_run.go b/engine/api/workflow/dao_run.go
index 863b9524d..a08a30069 100644
--- a/engine/api/workflow/dao_run.go
+++ b/engine/api/workflow/dao_run.go
@@ -706,9 +706,7 @@ func UpdateRunNum(db gorp.SqlExecutor, w *sdk.Workflow, num int64) error {
 		return nil
 	}
 
-	query := `
-		UPDATE workflow_sequences set current_val = $1 WHERE workflow_id = $2
-	`
+	query := `UPDATE workflow_sequences set current_val = $1 WHERE workflow_id = $2`
 	if _, err := db.Exec(query, num, w.ID); err != nil {
 		return sdk.WrapError(err, "Cannot update run number")
 	}
diff --git a/engine/api/workflow_hook.go b/engine/api/workflow_hook.go
index 2e29c9743..186d6463a 100644
--- a/engine/api/workflow_hook.go
+++ b/engine/api/workflow_hook.go
@@ -242,9 +242,13 @@ func (api *API) putWorkflowHookModelHandler() service.Handler {
 
 func (api *API) postWorkflowJobHookCallbackHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+		if !isHooks(ctx) {
+			return sdk.WithStack(sdk.ErrForbidden)
+		}
+
 		vars := mux.Vars(r)
 		key := vars["key"]
-		workflowName := vars["permWorkflowName"]
+		workflowName := vars["permWorkflowNameAdvanced"]
 		hookRunID := vars["hookRunID"]
 		number, err := requestVarInt(r, "number")
 		if err != nil {
@@ -279,7 +283,7 @@ func (api *API) postWorkflowJobHookCallbackHandler() service.Handler {
 			DisableDetailledNodeRun: true,
 		})
 		if err != nil {
-			return sdk.WrapError(err, "cannot load workflow run")
+			return sdk.WrapError(err, "cannot load workflow %s/%s run %d", key, workflowName, number)
 		}
 
 		secrets, err := workflow.LoadDecryptSecrets(ctx, tx, wr, nil)
diff --git a/engine/api/workflow_run.go b/engine/api/workflow_run.go
index 0cd0bc649..4857a4559 100644
--- a/engine/api/workflow_run.go
+++ b/engine/api/workflow_run.go
@@ -151,10 +151,10 @@ func (api *API) getWorkflowRunsHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 		vars := mux.Vars(r)
 		key := vars["key"]
-		name := vars["permWorkflowName"]
+		name := vars["permWorkflowNameAdvanced"]
 		route := api.Router.GetRoute("GET", api.getWorkflowRunsHandler, map[string]string{
-			"key":          key,
-			"workflowName": name,
+			"key":                      key,
+			"permWorkflowNameAdvanced": name,
 		})
 		return api.searchWorkflowRun(ctx, w, r, route, key, name)
 	}
@@ -265,7 +265,7 @@ func (api *API) getWorkflowRunHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 		vars := mux.Vars(r)
 		key := vars["key"]
-		name := vars["permWorkflowName"]
+		name := vars["permWorkflowNameAdvanced"]
 		number, err := requestVarInt(r, "number")
 		if err != nil {
 			return err
@@ -311,9 +311,13 @@ func (api *API) getWorkflowRunHandler() service.Handler {
 
 func (api *API) deleteWorkflowRunHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+		if isHooks(ctx) {
+			return sdk.WithStack(sdk.ErrForbidden)
+		}
+
 		vars := mux.Vars(r)
 		key := vars["key"]
-		name := vars["permWorkflowName"]
+		name := vars["permWorkflowNameAdvanced"]
 		number, err := requestVarInt(r, "number")
 		if err != nil {
 			return err
@@ -807,7 +811,7 @@ func (api *API) postWorkflowRunHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 		vars := mux.Vars(r)
 		key := vars["key"]
-		name := vars["permWorkflowName"]
+		name := vars["permWorkflowNameAdvanced"]
 
 		consumer := getAPIConsumer(ctx)
 
diff --git a/engine/api/workflowtemplate/instance.go b/engine/api/workflowtemplate/instance.go
index 9b39bc8d6..22b31e712 100644
--- a/engine/api/workflowtemplate/instance.go
+++ b/engine/api/workflowtemplate/instance.go
@@ -141,7 +141,8 @@ func CheckAndExecuteTemplate(ctx context.Context, db *gorp.DbMap, store cache.St
 
 	// Check if the template can be used by the current consumer
 	var groupPermissionValid bool
-	if consumer.Maintainer() {
+	// Maintainer and the Hooks micro-service are allowed
+	if consumer.Maintainer() || (consumer.Service != nil && consumer.Service.Type == sdk.TypeHooks) {
 		groupPermissionValid = true
 	} else if grp.ID == group.SharedInfraGroup.ID {
 		groupPermissionValid = true
@@ -155,6 +156,7 @@ func CheckAndExecuteTemplate(ctx context.Context, db *gorp.DbMap, store cache.St
 		}
 	}
 	if !groupPermissionValid {
+		log.Error(ctx, "invalid permission for template %s/%s %s and consumer %s (%+v)", groupName, templateSlug, templateVersion, consumer.ID, consumer.GetGroupIDs())
 		return allMsgs, nil, sdk.NewErrorFrom(sdk.ErrWrongRequest, "could not find given workflow template")
 	}
 
diff --git a/sdk/cdsclient/client.go b/sdk/cdsclient/client.go
index ee88a77df..22e582e69 100644
--- a/sdk/cdsclient/client.go
+++ b/sdk/cdsclient/client.go
@@ -22,11 +22,12 @@ import (
 var _ Interface = new(client)
 
 type client struct {
-	httpClient          *http.Client
-	httpNoTimeoutClient *http.Client
-	httpWebsocketClient *websocket.Dialer
-	config              *Config
-	name                string
+	httpClient                       *http.Client
+	httpNoTimeoutClient              *http.Client
+	httpWebsocketClient              *websocket.Dialer
+	config                           *Config
+	name                             string
+	serviceAuthConsumerSigninRequest *sdk.AuthConsumerSigninRequest
 }
 
 func NewWebsocketDialer(insecureSkipVerifyTLS bool) *websocket.Dialer {
@@ -151,15 +152,16 @@ func NewServiceClient(ctx context.Context, clientConfig ServiceConfig, registerP
 		}
 	}
 
+	cli.serviceAuthConsumerSigninRequest = &sdk.AuthConsumerSigninRequest{
+		"token":   clientConfig.Token,
+		"service": registerPayload,
+	}
+
 	var nbError int
 retry:
 	var res sdk.AuthConsumerSigninResponse
 	_, headers, code, err := cli.RequestJSON(ctx, "POST", "/auth/consumer/"+string(sdk.ConsumerBuiltin)+"/signin",
-		sdk.AuthConsumerSigninRequest{
-			"token":   clientConfig.Token,
-			"service": registerPayload,
-		},
-		&res)
+		cli.serviceAuthConsumerSigninRequest, &res)
 	if err != nil {
 		if code == 401 {
 			nbError++
diff --git a/sdk/cdsclient/client_project_vcs.go b/sdk/cdsclient/client_project_vcs.go
new file mode 100644
index 000000000..822d6055d
--- /dev/null
+++ b/sdk/cdsclient/client_project_vcs.go
@@ -0,0 +1,66 @@
+package cdsclient
+
+import (
+	"context"
+	"fmt"
+	"io"
+
+	"github.com/ghodss/yaml"
+
+	"github.com/ovh/cds/sdk"
+)
+
+func (c *client) ProjectVCSGet(ctx context.Context, projectKey string, vcsName string) (sdk.VCSProject, error) {
+	path := fmt.Sprintf("/v2/project/%s/vcs/%s", projectKey, vcsName)
+	var pf sdk.VCSProject
+	if _, err := c.GetJSON(ctx, path, &pf); err != nil {
+		return pf, err
+	}
+	return pf, nil
+}
+
+func (c *client) ProjectVCSList(ctx context.Context, projectKey string) ([]sdk.VCSProject, error) {
+	path := fmt.Sprintf("/v2/project/%s/vcs", projectKey)
+	var pfs []sdk.VCSProject
+	if _, err := c.GetJSON(ctx, path, &pfs); err != nil {
+		return pfs, err
+	}
+	return pfs, nil
+}
+
+func (c *client) ProjectVCSDelete(ctx context.Context, projectKey string, vcsName string) error {
+	path := fmt.Sprintf("/v2/project/%s/vcs/%s", projectKey, vcsName)
+	var pf sdk.VCSProject
+	if _, err := c.DeleteJSON(ctx, path, &pf); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (c *client) ProjectVCSImport(ctx context.Context, projectKey string, content io.Reader, mods ...RequestModifier) (sdk.VCSProject, error) {
+	var pf sdk.VCSProject
+
+	body, err := io.ReadAll(content)
+	if err != nil {
+		return pf, err
+	}
+
+	if err := yaml.Unmarshal(body, &pf); err != nil {
+		return pf, err
+	}
+
+	oldvcs, _ := c.ProjectVCSGet(ctx, projectKey, pf.Name)
+	if oldvcs.Name == "" {
+		path := fmt.Sprintf("/v2/project/%s/vcs", projectKey)
+		if _, err := c.PostJSON(ctx, path, &pf, &pf, mods...); err != nil {
+			return pf, err
+		}
+		return pf, nil
+	}
+
+	path := fmt.Sprintf("/v2/project/%s/vcs/%s", projectKey, pf.Name)
+	if _, err := c.PutJSON(ctx, path, &pf, &pf, mods...); err != nil {
+		return pf, err
+	}
+	return pf, nil
+}
diff --git a/sdk/cdsclient/http.go b/sdk/cdsclient/http.go
index b9c9a7911..5e1dcbc7b 100644
--- a/sdk/cdsclient/http.go
+++ b/sdk/cdsclient/http.go
@@ -184,7 +184,11 @@ func (c *client) StreamNoRetry(ctx context.Context, httpClient HTTPClient, metho
 		if c.config.Verbose {
 			log.Printf("session token invalid: (%s). Relogin...\n", c.config.SessionToken)
 		}
-		resp, err := c.AuthConsumerSignin(sdk.ConsumerBuiltin, sdk.AuthConsumerSigninRequest{"token": c.config.BuitinConsumerAuthenticationToken})
+		var req = sdk.AuthConsumerSigninRequest{"token": c.config.BuitinConsumerAuthenticationToken}
+		if c.serviceAuthConsumerSigninRequest != nil {
+			req = *c.serviceAuthConsumerSigninRequest
+		}
+		resp, err := c.AuthConsumerSignin(sdk.ConsumerBuiltin, req)
 		if err != nil {
 			return nil, nil, -1, err
 		}
@@ -287,7 +291,11 @@ func (c *client) Stream(ctx context.Context, httpClient HTTPClient, method strin
 		if c.config.Verbose {
 			log.Printf("session token invalid: (%s). Relogin...\n", c.config.SessionToken)
 		}
-		resp, err := c.AuthConsumerSignin(sdk.ConsumerBuiltin, sdk.AuthConsumerSigninRequest{"token": c.config.BuitinConsumerAuthenticationToken})
+		var req = sdk.AuthConsumerSigninRequest{"token": c.config.BuitinConsumerAuthenticationToken}
+		if c.serviceAuthConsumerSigninRequest != nil {
+			req = *c.serviceAuthConsumerSigninRequest
+		}
+		resp, err := c.AuthConsumerSignin(sdk.ConsumerBuiltin, req)
 		if err != nil {
 			return nil, nil, -1, err
 		}
@@ -418,7 +426,11 @@ func (c *client) UploadMultiPart(method string, path string, body *bytes.Buffer,
 	// Checks that current session_token is still valid
 	// If not, challenge a new one against the authenticationToken
 	if !c.config.HasValidSessionToken() && c.config.BuitinConsumerAuthenticationToken != "" {
-		resp, err := c.AuthConsumerSignin(sdk.ConsumerBuiltin, sdk.AuthConsumerSigninRequest{"token": c.config.BuitinConsumerAuthenticationToken})
+		var req = sdk.AuthConsumerSigninRequest{"token": c.config.BuitinConsumerAuthenticationToken}
+		if c.serviceAuthConsumerSigninRequest != nil {
+			req = *c.serviceAuthConsumerSigninRequest
+		}
+		resp, err := c.AuthConsumerSignin(sdk.ConsumerBuiltin, req)
 		if err != nil {
 			return nil, -1, err
 		}
diff --git a/sdk/cdsclient/interface.go b/sdk/cdsclient/interface.go
index efc8ba7e5..d64450e10 100644
--- a/sdk/cdsclient/interface.go
+++ b/sdk/cdsclient/interface.go
@@ -226,6 +226,10 @@ type ProjectClient interface {
 	ProjectAccess(ctx context.Context, projectKey, sessionID string, itemType sdk.CDNItemType) error
 	ProjectIntegrationWorkerHookGet(projectKey string, integrationName string) (*sdk.WorkerHookProjectIntegrationModel, error)
 	ProjectIntegrationWorkerHooksImport(projectKey string, integrationName string, hook sdk.WorkerHookProjectIntegrationModel) error
+	ProjectVCSImport(ctx context.Context, projectKey string, content io.Reader, mods ...RequestModifier) (sdk.VCSProject, error)
+	ProjectVCSGet(ctx context.Context, projectKey string, integrationName string) (sdk.VCSProject, error)
+	ProjectVCSList(ctx context.Context, projectKey string) ([]sdk.VCSProject, error)
+	ProjectVCSDelete(ctx context.Context, projectKey string, integrationName string) error
 }
 
 // ProjectKeysClient exposes project keys related functions
diff --git a/sdk/cdsclient/mock_cdsclient/interface_mock.go b/sdk/cdsclient/mock_cdsclient/interface_mock.go
index 43e5aaefc..8fc2046fa 100644
--- a/sdk/cdsclient/mock_cdsclient/interface_mock.go
+++ b/sdk/cdsclient/mock_cdsclient/interface_mock.go
@@ -2687,6 +2687,70 @@ func (mr *MockProjectClientMockRecorder) ProjectUpdate(key, project interface{})
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectUpdate", reflect.TypeOf((*MockProjectClient)(nil).ProjectUpdate), key, project)
 }
 
+// ProjectVCSDelete mocks base method.
+func (m *MockProjectClient) ProjectVCSDelete(ctx context.Context, projectKey, integrationName string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ProjectVCSDelete", ctx, projectKey, integrationName)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// ProjectVCSDelete indicates an expected call of ProjectVCSDelete.
+func (mr *MockProjectClientMockRecorder) ProjectVCSDelete(ctx, projectKey, integrationName interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectVCSDelete", reflect.TypeOf((*MockProjectClient)(nil).ProjectVCSDelete), ctx, projectKey, integrationName)
+}
+
+// ProjectVCSGet mocks base method.
+func (m *MockProjectClient) ProjectVCSGet(ctx context.Context, projectKey, integrationName string) (sdk.VCSProject, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ProjectVCSGet", ctx, projectKey, integrationName)
+	ret0, _ := ret[0].(sdk.VCSProject)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ProjectVCSGet indicates an expected call of ProjectVCSGet.
+func (mr *MockProjectClientMockRecorder) ProjectVCSGet(ctx, projectKey, integrationName interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectVCSGet", reflect.TypeOf((*MockProjectClient)(nil).ProjectVCSGet), ctx, projectKey, integrationName)
+}
+
+// ProjectVCSImport mocks base method.
+func (m *MockProjectClient) ProjectVCSImport(ctx context.Context, projectKey string, content io.Reader, mods ...cdsclient.RequestModifier) (sdk.VCSProject, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{ctx, projectKey, content}
+	for _, a := range mods {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "ProjectVCSImport", varargs...)
+	ret0, _ := ret[0].(sdk.VCSProject)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ProjectVCSImport indicates an expected call of ProjectVCSImport.
+func (mr *MockProjectClientMockRecorder) ProjectVCSImport(ctx, projectKey, content interface{}, mods ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{ctx, projectKey, content}, mods...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectVCSImport", reflect.TypeOf((*MockProjectClient)(nil).ProjectVCSImport), varargs...)
+}
+
+// ProjectVCSList mocks base method.
+func (m *MockProjectClient) ProjectVCSList(ctx context.Context, projectKey string) ([]sdk.VCSProject, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ProjectVCSList", ctx, projectKey)
+	ret0, _ := ret[0].([]sdk.VCSProject)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ProjectVCSList indicates an expected call of ProjectVCSList.
+func (mr *MockProjectClientMockRecorder) ProjectVCSList(ctx, projectKey interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectVCSList", reflect.TypeOf((*MockProjectClient)(nil).ProjectVCSList), ctx, projectKey)
+}
+
 // ProjectVariableCreate mocks base method.
 func (m *MockProjectClient) ProjectVariableCreate(projectKey string, variable *sdk.Variable) error {
 	m.ctrl.T.Helper()
@@ -6788,6 +6852,70 @@ func (mr *MockInterfaceMockRecorder) ProjectUpdate(key, project interface{}) *go
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectUpdate", reflect.TypeOf((*MockInterface)(nil).ProjectUpdate), key, project)
 }
 
+// ProjectVCSDelete mocks base method.
+func (m *MockInterface) ProjectVCSDelete(ctx context.Context, projectKey, integrationName string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ProjectVCSDelete", ctx, projectKey, integrationName)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// ProjectVCSDelete indicates an expected call of ProjectVCSDelete.
+func (mr *MockInterfaceMockRecorder) ProjectVCSDelete(ctx, projectKey, integrationName interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectVCSDelete", reflect.TypeOf((*MockInterface)(nil).ProjectVCSDelete), ctx, projectKey, integrationName)
+}
+
+// ProjectVCSGet mocks base method.
+func (m *MockInterface) ProjectVCSGet(ctx context.Context, projectKey, integrationName string) (sdk.VCSProject, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ProjectVCSGet", ctx, projectKey, integrationName)
+	ret0, _ := ret[0].(sdk.VCSProject)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ProjectVCSGet indicates an expected call of ProjectVCSGet.
+func (mr *MockInterfaceMockRecorder) ProjectVCSGet(ctx, projectKey, integrationName interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectVCSGet", reflect.TypeOf((*MockInterface)(nil).ProjectVCSGet), ctx, projectKey, integrationName)
+}
+
+// ProjectVCSImport mocks base method.
+func (m *MockInterface) ProjectVCSImport(ctx context.Context, projectKey string, content io.Reader, mods ...cdsclient.RequestModifier) (sdk.VCSProject, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{ctx, projectKey, content}
+	for _, a := range mods {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "ProjectVCSImport", varargs...)
+	ret0, _ := ret[0].(sdk.VCSProject)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ProjectVCSImport indicates an expected call of ProjectVCSImport.
+func (mr *MockInterfaceMockRecorder) ProjectVCSImport(ctx, projectKey, content interface{}, mods ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{ctx, projectKey, content}, mods...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectVCSImport", reflect.TypeOf((*MockInterface)(nil).ProjectVCSImport), varargs...)
+}
+
+// ProjectVCSList mocks base method.
+func (m *MockInterface) ProjectVCSList(ctx context.Context, projectKey string) ([]sdk.VCSProject, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ProjectVCSList", ctx, projectKey)
+	ret0, _ := ret[0].([]sdk.VCSProject)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ProjectVCSList indicates an expected call of ProjectVCSList.
+func (mr *MockInterfaceMockRecorder) ProjectVCSList(ctx, projectKey interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectVCSList", reflect.TypeOf((*MockInterface)(nil).ProjectVCSList), ctx, projectKey)
+}
+
 // ProjectVariableCreate mocks base method.
 func (m *MockInterface) ProjectVariableCreate(projectKey string, variable *sdk.Variable) error {
 	m.ctrl.T.Helper()
diff --git a/sdk/common.go b/sdk/common.go
index 588cfa5c4..0b3485c18 100644
--- a/sdk/common.go
+++ b/sdk/common.go
@@ -13,6 +13,7 @@ import (
 	"io"
 	"os"
 	"path"
+	"path/filepath"
 	"reflect"
 	"regexp"
 	"runtime"
@@ -338,3 +339,33 @@ type KeyValues struct {
 	Key    string
 	Values []string
 }
+
+func CleanPath(path string) string {
+	// Deal with empty strings nicely.
+	if path == "" {
+		return ""
+	}
+
+	// Ensure that all paths are cleaned (especially problematic ones like
+	// "/../../../../../" which can cause lots of issues).
+	path = filepath.Clean(path)
+
+	// If the path isn't absolute, we need to do more processing to fix paths
+	// such as "../../../../<etc>/some/path". We also shouldn't convert absolute
+	// paths to relative ones.
+	if !filepath.IsAbs(path) {
+		path = filepath.Clean(string(os.PathSeparator) + path)
+		// This can't fail, as (by definition) all paths are relative to root.
+		path, _ = filepath.Rel(string(os.PathSeparator), path)
+	}
+
+	// Clean the path again for good measure.
+	return filepath.Clean(path)
+}
+
+func NoPath(path string) string {
+	if path == "" {
+		return ""
+	}
+	return filepath.Base(CleanPath(path))
+}
diff --git a/sdk/doc/route.go b/sdk/doc/route.go
index 7655cb50e..dc99c6133 100644
--- a/sdk/doc/route.go
+++ b/sdk/doc/route.go
@@ -29,7 +29,7 @@ func CleanURLParameter(u string) string {
 	switch u {
 	case "consumerType":
 		u = "consumer-type"
-	case "key", "permProjectKey", "projectKey":
+	case "key", "permProjectKey", "projectKey", "permProjectKeyWithHooksAllowed":
 		u = "project-key"
 	case "permWorkflowName", "permWorkflowNameAdvanced", "workflowName":
 		u = "workflow-name"
diff --git a/sdk/download.go b/sdk/download.go
index aa976035c..58bf183fc 100644
--- a/sdk/download.go
+++ b/sdk/download.go
@@ -124,12 +124,12 @@ func BinaryFilename(name, os, arch, variant string) string {
 	if os == "windows" {
 		suffix = ".exe"
 	}
-	return fmt.Sprintf("%s%s-%s-%s%s%s", prefix, name, os, getArchName(arch), variant, suffix)
+	return NoPath(fmt.Sprintf("%s%s-%s-%s%s%s", prefix, name, os, getArchName(arch), variant, suffix))
 }
 
 // IsDownloadedBinary returns true if the binary is already downloaded, false otherwise
 func IsDownloadedBinary(directoriesDownload, filename string) bool {
-	if _, err := os.Stat(path.Join(directoriesDownload, filename)); err == nil {
+	if _, err := os.Stat(CleanPath(path.Join(directoriesDownload, filename))); err == nil {
 		return true
 	}
 	return false
diff --git a/sdk/log/fields.go b/sdk/log/fields.go
index f3ae5fd91..4771badc6 100644
--- a/sdk/log/fields.go
+++ b/sdk/log/fields.go
@@ -11,6 +11,7 @@ const (
 	Action             = log.Field("action")
 	AuthConsumerID     = log.Field("auth_consumer_id")
 	AuthServiceName    = log.Field("auth_service_name")
+	AuthServiceType    = log.Field("auth_service_type")
 	AuthSessionIAT     = log.Field("auth_session_iat")
 	AuthSessionID      = log.Field("auth_session_id")
 	AuthSessionTokenID = log.Field("auth_session_token")
@@ -45,6 +46,7 @@ func init() {
 		AuthUserID,
 		AuthUsername,
 		AuthServiceName,
+		AuthServiceType,
 		AuthWorkerName,
 		AuthConsumerID,
 		AuthSessionID,
diff --git a/sdk/luascript/luascript.go b/sdk/luascript/luascript.go
index 043f0df35..a51d91c4d 100644
--- a/sdk/luascript/luascript.go
+++ b/sdk/luascript/luascript.go
@@ -56,7 +56,7 @@ func NewCheck() (*Check, error) {
 	state.PreloadModule("json", luajson.Loader)
 
 	// Sandboxing lua engine
-	if err := state.DoString("coroutine=nil;debug=nil;io=nil;open=nil;os.rename=nil;os.remove=nil;os.exit=nil;os.clock=nil;os.execute=nil;os.getenv=nil;os.setlocale=nil;os.tmpname=nil"); err != nil {
+	if err := state.DoString("coroutine=nil;debug=nil;io=nil;open=nil;dofile=nil;loadfile=nil;getfenv=nil;print=nil;pcall=nil;os.rename=nil;os.remove=nil;os.exit=nil;os.clock=nil;os.execute=nil;os.getenv=nil;os.setlocale=nil;os.tmpname=nil"); err != nil {
 		return nil, err
 	}
 
diff --git a/sdk/token.go b/sdk/token.go
index 952070d28..ff7fb7c33 100644
--- a/sdk/token.go
+++ b/sdk/token.go
@@ -523,11 +523,12 @@ func (c AuthConsumer) GetGroupIDs() []int64 {
 }
 
 func (c AuthConsumer) Admin() bool {
-	return c.AuthentifiedUser.Ring == UserRingAdmin && c.Worker == nil
+	// Worker and Service can't be considered as admin
+	return c.AuthentifiedUser.Ring == UserRingAdmin && c.Worker == nil && c.Service == nil
 }
 
 func (c AuthConsumer) Maintainer() bool {
-	return (c.AuthentifiedUser.Ring == UserRingMaintainer || c.AuthentifiedUser.Ring == UserRingAdmin) && c.Worker == nil
+	return (c.AuthentifiedUser.Ring == UserRingMaintainer || c.AuthentifiedUser.Ring == UserRingAdmin) && c.Worker == nil && c.Service == nil
 }
 
 func (c AuthConsumer) GetUsername() string {
diff --git a/sdk/vcs.go b/sdk/vcs.go
index cbdf7f6f7..c93af6039 100644
--- a/sdk/vcs.go
+++ b/sdk/vcs.go
@@ -180,7 +180,7 @@ type BuildNumberAndHash struct {
 
 type VCSProject struct {
 	ID           string            `json:"id" db:"id"`
-	Name         string            `json:"name" db:"name"`
+	Name         string            `json:"name" db:"name" cli:"name,key"`
 	Type         string            `json:"type" db:"type"`
 	Created      time.Time         `json:"created" db:"created"`
 	LastModified time.Time         `json:"last_modified" db:"last_modified"`
diff --git a/ui/angular.json b/ui/angular.json
index c54194fe7..e4896a8f1 100644
--- a/ui/angular.json
+++ b/ui/angular.json
@@ -26,13 +26,14 @@
                 "glob": "*",
                 "input": "node_modules/@ant-design/icons-angular/src/inline-svg/",
                 "output": "/assets/"
-              }, {
-                "glob": "ng-zorro-antd.dark.min.css",
-                "input": "./node_modules/ng-zorro-antd",
-                "output": "/assets/"
               }
             ],
             "styles": [
+              {
+                "input": "src/ngzorro.dark.less",
+                "bundleName": "ngzorro.dark",
+                "inject": false
+              },
               "node_modules/ng-zorro-antd/ng-zorro-antd.min.css",
               "node_modules/dragula/dist/dragula.css",
               "node_modules/codemirror/lib/codemirror.css",
diff --git a/ui/src/app/service/theme/theme.store.ts b/ui/src/app/service/theme/theme.store.ts
index 335a40631..55de7dad3 100644
--- a/ui/src/app/service/theme/theme.store.ts
+++ b/ui/src/app/service/theme/theme.store.ts
@@ -25,7 +25,7 @@ export class ThemeStore {
             style.type = 'text/css';
             style.rel = 'stylesheet';
             style.id = 'dark-theme';
-            style.href = 'assets/ng-zorro-antd.dark.min.css';
+            style.href = 'ngzorro.dark.css';
             document.body.appendChild(style);
         } else {
             const dom = document.getElementById('dark-theme');
diff --git a/ui/src/app/shared/shared.module.ts b/ui/src/app/shared/shared.module.ts
index 59c2d7e9c..dbc04096b 100644
--- a/ui/src/app/shared/shared.module.ts
+++ b/ui/src/app/shared/shared.module.ts
@@ -116,6 +116,8 @@ import { ZoneComponent } from './zone/zone.component';
 import { NgxAutoScrollDirective } from 'app/shared/directives/auto-scroll.directive';
 import { NZ_CONFIG, NzConfig } from 'ng-zorro-antd/core/config';
 import { NzNotificationModule } from 'ng-zorro-antd/notification';
+import { NzMenuModule } from 'ng-zorro-antd/menu';
+import { NzButtonModule } from 'ng-zorro-antd/button';
 
 const ngZorroConfig: NzConfig = {
     notification: {nzPauseOnHover: true, nzPlacement: "topRight"}
@@ -125,7 +127,9 @@ const ngZorroConfig: NzConfig = {
 
 @NgModule({
     imports: [
+        NzButtonModule,
         NzNotificationModule,
+        NzMenuModule,
         CommonModule,
         ClipboardModule,
         NgxSemanticModule,
@@ -296,6 +300,8 @@ const ngZorroConfig: NzConfig = {
         NgForNumber,
         NgxSemanticModule,
         NgxAutoScrollDirective,
+        NzButtonModule,
+        NzMenuModule,
         NzNotificationModule,
         PaginationComponent,
         ParameterFormComponent,
diff --git a/ui/src/app/shared/tabs/tab.component.ts b/ui/src/app/shared/tabs/tab.component.ts
index 74771010a..0316d0692 100644
--- a/ui/src/app/shared/tabs/tab.component.ts
+++ b/ui/src/app/shared/tabs/tab.component.ts
@@ -21,10 +21,7 @@ import { Tab } from './tabs.component';
 @AutoUnsubscribe()
 export class TabComponent implements AfterViewInit, OnChanges {
     @ViewChild('templateSibling', { read: ViewContainerRef }) templateSibling: ViewContainerRef;
-
     @Input() tab: Tab;
-    @Input() active: boolean;
-    @Output() onSelect = new EventEmitter();
 
     constructor(
         private _cd: ChangeDetectorRef
@@ -45,8 +42,4 @@ export class TabComponent implements AfterViewInit, OnChanges {
             this._cd.markForCheck();
         }
     }
-
-    click() {
-        this.onSelect.emit();
-    }
 }
diff --git a/ui/src/app/shared/tabs/tab.html b/ui/src/app/shared/tabs/tab.html
index 0e9dde32c..8549ade94 100644
--- a/ui/src/app/shared/tabs/tab.html
+++ b/ui/src/app/shared/tabs/tab.html
@@ -1,7 +1,9 @@
-<a sm-item [class.active]="active" (click)="click()">
-    <ng-container *ngIf="!tab.template">
-        <i *ngIf="tab.icon" [ngClass]="[tab.icon, 'icon']"></i>
-        {{tab.translate | translate:tab.translate_args}}
+<ng-container *ngIf="!tab.template">
+    <i *ngIf="tab.icon" [ngClass]="[tab.icon, 'icon']"></i>
+    {{tab.title}}
+    <ng-container *ngIf="tab.warningText || tab.warningTemplate">
+        <i class="exclamation triangle orange icon" suiPopup [popupTemplate]="tab.warningTemplate" [popupText]="tab.warningText"></i>
     </ng-container>
-    <span #templateSibling></span>
-</a>
+</ng-container>
+<span #templateSibling></span>
+
diff --git a/ui/src/app/shared/tabs/tabs.component.ts b/ui/src/app/shared/tabs/tabs.component.ts
index b16891e1c..3a6eded71 100644
--- a/ui/src/app/shared/tabs/tabs.component.ts
+++ b/ui/src/app/shared/tabs/tabs.component.ts
@@ -4,12 +4,14 @@ import { AutoUnsubscribe } from 'app/shared/decorator/autoUnsubscribe';
 import { Subscription } from 'rxjs/Subscription';
 
 export class Tab {
-    translate: string;
-    translate_args?: {};
+    title: string;
     icon: string;
     key: string;
     default: boolean;
     template: TemplateRef<any>;
+    warningText: string;
+    warningTemplate: TemplateRef<any>;
+    disabled: boolean;
 }
 
 @Component({
@@ -52,6 +54,9 @@ export class TabsComponent implements OnInit, OnChanges, OnDestroy {
     }
 
     clickSelect(tab: Tab) {
+        if (tab.disabled) {
+            return;
+        }
         if (!this.disableNavigation) {
             this._router.navigate([], {
                 relativeTo: this._route,
diff --git a/ui/src/app/shared/tabs/tabs.html b/ui/src/app/shared/tabs/tabs.html
index 39f4e750b..f56973c99 100644
--- a/ui/src/app/shared/tabs/tabs.html
+++ b/ui/src/app/shared/tabs/tabs.html
@@ -1,3 +1,7 @@
-<sm-menu class="pointing secondary stackable">
-    <app-tab *ngFor="let t of tabs" [tab]="t" [active]="selected.key === t.key" (onSelect)="clickSelect(t)"></app-tab>
-</sm-menu>
+<ul nz-menu nzMode="horizontal">
+    <ng-container *ngFor="let t of tabs">
+        <li nz-menu-item [nzSelected]="selected.key === t.key" (click)="clickSelect(t)" [nzDisabled]="t.disabled">
+            <app-tab [tab]="t"></app-tab>
+        </li>
+    </ng-container>
+</ul>
diff --git a/ui/src/app/shared/workflow/modal/node-edit/node.edit.modal.html b/ui/src/app/shared/workflow/modal/node-edit/node.edit.modal.html
index dd99dc327..ec79b7243 100644
--- a/ui/src/app/shared/workflow/modal/node-edit/node.edit.modal.html
+++ b/ui/src/app/shared/workflow/modal/node-edit/node.edit.modal.html
@@ -33,7 +33,7 @@
                             </a>
                             <a class="item" [class.active]="selected === 'permissions'"
                                 (click)="changeView('permissions')">
-                                {{ 'common_permissions' | translate }}
+                                Permissions
                             </a>
                         </ng-container>
                         <ng-container *ngIf="hookSelected">
diff --git a/ui/src/app/views/application/show/application.component.ts b/ui/src/app/views/application/show/application.component.ts
index 6950251fd..dd2dbf20b 100644
--- a/ui/src/app/views/application/show/application.component.ts
+++ b/ui/src/app/views/application/show/application.component.ts
@@ -6,7 +6,6 @@ import { Application } from 'app/model/application.model';
 import { Environment } from 'app/model/environment.model';
 import { Pipeline } from 'app/model/pipeline.model';
 import { Project } from 'app/model/project.model';
-import { AuthentifiedUser } from 'app/model/user.model';
 import { Workflow } from 'app/model/workflow.model';
 import { ApplicationStore } from 'app/service/application/application.store';
 import { AsCodeSaveModalComponent } from 'app/shared/ascode/save-modal/ascode.save-modal.component';
@@ -17,11 +16,11 @@ import { VariableEvent } from 'app/shared/variable/variable.event.model';
 import * as applicationsActions from 'app/store/applications.action';
 import { CancelApplicationEdition, ClearCacheApplication } from 'app/store/applications.action';
 import { ApplicationsState, ApplicationStateModel } from 'app/store/applications.state';
-import { AuthenticationState } from 'app/store/authentication.state';
 import { ProjectState, ProjectStateModel } from 'app/store/project.state';
 import cloneDeep from 'lodash-es/cloneDeep';
 import { Subscription } from 'rxjs';
 import { filter, finalize } from 'rxjs/operators';
+import { Tab } from 'app/shared/tabs/tabs.component';
 
 @Component({
     selector: 'app-application-show',
@@ -50,8 +49,9 @@ export class ApplicationShowComponent implements OnInit, OnDestroy {
     _routeParamsSub: Subscription;
     _queryParamsSub: Subscription;
 
-    // Selected tab
-    selectedTab = 'home';
+    // tabs
+    tabs: Array<Tab>;
+    selectedTab: Tab;
 
     @ViewChild('varWarning')
     private varWarningModal: WarningModalComponent;
@@ -132,6 +132,7 @@ export class ApplicationShowComponent implements OnInit, OnDestroy {
                     this.pipelines = this.application.usage.pipelines || [];
                     this.usageCount = this.pipelines.length + this.environments.length + this.workflows.length;
                 }
+                this.initTabs();
 
                 // Update recent application viewed
                 this._applicationStore.updateRecentApplication(s.currentProjectKey, this.application);
@@ -142,17 +143,55 @@ export class ApplicationShowComponent implements OnInit, OnDestroy {
     }
 
     ngOnInit() {
+        this.initTabs();
         this._queryParamsSub = this._routeActivated.queryParams.subscribe(params => {
             let tab = params['tab'];
             if (tab) {
-                this.selectedTab = tab;
+                let current_tab = this.tabs.find((t) => t.key === tab);
+                if (current_tab) {
+                    this.selectTab(current_tab);
+                }
             }
             this._cd.markForCheck();
         });
     }
 
-    showTab(tab: string): void {
-        this._router.navigateByUrl('/project/' + this.project.key + '/application/' + this.application.name + '?tab=' + tab);
+    initTabs() {
+        let usageText = 'Usage';
+        if (this.application) {
+            usageText = 'Usage (' + this.usageCount + ')';
+        }
+        this.tabs = [<Tab>{
+            title: 'Overview',
+            key: 'home',
+            default: true,
+        }, <Tab>{
+            title: 'Variables',
+            key: 'variables',
+            icon: 'font'
+        }, <Tab>{
+            title: 'Vulnerabilities',
+            key: 'vuln'
+        }, <Tab>{
+            title: usageText,
+            icon: 'map signs',
+            key: 'usage'
+        }, <Tab>{
+            title: 'Keys',
+            icon: 'privacy',
+            key: 'keys'
+        }]
+        if (this.project?.permissions?.writable) {
+            this.tabs.push(<Tab>{
+                title: 'Advanced',
+                icon: 'graduation',
+                key: 'advanced'
+            })
+        }
+    }
+
+    selectTab(tab: Tab): void {
+        this.selectedTab = tab;
     }
 
     /**
diff --git a/ui/src/app/views/application/show/application.html b/ui/src/app/views/application/show/application.html
index d540a251b..bae928002 100644
--- a/ui/src/app/views/application/show/application.html
+++ b/ui/src/app/views/application/show/application.html
@@ -34,43 +34,20 @@
                 </ng-template>
             </div>
         </app-project-breadcrumb>
-
-        <div class="CDSmenu" *ngIf="application">
-            <sm-menu title="" class="pointing secondary">
-                <a sm-item [class.active]="selectedTab === 'home'" (click)="showTab('home')">
-                    <i class=""></i>{{ 'common_home' | translate }}
-                </a>
-                <a sm-item [class.active]="selectedTab === 'variables'" (click)="showTab('variables')">
-                    <i class="font icon"></i>{{ 'common_variables' | translate }}
-                </a>
-                <a sm-item [class.active]="selectedTab === 'vuln'" (click)="showTab('vuln')">
-                    {{ 'common_vulnerabilities' | translate}}
-                </a>
-                <a sm-item [class.active]="selectedTab === 'usage'" (click)="showTab('usage')">
-                    <i class="map signs icon"></i>{{ 'common_usage' | translate }}{{ ' (' + usageCount + ')' }}
-                </a>
-                <a sm-item [class.active]="selectedTab === 'keys'" id="ApplicationKeysTab" (click)="showTab('keys')"><i
-                        class="privacy icon"></i>
-                    {{ 'common_keys' | translate }}
-                </a>
-                <a sm-item [class.active]="selectedTab === 'advanced'" (click)="showTab('advanced')">
-                    <i class="graduation icon"></i>{{ 'common_advanced' | translate }}
-                </a>
-                <div class="ui small buttons" *ngIf="editMode && !readOnly">
-                    <ng-container *ngIf="application.editModeChanged">
-                        <button class="ui button" (click)="cancelApplication()">{{'common_cancel' | translate }}</button>
-                        <div class="or"></div>
-                    </ng-container>
-                    <button class="ui green button" [disabled]="!application.editModeChanged"
-                            (click)="saveEditMode()">{{'btn_save_application' | translate }}</button>
-                </div>
-            </sm-menu>
-        </div>
+        <app-tabs [tabs]="tabs" (onSelect)="selectTab($event)"></app-tabs>
+        <ng-container *ngIf="editMode && !readOnly">
+            <nz-button-group class="ascodeButton">
+                <ng-container *ngIf="application.editModeChanged">
+                    <button nz-button nzType="default" (click)="cancelApplication()">{{'common_cancel' | translate }}</button>
+                </ng-container>
+                <button nz-button nzType="primary" [disabled]="!application.editModeChanged" (click)="saveEditMode()">{{'btn_save_application' | translate }}</button>
+            </nz-button-group>
+        </ng-container>
 
         <div class="scrollingContent">
             <div class="paddingContent">
                 <div class="viewContent">
-                    <div [ngSwitch]="selectedTab">
+                    <div [ngSwitch]="selectedTab.key">
                         <div *ngSwitchCase="'home'">
                             <app-home [project]="project" [application]="application"></app-home>
                         </div>
diff --git a/ui/src/app/views/application/show/application.scss b/ui/src/app/views/application/show/application.scss
index f33cdef75..72367169b 100644
--- a/ui/src/app/views/application/show/application.scss
+++ b/ui/src/app/views/application/show/application.scss
@@ -13,11 +13,9 @@
   height: 100%;
 }
 
-sm-menu {
-  .ui.buttons {
-    position: absolute;
-    right: 15px;
-    margin-top: 2px;
-  }
+.ascodeButton {
+  position: absolute;
+  right: 15px;
+  top: 107px;
 }
 
diff --git a/ui/src/app/views/application/show/home/application.home.html b/ui/src/app/views/application/show/home/application.home.html
index 4c268a706..bea717660 100644
--- a/ui/src/app/views/application/show/home/application.home.html
+++ b/ui/src/app/views/application/show/home/application.home.html
@@ -52,7 +52,7 @@
                     [queryParams]="{tab: 'advanced'}">{{
                     'application_home_no_integration' | translate }}</a>
             </div>
-            <h4>{{ 'common_usage' | translate}}</h4>
+            <h4>Usage</h4>
             <ng-container *ngIf="application.usage && application.usage.workflows">
                 <ul>
                     <li *ngFor="let w of application.usage.workflows">
diff --git a/ui/src/app/views/environment/show/environment.show.component.ts b/ui/src/app/views/environment/show/environment.show.component.ts
index b24faf585..7bc6224f3 100644
--- a/ui/src/app/views/environment/show/environment.show.component.ts
+++ b/ui/src/app/views/environment/show/environment.show.component.ts
@@ -4,13 +4,11 @@ import { TranslateService } from '@ngx-translate/core';
 import { Store } from '@ngxs/store';
 import { Environment } from 'app/model/environment.model';
 import { Project } from 'app/model/project.model';
-import { AuthentifiedUser } from 'app/model/user.model';
 import { Workflow } from 'app/model/workflow.model';
 import { AsCodeSaveModalComponent } from 'app/shared/ascode/save-modal/ascode.save-modal.component';
 import { AutoUnsubscribe } from 'app/shared/decorator/autoUnsubscribe';
 import { ToastService } from 'app/shared/toast/ToastService';
 import { VariableEvent } from 'app/shared/variable/variable.event.model';
-import { AuthenticationState } from 'app/store/authentication.state';
 import { CleanEnvironmentState } from 'app/store/environment.action';
 import * as envActions from 'app/store/environment.action';
 import { EnvironmentState, EnvironmentStateModel } from 'app/store/environment.state';
@@ -18,6 +16,7 @@ import { ProjectState, ProjectStateModel } from 'app/store/project.state';
 import { cloneDeep } from 'lodash-es';
 import { Subscription } from 'rxjs';
 import { finalize } from 'rxjs/operators';
+import { Tab } from 'app/shared/tabs/tabs.component';
 
 @Component({
     selector: 'app-environment-show',
@@ -50,8 +49,9 @@ export class EnvironmentShowComponent implements OnInit, OnDestroy {
     _routeDataSub: Subscription;
     _queryParamsSub: Subscription;
 
-    // Selected tab
-    selectedTab = 'variables';
+    // tabs
+    tabs: Array<Tab>;
+    selectedTab: Tab;
 
     // queryparam for breadcrum
     workflowName: string;
@@ -123,6 +123,7 @@ export class EnvironmentShowComponent implements OnInit, OnDestroy {
                                 this.workflows = this.environment.usage.workflows || [];
                                 this.usageCount = this.workflows.length;
                             }
+                            this.initTabs();
                             this._cd.markForCheck();
                         }, () => {
                             this._router.navigate(['/project', key], { queryParams: { tab: 'environments' } });
@@ -133,14 +134,51 @@ export class EnvironmentShowComponent implements OnInit, OnDestroy {
     }
 
     ngOnInit() {
+        this.initTabs();
         this._queryParamsSub = this._route.queryParams.subscribe(params => {
             let tab = params['tab'];
             if (tab) {
-                this.selectedTab = tab;
+                let current_tab = this.tabs.find((t) => t.key === tab);
+                if (current_tab) {
+                    this.selectTab(current_tab);
+                }
+                this._cd.markForCheck();
             }
         });
     }
 
+    initTabs() {
+        let usageText = 'Usage';
+        if (this.environment) {
+            usageText = 'Usage (' + this.usageCount + ')';
+        }
+        this.tabs = [<Tab>{
+            title: 'Variables',
+            key: 'variables',
+            default: true,
+            icon: 'font'
+        }, <Tab>{
+            title: 'Keys',
+            key: 'keys',
+            icon: 'privacy'
+        }, <Tab>{
+            title: usageText,
+            icon: 'map signs',
+            key: 'usage'
+        }]
+        if (this.project?.permissions?.writable) {
+            this.tabs.push(<Tab>{
+                title: 'Advanced',
+                icon: 'graduation',
+                key: 'advanced'
+            })
+        }
+    }
+
+    selectTab(tab: Tab): void {
+        this.selectedTab = tab;
+    }
+
     ngOnDestroy() {
         this._store.dispatch(new CleanEnvironmentState());
     }
diff --git a/ui/src/app/views/environment/show/environment.show.html b/ui/src/app/views/environment/show/environment.show.html
index 889837946..f3893355e 100644
--- a/ui/src/app/views/environment/show/environment.show.html
+++ b/ui/src/app/views/environment/show/environment.show.html
@@ -20,38 +20,20 @@
                 </ng-template>
             </div>
         </app-project-breadcrumb>
-
-        <div class="CDSmenu" *ngIf="environment">
-            <sm-menu title="" class="pointing secondary">
-                <a sm-item [class.active]="selectedTab === 'variables'" (click)="showTab('variables')">
-                    <i class="font icon"></i>{{ 'common_variables' | translate }}
-                </a>
-                <a sm-item [class.active]="selectedTab === 'keys'" id="EnvironmentKeysTab" (click)="showTab('keys')"><i
-                        class="privacy icon"></i>
-                    {{ 'common_keys' | translate }}
-                </a>
-                <a sm-item [class.active]="selectedTab === 'usage'" (click)="showTab('usage')">
-                    <i class="map signs icon"></i>{{ 'common_usage' | translate }}{{ ' (' + usageCount + ')' }}
-                </a>
-                <a sm-item [class.active]="selectedTab === 'advanced'" (click)="showTab('advanced')">
-                    <i class="graduation icon"></i>
-                    {{ 'common_advanced' | translate }}
-                </a>
-                <div class="ui small buttons" *ngIf="editMode && !readonly">
-                    <ng-container *ngIf="environment.editModeChanged">
-                        <button class="ui button" (click)="cancelEnvironment()">{{'common_cancel' | translate }}</button>
-                        <div class="or"></div>
-                    </ng-container>
-                    <button class="ui green button" [disabled]="!environment.editModeChanged"
-                            (click)="saveEditMode()">{{'btn_save_environment' | translate }}</button>
-                </div>
-            </sm-menu>
-        </div>
+        <app-tabs [tabs]="tabs" (onSelect)="selectTab($event)"></app-tabs>
+        <ng-container *ngIf="editMode && !readonly">
+            <nz-button-group class="ascodeButton">
+                <ng-container *ngIf="environment.editModeChanged">
+                    <button nz-button nzType="default" (click)="cancelEnvironment()">{{'common_cancel' | translate }}</button>
+                </ng-container>
+                <button nz-button nzType="primary" [disabled]="!environment.editModeChanged" (click)="saveEditMode()">{{'btn_save_environment' | translate }}</button>
+            </nz-button-group>
+        </ng-container>
 
         <div class="scrollingContent">
             <div class="paddingContent">
                 <div class="viewContent">
-                    <div [ngSwitch]="selectedTab">
+                    <div [ngSwitch]="selectedTab.key">
                         <div *ngSwitchCase="'variables'">
                             <ng-container
                                 *ngIf="!readonly">
diff --git a/ui/src/app/views/environment/show/environment.show.scss b/ui/src/app/views/environment/show/environment.show.scss
index e662d1a1b..3e114fd91 100644
--- a/ui/src/app/views/environment/show/environment.show.scss
+++ b/ui/src/app/views/environment/show/environment.show.scss
@@ -8,12 +8,10 @@
         overflow-x: auto;
     }
 
-  sm-menu {
-    .ui.buttons {
-      position: absolute;
-      right: 15px;
-      margin-top: 2px;
-    }
+  .ascodeButton {
+    position: absolute;
+    right: 15px;
+    top: 107px;
   }
 }
 
diff --git a/ui/src/app/views/navbar/navbar.html b/ui/src/app/views/navbar/navbar.html
index 3f5cc3722..56e5a9f28 100644
--- a/ui/src/app/views/navbar/navbar.html
+++ b/ui/src/app/views/navbar/navbar.html
@@ -141,7 +141,7 @@
                             <div class="ui relaxed divided list">
                                 <div class="item">
                                     <div class="content">
-                                        {{ 'common_workflows' | translate }}
+                                        Workflows
                                     </div>
                                 </div>
                             </div>
diff --git a/ui/src/app/views/pipeline/show/pipeline.show.component.spec.ts b/ui/src/app/views/pipeline/show/pipeline.show.component.spec.ts
index 4e660cce0..f4fc2fc40 100644
--- a/ui/src/app/views/pipeline/show/pipeline.show.component.spec.ts
+++ b/ui/src/app/views/pipeline/show/pipeline.show.component.spec.ts
@@ -18,18 +18,18 @@ import { AddPipelineParameter, DeletePipelineParameter, FetchPipeline, UpdatePip
 import { NgxsStoreModule } from 'app/store/store.module';
 import { of } from 'rxjs';
 import 'rxjs/add/observable/of';
-import { Parameter } from '../../../model/parameter.model';
-import { Pipeline } from '../../../model/pipeline.model';
-import { Project } from '../../../model/project.model';
-import { KeyService } from '../../../service/keys/keys.service';
-import { NavbarService } from '../../../service/navbar/navbar.service';
-import { PipelineCoreService } from '../../../service/pipeline/pipeline.core.service';
-import { PipelineService } from '../../../service/pipeline/pipeline.service';
-import { ProjectService } from '../../../service/project/project.service';
-import { ProjectStore } from '../../../service/project/project.store';
-import { ParameterEvent } from '../../../shared/parameter/parameter.event.model';
-import { SharedModule } from '../../../shared/shared.module';
-import { ToastService } from '../../../shared/toast/ToastService';
+import { Parameter } from 'app/model/parameter.model';
+import { Pipeline } from 'app/model/pipeline.model';
+import { Project } from 'app/model/project.model';
+import { KeyService } from 'app/service/keys/keys.service';
+import { NavbarService } from 'app/service/navbar/navbar.service';
+import { PipelineCoreService } from 'app/service/pipeline/pipeline.core.service';
+import { PipelineService } from 'app/service/pipeline/pipeline.service';
+import { ProjectService } from 'app/service/project/project.service';
+import { ProjectStore } from 'app/service/project/project.store';
+import { ParameterEvent } from 'app/shared/parameter/parameter.event.model';
+import { SharedModule } from 'app/shared/shared.module';
+import { ToastService } from 'app/shared/toast/ToastService';
 import { PipelineModule } from '../pipeline.module';
 import { PipelineShowComponent } from './pipeline.show.component';
 
@@ -96,7 +96,6 @@ describe('CDS: Pipeline Show', () => {
         fixture.componentInstance.project = project;
         fixture.componentInstance.ngOnInit();
 
-        expect(fixture.componentInstance.selectedTab).toBe('workflow');
         expect(fixture.componentInstance.pipeline.name).toBe('pip1');
         expect(fixture.componentInstance.project.key).toBe('key1');
 
diff --git a/ui/src/app/views/pipeline/show/pipeline.show.component.ts b/ui/src/app/views/pipeline/show/pipeline.show.component.ts
index f5eb43d28..d0132193f 100644
--- a/ui/src/app/views/pipeline/show/pipeline.show.component.ts
+++ b/ui/src/app/views/pipeline/show/pipeline.show.component.ts
@@ -26,6 +26,7 @@ import { ProjectState, ProjectStateModel } from 'app/store/project.state';
 import cloneDeep from 'lodash-es/cloneDeep';
 import { Subscription } from 'rxjs';
 import { filter, finalize, first } from 'rxjs/operators';
+import { Tab } from 'app/shared/tabs/tabs.component';
 
 @Component({
     selector: 'app-pipeline-show',
@@ -72,8 +73,9 @@ export class PipelineShowComponent implements OnInit, OnDestroy {
     keys: AllKeys;
     asCodeEditorOpen: boolean;
 
-    // Selected tab
-    selectedTab = 'pipeline';
+    // tabs
+    tabs: Array<Tab>;
+    selectedTab: Tab;
 
     readOnly: boolean;
 
@@ -133,6 +135,7 @@ export class PipelineShowComponent implements OnInit, OnDestroy {
     }
 
     ngOnInit() {
+        this.initTabs();
         this.projectKey = this._routeActivated.snapshot.params['key'];
         this.pipName = this._routeActivated.snapshot.params['pipName'];
 
@@ -153,7 +156,10 @@ export class PipelineShowComponent implements OnInit, OnDestroy {
             this.queryParams = params;
             let tab = params['tab'];
             if (tab) {
-                this.selectedTab = tab;
+                let current_tab = this.tabs.find((t) => t.key === tab);
+                if (current_tab) {
+                    this.selectTab(current_tab);
+                }
             }
             this._cd.markForCheck();
         });
@@ -199,14 +205,50 @@ export class PipelineShowComponent implements OnInit, OnDestroy {
                 }
 
                 this.usageCount = this.applications.length + this.environments.length + this.workflows.length;
+                this.initTabs();
                 this._cd.markForCheck();
             }, () => {
                 this._router.navigate(['/project', this.projectKey], { queryParams: { tab: 'pipelines' } });
             });
     }
 
-    showTab(tab: string): void {
-        this._router.navigateByUrl(`/project/${this.project.key}/pipeline/${this.pipeline.name}?tab=${tab}`);
+    initTabs() {
+        let usageText = 'Usage';
+        if (this.pipeline) {
+            usageText = 'Usage (' + this.usageCount + ')';
+        }
+        this.tabs = [<Tab>{
+            title: 'Pipeline',
+            key: 'pipeline',
+            default: true,
+            icon: 'sitemap'
+        }, <Tab>{
+            title: 'Parameters',
+            key: 'parameters',
+            icon: 'font'
+        }, <Tab>{
+            title: usageText,
+            icon: 'map signs',
+            key: 'usage'
+        }]
+        if (!this.pipeline?.from_repository) {
+            this.tabs.push(<Tab>{
+                title: 'Audits',
+                icon: 'history',
+                key: 'audits'
+            })
+        }
+        if (this.project?.permissions?.writable) {
+            this.tabs.push( <Tab>{
+                title: 'Advanced',
+                icon: 'graduation',
+                key: 'advanced'
+            })
+        }
+    }
+
+    selectTab(tab: Tab): void {
+        this.selectedTab = tab;
     }
 
     parameterEvent(event: ParameterEvent, skip?: boolean): void {
diff --git a/ui/src/app/views/pipeline/show/pipeline.show.html b/ui/src/app/views/pipeline/show/pipeline.show.html
index 8dcc29b22..074990331 100644
--- a/ui/src/app/views/pipeline/show/pipeline.show.html
+++ b/ui/src/app/views/pipeline/show/pipeline.show.html
@@ -37,40 +37,19 @@
                 </ng-template>
             </div>
         </app-project-breadcrumb>
-
-        <div class="CDSmenu">
-            <sm-menu title="" class="pointing secondary">
-                <a sm-item [class.active]="selectedTab === 'pipeline'" (click)="showTab('pipeline')">
-                    <i class="sitemap icon"></i>{{ 'common_pipeline' | translate }}
-                </a>
-                <a sm-item [class.active]="selectedTab === 'parameters'" (click)="showTab('parameters')">
-                    <i class="font icon"></i>{{ 'common_parameters' | translate }}
-                </a>
-                <a sm-item [class.active]="selectedTab === 'usage'" [class.disabled]="usageCount === 0"
-                    (click)="usageCount > 0 && showTab('usage')">
-                    <i class="map signs icon"></i>{{ 'common_usage' | translate }}{{ ' (' + usageCount + ')' }}
-                </a>
-                <a sm-item [class.active]="selectedTab === 'audits'" (click)="showTab('audits')">
-                    <i class="history icon"></i>{{'common_audit' | translate }}
-                </a>
-                <a sm-item [class.active]="selectedTab === 'advanced'" (click)="showTab('advanced')"
-                    *ngIf="project.permissions.writable">
-                    <i class="graduation icon"></i>{{ 'common_advanced' | translate }}
-                </a>
-                <div class="ui small buttons" *ngIf="editMode && !readOnly">
-                    <ng-container *ngIf="pipeline.editModeChanged">
-                        <button class="ui button" (click)="cancelPipeline()">{{'common_cancel' | translate }}</button>
-                        <div class="or"></div>
-                    </ng-container>
-                    <button class="ui green button" [disabled]="!pipeline.editModeChanged"
-                        (click)="saveEditMode()">{{'btn_save_pipeline' | translate }}</button>
-                </div>
-            </sm-menu>
-        </div>
+        <app-tabs [tabs]="tabs" (onSelect)="selectTab($event)"></app-tabs>
+        <ng-container *ngIf="editMode && !readOnly">
+            <nz-button-group class="ascodeButton">
+                <ng-container *ngIf="pipeline.editModeChanged">
+                    <button nz-button nzType="default" (click)="cancelPipeline()">{{'common_cancel' | translate }}</button>
+                </ng-container>
+                <button nz-button nzType="primary" [disabled]="!pipeline.editModeChanged" (click)="saveEditMode()">{{'btn_save_pipeline' | translate }}</button>
+            </nz-button-group>
+        </ng-container>
 
         <div class="scrollingContent">
             <div class="paddingContent">
-                <div class="viewContent" [ngSwitch]="selectedTab">
+                <div class="viewContent" [ngSwitch]="selectedTab.key">
                     <div *ngSwitchCase="'pipeline'">
                         <app-pipeline-workflow [project]="project" [currentPipeline]="pipeline"
                             [queryParams]="queryParams" [editMode]="editMode" [readOnly]="readOnly">
diff --git a/ui/src/app/views/pipeline/show/pipeline.show.scss b/ui/src/app/views/pipeline/show/pipeline.show.scss
index 3a9567173..cc78d0a22 100644
--- a/ui/src/app/views/pipeline/show/pipeline.show.scss
+++ b/ui/src/app/views/pipeline/show/pipeline.show.scss
@@ -5,10 +5,8 @@
   height: 100%;
 }
 
-sm-menu {
-  .ui.buttons {
-    position: absolute;
-    right: 15px;
-    margin-top: 2px;
-  }
+.ascodeButton {
+  position: absolute;
+  right: 15px;
+  top: 107px;
 }
diff --git a/ui/src/app/views/project/show/project.component.ts b/ui/src/app/views/project/show/project.component.ts
index 12d663398..a1f06ed92 100644
--- a/ui/src/app/views/project/show/project.component.ts
+++ b/ui/src/app/views/project/show/project.component.ts
@@ -97,46 +97,48 @@ export class ProjectShowComponent implements OnInit, OnDestroy, AfterViewInit {
 
     ngOnInit() {
         this.tabs = [<Tab>{
-            translate: 'common_workflows',
+            title: 'Workflows',
             icon: 'share alternate',
             key: 'workflows',
             default: true,
         }, <Tab>{
-            translate: 'common_applications',
+            title: 'Applications',
             icon: 'rocket',
             key: 'applications'
         }, <Tab>{
-            translate: 'common_pipelines',
+            title: 'Pipelines',
             icon: 'sitemap',
             key: 'pipelines'
         }, <Tab>{
-            translate: 'common_environments',
+            title: 'Environments',
             icon: 'tree',
             key: 'environments'
         }, <Tab>{
-            translate: 'common_variables',
+            title: 'Variables',
             icon: 'font',
             key: 'variables'
         }, <Tab>{
             key: 'permissions',
         }, <Tab>{
-            translate: 'common_keys',
+            title: 'Keys',
             icon: 'privacy',
             key: 'keys'
         }, <Tab>{
-            translate: 'common_integrations',
+            title: 'Integrations',
             icon: 'plug',
             key: 'integrations'
         }, <Tab>{
-            translate: 'common_warnings',
+            title: 'Warnings',
             icon: 'bug',
             key: 'warnings'
-        }, <Tab>{
-            translate: 'common_advanced',
-            icon: 'graduation',
-            key: 'advanced'
-        }];
-
+        }]
+        if (this.project?.permissions?.writable) {
+            this.tabs.push(<Tab>{
+                title: 'Advanced',
+                icon: 'graduation',
+                key: 'advanced'
+            })
+        }
         this._route.queryParams.subscribe((queryParams) => {
             if (queryParams['tab']) {
                 let current_tab = this.tabs.find((tab) => tab.key === queryParams['tab']);
diff --git a/ui/src/app/views/project/show/project.html b/ui/src/app/views/project/show/project.html
index 91c673804..c626e1392 100644
--- a/ui/src/app/views/project/show/project.html
+++ b/ui/src/app/views/project/show/project.html
@@ -1,6 +1,6 @@
 <ng-template #tabPermissionTemplate>
     <i [ngClass]="['users', 'icon']"></i>
-    {{'common_permissions' | translate}}
+    Permissions
     <ng-template let-popup #warnPermission>
         Following groups that are not from organization "{{project.organization}}" can read the
         project:
diff --git a/ui/src/app/views/settings/action/edit/action.edit.component.ts b/ui/src/app/views/settings/action/edit/action.edit.component.ts
index 9c639553e..170413ffc 100644
--- a/ui/src/app/views/settings/action/edit/action.edit.component.ts
+++ b/ui/src/app/views/settings/action/edit/action.edit.component.ts
@@ -51,16 +51,16 @@ export class ActionEditComponent implements OnInit, OnDestroy {
 
     ngOnInit() {
         this.tabs = [<Tab>{
-            translate: 'common_action',
+            title: 'Action',
             icon: 'list',
             key: 'action',
             default: true
         }, <Tab>{
-            translate: 'common_audit',
+            title: 'Audit',
             icon: 'history',
             key: 'audits'
         }, <Tab>{
-            translate: 'common_usage',
+            title: 'Usage',
             icon: 'map signs',
             key: 'usage'
         }];
@@ -85,7 +85,7 @@ export class ActionEditComponent implements OnInit, OnDestroy {
             <Column<AuditAction>>{
                 disabled: true,
                 type: ColumnType.CONFIRM_BUTTON,
-                name: 'common_action',
+                name: 'Action',
                 class: 'two right aligned',
                 selector: (aa: AuditAction) => ({
                         title: 'common_rollback',
diff --git a/ui/src/app/views/settings/action/list/action.list.component.ts b/ui/src/app/views/settings/action/list/action.list.component.ts
index ce4eac3ff..43195fcca 100644
--- a/ui/src/app/views/settings/action/list/action.list.component.ts
+++ b/ui/src/app/views/settings/action/list/action.list.component.ts
@@ -28,12 +28,12 @@ export class ActionListComponent {
         private _cd: ChangeDetectorRef
     ) {
         this.tabs = [<Tab>{
-            translate: 'action_custom',
+            title: 'Custom',
             icon: '',
             key: 'custom',
             default: true
         }, <Tab>{
-            translate: 'action_builtin',
+            title: 'Builtin',
             icon: '',
             key: 'builtin'
         }];
diff --git a/ui/src/app/views/settings/action/show/action.show.component.ts b/ui/src/app/views/settings/action/show/action.show.component.ts
index 9847006fe..19a380244 100644
--- a/ui/src/app/views/settings/action/show/action.show.component.ts
+++ b/ui/src/app/views/settings/action/show/action.show.component.ts
@@ -37,12 +37,12 @@ export class ActionShowComponent implements OnInit, OnDestroy {
 
     ngOnInit() {
         this.tabs = [<Tab>{
-            translate: 'common_action',
+            title: 'Action',
             icon: 'list',
             key: 'action',
             default: true
         }, <Tab>{
-            translate: 'common_usage',
+            title: 'Usage',
             icon: 'map signs',
             key: 'usage'
         }];
diff --git a/ui/src/app/views/settings/user/consumer-create-modal/scope-detail.html b/ui/src/app/views/settings/user/consumer-create-modal/scope-detail.html
index c27f6cf69..da4a36c4b 100644
--- a/ui/src/app/views/settings/user/consumer-create-modal/scope-detail.html
+++ b/ui/src/app/views/settings/user/consumer-create-modal/scope-detail.html
@@ -4,7 +4,7 @@
             {{scopeDetail.scope}}
         </div>
         <div class="ui twelve wide column right aligned">
-            <a class="pointing advanced" (click)="clickAdvanced()">{{'common_advanced' | translate}}</a>
+            <a class="pointing advanced" (click)="clickAdvanced()">Advanced</a>
             <div class="ui checkbox">
                 <input type="checkbox" [ngModel]="allReadRoutesSelected" (click)="clickSelectAllRoutesRead()">
                 <label>{{'common_read' | translate}}</label>
diff --git a/ui/src/app/views/settings/user/consumer-details-modal/consumer-details-modal.component.ts b/ui/src/app/views/settings/user/consumer-details-modal/consumer-details-modal.component.ts
index 6ce26dcb7..f3d78ec0e 100644
--- a/ui/src/app/views/settings/user/consumer-details-modal/consumer-details-modal.component.ts
+++ b/ui/src/app/views/settings/user/consumer-details-modal/consumer-details-modal.component.ts
@@ -120,7 +120,7 @@ export class ConsumerDetailsModalComponent {
             },
             <Column<AuthConsumer>>{
                 type: ColumnType.BUTTON,
-                name: 'common_action',
+                name: 'Action',
                 class: 'two right aligned',
                 selector: (c: AuthConsumer) => ({
                         title: 'common_details',
diff --git a/ui/src/app/views/settings/user/edit/user.edit.component.ts b/ui/src/app/views/settings/user/edit/user.edit.component.ts
index 44a9f2cb6..54e126372 100644
--- a/ui/src/app/views/settings/user/edit/user.edit.component.ts
+++ b/ui/src/app/views/settings/user/edit/user.edit.component.ts
@@ -244,7 +244,7 @@ export class UserEditComponent implements OnInit {
             },
             <Column<AuthConsumer>>{
                 type: ColumnType.BUTTON,
-                name: 'common_action',
+                name: 'Action',
                 class: 'two right aligned',
                 selector: (c: AuthConsumer) => ({
                     title: 'common_details',
@@ -345,7 +345,7 @@ export class UserEditComponent implements OnInit {
             },
             <Column<AuthSession>>{
                 type: ColumnType.CONFIRM_BUTTON,
-                name: 'common_action',
+                name: 'Action',
                 class: 'two right aligned',
                 disabled: true,
                 selector: (s: AuthSession) => ({
diff --git a/ui/src/app/views/settings/user/edit/user.edit.html b/ui/src/app/views/settings/user/edit/user.edit.html
index a9a51d77f..03d2af1c3 100644
--- a/ui/src/app/views/settings/user/edit/user.edit.html
+++ b/ui/src/app/views/settings/user/edit/user.edit.html
@@ -15,7 +15,7 @@
                                     <div class="field">
                                         <label>{{'user_label_username' | translate}}</label>
                                         <input class="ui input" type="text" name="username" [(ngModel)]="user.username"
-                                            [disabled]="loading" [readonly]="!editable">
+                                            [disabled]="loading" readonly>
                                     </div>
                                     <div class="field">
                                         <label>{{'user_label_fullname' | translate}}</label>
diff --git a/ui/src/app/views/settings/worker-model/edit/worker-model.edit.component.ts b/ui/src/app/views/settings/worker-model/edit/worker-model.edit.component.ts
index 081b9b419..775c1dc75 100644
--- a/ui/src/app/views/settings/worker-model/edit/worker-model.edit.component.ts
+++ b/ui/src/app/views/settings/worker-model/edit/worker-model.edit.component.ts
@@ -64,16 +64,16 @@ export class WorkerModelEditComponent implements OnInit, OnDestroy {
 
     ngOnInit() {
         this.tabs = [<Tab>{
-            translate: 'worker_model',
+            title: 'Worker Model',
             icon: '',
             key: 'worker_model',
             default: true
         }, <Tab>{
-            translate: 'common_capabilities',
+            title: 'Capabilities',
             icon: 'file outline',
             key: 'capabilities'
         }, <Tab>{
-            translate: 'common_usage',
+            title: 'Usage',
             icon: 'map signs',
             key: 'usage'
         }];
diff --git a/ui/src/app/views/settings/workflow-template/edit/workflow-template.edit.component.ts b/ui/src/app/views/settings/workflow-template/edit/workflow-template.edit.component.ts
index b05e2c7df..4ed829317 100644
--- a/ui/src/app/views/settings/workflow-template/edit/workflow-template.edit.component.ts
+++ b/ui/src/app/views/settings/workflow-template/edit/workflow-template.edit.component.ts
@@ -76,20 +76,20 @@ export class WorkflowTemplateEditComponent implements OnInit, OnDestroy {
 
     ngOnInit() {
         this.tabs = [<Tab>{
-            translate: 'workflow_template',
+            title: 'Workflow template',
             icon: 'paste',
             key: 'workflow_template',
             default: true
         }, <Tab>{
-            translate: 'workflow_template_instances',
+            title: 'Instances',
             icon: 'file outline',
             key: 'instances'
         }, <Tab>{
-            translate: 'common_audit',
+            title: 'Audit',
             icon: 'history',
             key: 'audits'
         }, <Tab>{
-            translate: 'common_usage',
+            title: 'Usage',
             icon: 'map signs',
             key: 'usage'
         }];
@@ -172,7 +172,7 @@ export class WorkflowTemplateEditComponent implements OnInit, OnDestroy {
                 }
             }, <Column<WorkflowTemplateInstance>>{
                 type: ColumnType.BUTTON,
-                name: 'common_action',
+                name: 'Action',
                 class: 'two right aligned',
                 selector: (i: WorkflowTemplateInstance) => ({
                         title: 'common_update',
@@ -208,7 +208,7 @@ export class WorkflowTemplateEditComponent implements OnInit, OnDestroy {
                 if (this.workflowTemplate.editable) {
                     this.columnsAudits.push(<Column<AuditWorkflowTemplate>>{
                         type: ColumnType.CONFIRM_BUTTON,
-                        name: 'common_action',
+                        name: 'Action',
                         class: 'two right aligned',
                         selector: (a: AuditWorkflowTemplate) => ({
                                 title: 'common_rollback',
@@ -349,10 +349,7 @@ export class WorkflowTemplateEditComponent implements OnInit, OnDestroy {
                 this.tabs = this.tabs.map((tab) => {
                     tab.default = false;
                     if (tab.key === 'instances') {
-                        tab.translate = 'workflow_template_instances_with_nb';
-                        tab.translate_args = {
-                            nb: this.instances.length,
-                        };
+                        tab.title = 'Instances ' + this.instances.length;
                     }
 
                     return tab;
diff --git a/ui/src/app/views/settings/workflow-template/form/workflow-template.form.html b/ui/src/app/views/settings/workflow-template/form/workflow-template.form.html
index a4f4ae92a..16fdb6f48 100644
--- a/ui/src/app/views/settings/workflow-template/form/workflow-template.form.html
+++ b/ui/src/app/views/settings/workflow-template/form/workflow-template.form.html
@@ -131,7 +131,7 @@
                 </div>
                 <div *ngIf="(workflowTemplate.editable && !importFromURL) || pipelineKeys.length > 0"
                     class="wide field">
-                    <label>{{ 'common_pipelines' | translate }}</label>
+                    <label>Pipelines</label>
                     <div class="codeContainer field" *ngFor="let k of pipelineKeys">
                         <app-workflow-template-editor name="pipeline-value-{{k}}"
                             [editable]="workflowTemplate.editable && !importFromURL" [removable]="true"
@@ -146,7 +146,7 @@
                 </div>
                 <div *ngIf="(workflowTemplate.editable && !importFromURL) || applicationKeys.length > 0"
                     class="wide field">
-                    <label>{{ 'common_applications' | translate }}</label>
+                    <label>Applications</label>
                     <div class="codeContainer field" *ngFor="let k of applicationKeys">
                         <app-workflow-template-editor name="application-value-{{k}}"
                             [editable]="workflowTemplate.editable && !importFromURL" [removable]="true"
@@ -161,7 +161,7 @@
                 </div>
                 <div *ngIf="(workflowTemplate.editable && !importFromURL) || environmentKeys.length > 0"
                     class="wide field">
-                    <label>{{ 'common_environments' | translate }}</label>
+                    <label>Environments</label>
                     <div class="codeContainer field" *ngFor="let k of environmentKeys">
                         <app-workflow-template-editor name="environment-value-{{k}}"
                             [editable]="workflowTemplate.editable && !importFromURL" [removable]="true"
diff --git a/ui/src/app/views/workflow/run/node/node.html b/ui/src/app/views/workflow/run/node/node.html
index ca4d2070f..e1601630c 100644
--- a/ui/src/app/views/workflow/run/node/node.html
+++ b/ui/src/app/views/workflow/run/node/node.html
@@ -2,61 +2,13 @@
     <ng-container>
         <app-workflow-node-run-summary></app-workflow-node-run-summary>
     </ng-container>
-    <div class="CDSmenu">
-        <sm-menu title="" class="pointing secondary">
-            <a sm-item [class.active]="selectedTab === 'pipeline'" (click)="showTab('pipeline')">
-                <i class="sitemap icon"></i>
-                {{ 'common_pipeline' | translate }}
-            </a>
-            <a sm-item [class.active]="selectedTab === 'commit'" [class.disabled]="commitsLength === 0"
-                (click)="commitsLength !== 0 && showTab('commit')">
-                {{ commitsLength + ' '}}
-                <span *ngIf="commitsLength > 1">{{ 'common_commits' | translate }}</span>
-                <span *ngIf="commitsLength < 2">{{ 'common_commit' | translate }}</span>
-            </a>
-            <a sm-item [class.active]="selectedTab === 'vuln'" [class.disabled]="!hasVulnerability"
-                (click)="showTab('vuln')">
-                <span *ngIf="nbVuln > 0 || deltaVul !== 0">{{nbVuln + ' '}}</span>
-                <span>{{ 'common_vulnerabilities' | translate }}</span>
-                <span *ngIf="deltaVul !== 0">
-                    (
-                    <span *ngIf="deltaVul < 0"><i class="green caret down icon"></i></span>
-                    <span *ngIf="deltaVul > 0"><i class="red caret up icon"></i></span>
-                    <span *ngIf="deltaVul !== 0">{{deltaVul}}</span>
-                    )
-                </span>
-            </a>
-            <a sm-item [class.active]="selectedTab === 'test'" (click)="nodeRunTests?.total !== 0 && showTab('test')" [class.disabled]="!nodeRunTests?.total">
-                <span *ngIf="nodeRunTests?.total > 1">{{ 'common_tests' | translate }}</span>
-                <span *ngIf="nodeRunTests?.total < 2">{{ 'common_test' | translate }}</span>
-                <ng-container *ngIf="nodeRunTests?.total > 0">
-                (
-                    <ng-container *ngIf="nodeRunTests?.ok"><i class="green check icon no-mrr"></i>{{nodeRunTests.ok}}</ng-container>
-                    <ng-container *ngIf="nodeRunTests?.ko"><i class="red remove icon status"></i>{{nodeRunTests.ko}}</ng-container>
-                    <ng-container *ngIf="nodeRunTests?.skipped"><i class="grey ban icon status"></i>{{nodeRunTests.skipped}}</ng-container>
-                )
-                </ng-container>
-            </a>
-            <a sm-item [class.active]="selectedTab === 'artifact'"
-                (click)="artifactLength !== 0 && showTab('artifact')"
-                [class.disabled]="artifactLength === 0">
-                <span *ngIf="artifactLength > 1">{{ 'common_artifacts' | translate }}</span>
-                <span *ngIf="artifactLength < 2">{{ 'common_artifact' | translate }}</span>
-                <span *ngIf="artifactLength">{{ ' (' + artifactLength + ')'}}</span>
-            </a>
-            <a sm-item [class.active]="selectedTab === 'history'" (click)="historyLength !== 0 && showTab('history')"
-                [class.disabled]="historyLength === 0">
-                {{ 'common_history' | translate }}
-                {{ ' (' + historyLength + ')'}}
-            </a>
-        </sm-menu>
-    </div>
-    <div class="pipeline" *ngIf="selectedTab === 'pipeline'">
+    <app-tabs [tabs]="tabs" (onSelect)="selectTab($event)"></app-tabs>
+    <div class="pipeline" *ngIf="selectedTab.key === 'pipeline'">
         <app-node-run-pipeline></app-node-run-pipeline>
     </div>
-    <div *ngIf="selectedTab !== 'pipeline'" class="scrollingContent">
+    <div *ngIf="selectedTab.key !== 'pipeline'" class="scrollingContent">
         <div class="paddingContent">
-            <div class="viewContent" [ngSwitch]="selectedTab">
+            <div class="viewContent" [ngSwitch]="selectedTab.key">
                 <div *ngSwitchCase="'artifact'">
                     <app-workflow-artifact-list></app-workflow-artifact-list>
                 </div>
diff --git a/ui/src/app/views/workflow/run/node/workflow.run.node.component.ts b/ui/src/app/views/workflow/run/node/workflow.run.node.component.ts
index b38d31dc2..161d13a42 100644
--- a/ui/src/app/views/workflow/run/node/workflow.run.node.component.ts
+++ b/ui/src/app/views/workflow/run/node/workflow.run.node.component.ts
@@ -11,6 +11,7 @@ import { ProjectState } from 'app/store/project.state';
 import { GetWorkflowNodeRun, GetWorkflowRun } from 'app/store/workflow.action';
 import { WorkflowState, WorkflowStateModel } from 'app/store/workflow.state';
 import { Observable, Subscription } from 'rxjs';
+import { Tab } from 'app/shared/tabs/tabs.component';
 
 @Component({
     selector: 'app-workflow-run-node',
@@ -41,7 +42,10 @@ export class WorkflowNodeRunComponent implements OnInit, OnDestroy {
 
     // History
     nodeRunsHistory = new Array<WorkflowNodeRun>();
-    selectedTab: string;
+
+    // tabs
+    tabs: Array<Tab>;
+    selectedTab: Tab;
 
     nbVuln = 0;
     deltaVul = 0;
@@ -56,14 +60,16 @@ export class WorkflowNodeRunComponent implements OnInit, OnDestroy {
         private _titleService: Title,
         private _cd: ChangeDetectorRef
     ) {
+        this.initTabs();
         this.project = this._store.selectSnapshot(ProjectState.projectSnapshot);
 
         // Tab selection
         this._activatedRoute.queryParams.subscribe(q => {
             if (q['tab']) {
-                this.selectedTab = q['tab'];
-            } else {
-                this.selectedTab = 'pipeline';
+                let current_tab = this.tabs.find((t) => t.key === q['tab']);
+                if (current_tab) {
+                    this.selectTab(current_tab);
+                }
             }
             this.pipelineName = q['name'] || '';
             this._cd.markForCheck();
@@ -154,18 +160,62 @@ export class WorkflowNodeRunComponent implements OnInit, OnDestroy {
                 }
             }
             if (refresh) {
+                this.initTabs();
                 this._cd.markForCheck();
             }
         });
     }
 
-    showTab(tab: string): void {
-        let queryParams = Object.assign({}, this._activatedRoute.snapshot.queryParams, { tab });
-        let navExtras: NavigationExtras = { queryParams };
-        this._router.navigate(['project', this.project.key,
-            'workflow', this.workflowName,
-            'run', this.currentNodeRunNum,
-            'node', this.currentNodeRunID], navExtras);
+    initTabs() {
+        let commitTitle = this.commitsLength > 1? 'Commits' : 'Commit';
+        if (this.commitsLength > 0) {
+            commitTitle = this.commitsLength.toString() + ' ' + commitTitle;
+        }
+
+        let testTitle = this.nodeRunTests?.total > 1? 'Tests' : 'Test';
+        let testIcon: string
+        if (this.nodeRunTests?.total > 0) {
+            testIcon = 'green check no-mrr';
+            testTitle = this.nodeRunTests?.total + ' ' + testTitle;
+        }
+        if (this.nodeRunTests?.ko > 0) {
+            testTitle += ` (${this.nodeRunTests.ko} ko)`
+            testIcon = 'red remove status';
+        }
+
+        let artifactTitle = this.artifactLength > 1 ? 'Artifacts' : 'Artifact';
+        if (this.artifactLength > 0) {
+            artifactTitle = artifactTitle + ' ('+ this.artifactLength+')';
+        }
+
+        let historyTitle = 'History' + ' ('+ this.historyLength+')';
+
+        this.tabs = [<Tab>{
+            title: 'Pipeline',
+            key: 'pipeline',
+            default: true,
+            icon: 'sitemap'
+        }, <Tab>{
+            title: commitTitle,
+            key: 'commit',
+            disabled: this.commitsLength === 0
+        }, <Tab>{
+            title: testTitle,
+            key: 'test',
+            icon: testIcon,
+            disabled: !this.nodeRunTests || this.nodeRunTests?.total === 0
+        }, <Tab>{
+            title: artifactTitle,
+            key: 'artifact',
+            disabled: this.artifactLength === 0
+        }, <Tab>{
+            title: historyTitle,
+            key: 'history'
+        }]
+    }
+
+    selectTab(tab: Tab): void {
+        this.selectedTab = tab;
     }
 
     initVulnerabilitySummary(nodeRun: WorkflowNodeRun): any[] {
diff --git a/ui/src/app/views/workflow/show/workflow.component.ts b/ui/src/app/views/workflow/show/workflow.component.ts
index 7ade176d4..3f2438f37 100644
--- a/ui/src/app/views/workflow/show/workflow.component.ts
+++ b/ui/src/app/views/workflow/show/workflow.component.ts
@@ -1,4 +1,13 @@
-import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
+import {
+    AfterViewInit,
+    ChangeDetectionStrategy,
+    ChangeDetectorRef,
+    Component,
+    OnDestroy,
+    OnInit,
+    TemplateRef,
+    ViewChild
+} from '@angular/core';
 import { ActivatedRoute, Router } from '@angular/router';
 import { TranslateService } from '@ngx-translate/core';
 import { Store } from '@ngxs/store';
@@ -19,6 +28,7 @@ import { WorkflowState, WorkflowStateModel } from 'app/store/workflow.state';
 import cloneDeep from 'lodash-es/cloneDeep';
 import { Subscription } from 'rxjs';
 import { finalize, first, tap } from 'rxjs/operators';
+import { Tab } from 'app/shared/tabs/tabs.component';
 
 @Component({
     selector: 'app-workflow',
@@ -27,7 +37,7 @@ import { finalize, first, tap } from 'rxjs/operators';
     changeDetection: ChangeDetectionStrategy.OnPush
 })
 @AutoUnsubscribe()
-export class WorkflowShowComponent implements OnInit, OnDestroy {
+export class WorkflowShowComponent implements OnInit, OnDestroy, AfterViewInit {
 
     project: Project;
     detailedWorkflow: Workflow;
@@ -51,9 +61,12 @@ export class WorkflowShowComponent implements OnInit, OnDestroy {
     @ViewChild('updateAsCode')
     updateAsCodeModal: AsCodeSaveModalComponent;
 
+    @ViewChild('warnPermission') warnPermission: TemplateRef<any>;
+
     selectedHookRef: string;
 
-    selectedTab = 'workflows';
+    tabs: Array<Tab>;
+    selectedTab: Tab;
 
     permFormLoading = false;
 
@@ -72,6 +85,12 @@ export class WorkflowShowComponent implements OnInit, OnDestroy {
         private _cd: ChangeDetectorRef
     ) { }
 
+    ngAfterViewInit(): void {
+        if (this.detailedWorkflow) {
+            this.initTabs();
+        }
+    }
+
     ngOnDestroy(): void { } // Should be set to use @AutoUnsubscribe with AOT
 
     ngOnInit(): void {
@@ -114,6 +133,8 @@ export class WorkflowShowComponent implements OnInit, OnDestroy {
 
                 this.usageCount = Object.keys(this.detailedWorkflow.usage).reduce((total, key) =>
                     total + this.detailedWorkflow.usage[key].length, 0);
+
+                this.initTabs();
             }
         });
 
@@ -125,7 +146,10 @@ export class WorkflowShowComponent implements OnInit, OnDestroy {
 
         this.qpsSubs = this.activatedRoute.queryParams.subscribe(params => {
             if (params['tab']) {
-                this.selectedTab = params['tab'];
+                let current_tab = this.tabs.find((t) => t.key === params['tab']);
+                if (current_tab) {
+                    this.selectTab(current_tab);
+                }
             }
             if (params['hook_ref']) {
                 this.selectedHookRef = params['hook_ref'];
@@ -143,6 +167,54 @@ export class WorkflowShowComponent implements OnInit, OnDestroy {
             });
     }
 
+    initTabs() {
+        let graphTab = <Tab> {
+            title: 'Workflows',
+            key: 'workflows',
+            icon: 'share alternate',
+            default: true
+        }
+        if (this.previewWorkflow) {
+            graphTab.warningText = this._translate.instant('workflow_preview_mode');
+        }
+        let notificationTab = <Tab>{
+            title: 'Notifications',
+            key: 'notifications',
+            icon: 'alarm'
+        };
+        let permissionTab = <Tab>{
+            title: 'Permissions',
+            key: 'permissions',
+            icon: 'users'
+        }
+        if (this.groupsOutsideOrganization && this.groupsOutsideOrganization.length > 0) {
+            permissionTab.warningTemplate = this.warnPermission
+        }
+
+        this.tabs = new Array<Tab>();
+        this.tabs.push(graphTab, notificationTab, permissionTab)
+
+        if (!this.detailedWorkflow.from_repository)   {
+            this.tabs.push(<Tab>{
+                title: 'Audit',
+                icon: 'history',
+                key: 'audits',
+            });
+        }
+        this.tabs.push(<Tab>{
+            title: 'Usage',
+            icon: 'map signs',
+            key: 'usage'
+        });
+        if (this.detailedWorkflow.permissions.writable) {
+            this.tabs.push( <Tab>{
+                title: 'Advanced',
+                icon: 'graduation',
+                key: 'advanced'
+            });
+        }
+    }
+
     savePreview() {
         this._workflowCoreService.toggleAsCodeEditor({ open: false, save: true });
     }
@@ -151,8 +223,8 @@ export class WorkflowShowComponent implements OnInit, OnDestroy {
         this.direction = this.direction === 'LR' ? 'TB' : 'LR';
     }
 
-    showTab(tab: string): void {
-        this._router.navigateByUrl('/project/' + this.project.key + '/workflow/' + this.detailedWorkflow.name + '?tab=' + tab);
+    selectTab(tab: Tab): void {
+        this.selectedTab = tab;
     }
 
     showAsCodeEditor() {
diff --git a/ui/src/app/views/workflow/show/workflow.html b/ui/src/app/views/workflow/show/workflow.html
index 08778e6b1..43ab252d6 100644
--- a/ui/src/app/views/workflow/show/workflow.html
+++ b/ui/src/app/views/workflow/show/workflow.html
@@ -1,63 +1,16 @@
 <div *ngIf="detailedWorkflow && project; then workflowPart;else loadingPart"></div>
 <ng-template #workflowPart>
     <div class="workflowShow">
-        <div class="CDSmenu">
-            <sm-menu title="" class="pointing secondary">
-                <a sm-item [class.active]="selectedTab === 'workflows'" id="WorkflowGraphTabs"
-                    (click)="showTab('workflows')">
-                    <i class="share alternate icon"></i>
-                    {{'common_workflows' | translate }}
-                    <i class="orange warning sign icon ml5" *ngIf="previewWorkflow" suiPopup
-                        [popupText]="'workflow_preview_mode' | translate" popupPlacement="top center">
-                    </i>
-                </a>
-                <a sm-item [class.active]="selectedTab === 'notifications'" id="WorkflowNotificationTabs"
-                    (click)="showTab('notifications')">
-                    <i class="alarm icon"></i>
-                    {{'common_notifications' | translate }}</a>
-                <a sm-item [class.active]="selectedTab === 'permissions'" id="WorkflowPermissionsTabs"
-                    (click)="showTab('permissions')">
-                    <i class="users icon"></i>
-                    {{'common_permissions' | translate }}
-                    <ng-template let-popup #warnPermission>
-                        Following groups that are not from organization "{{detailedWorkflow.organization}}" can read the
-                        workflow:
-                        <span *ngFor="let gp of groupsOutsideOrganization;let i = index">{{gp.group.name}}
-                            ({{gp.group.organization}}){{ i < groupsOutsideOrganization.length - 1 ? ', ' : '' }}</span>
-                                .
-                    </ng-template>
-                    <i *ngIf="detailedWorkflow && groupsOutsideOrganization && groupsOutsideOrganization.length > 0"
-                        class="exclamation triangle orange icon" suiPopup [popupTemplate]="warnPermission"
-                        popupPlacement="bottom left"></i>
-                </a>
-                <ng-container *ngIf="!detailedWorkflow.from_repository">
-                    <a sm-item [class.active]="selectedTab === 'audits'" id="WorkflowAuditsTabs"
-                        (click)="showTab('audits')">
-                        <i class="history icon"></i>
-                        {{'common_audit' | translate }}
-                    </a>
+        <app-tabs [tabs]="tabs" (onSelect)="selectTab($event)"></app-tabs>
+        <ng-container *ngIf="editMode && !isReadOnly">
+            <nz-button-group class="ascodeButton">
+                <ng-container *ngIf="editModeWorkflowChanged">
+                    <button nz-button nzType="default" (click)="rollbackWorkflow()">{{'common_cancel' | translate }}</button>
                 </ng-container>
-
-                <a sm-item [class.active]="selectedTab === 'usage'" id="WorkflowUsageTabs"
-                    [class.disabled]="usageCount === 0" (click)="usageCount !== 0 && showTab('usage')">
-                    <i class="map signs icon"></i>
-                    {{'common_usage' | translate }}{{ ' (' + usageCount + ')' }}
-                </a>
-                <a sm-item [class.active]="selectedTab === 'advanced'" id="WorkflowAdvancesTabs"
-                    (click)="showTab('advanced')" *ngIf="detailedWorkflow.permissions.writable">
-                    <i class="graduation icon"></i>
-                    {{'common_advanced' | translate }}</a>
-                <div class="ui small buttons" *ngIf="editMode && !isReadOnly">
-                    <ng-container *ngIf="editModeWorkflowChanged">
-                        <button class="ui button" (click)="rollbackWorkflow()">{{'common_cancel' | translate }}</button>
-                        <div class="or"></div>
-                    </ng-container>
-                    <button class="ui green button" [disabled]="!editModeWorkflowChanged"
-                        (click)="saveWorkflow()">{{'btn_save_workflow' | translate }}</button>
-                </div>
-            </sm-menu>
-        </div>
-        <div *ngIf="selectedTab === 'workflows'" class="workflowShowGraph">
+                    <button nz-button nzType="primary" [disabled]="!editModeWorkflowChanged" (click)="saveWorkflow()">{{'btn_save_workflow' | translate }}</button>
+            </nz-button-group>
+        </ng-container>
+        <div *ngIf="selectedTab.key === 'workflows'" class="workflowShowGraph">
             <div class="ui grid">
                 <div class="row">
                     <div class="two wide column"></div>
@@ -117,7 +70,7 @@
             <app-workflow-graph *ngIf="detailedWorkflow" [workflowData]="previewWorkflow || detailedWorkflow"
                 [project]="project" [direction]="direction"></app-workflow-graph>
         </div>
-        <app-scrollview class="scrollview" *ngIf="selectedTab !== 'workflows'" [ngSwitch]="selectedTab">
+        <app-scrollview class="scrollview" *ngIf="selectedTab.key !== 'workflows'" [ngSwitch]="selectedTab.key">
             <div *ngSwitchCase="'notifications'">
                 <app-workflow-notification-list [project]="project" [workflow]="detailedWorkflow" [editMode]="editMode"
                     [readOnly]="isReadOnly"></app-workflow-notification-list>
@@ -173,3 +126,9 @@
     <app-ascode-save-modal [project]="project" [workflow]="detailedWorkflow" [name]="detailedWorkflow.name"
         #updateAsCode></app-ascode-save-modal>
 </ng-container>
+<ng-template #warnPermission>
+    Following groups that are not from organization "{{detailedWorkflow.organization}}" can read the workflow:
+    <span *ngFor="let gp of groupsOutsideOrganization;let i = index">{{gp.group.name}}
+        ({{gp.group.organization}}){{ i < groupsOutsideOrganization.length - 1 ? ', ' : '' }}
+    </span>
+</ng-template>
diff --git a/ui/src/app/views/workflow/show/workflow.scss b/ui/src/app/views/workflow/show/workflow.scss
index bb5009be6..4382c5f07 100644
--- a/ui/src/app/views/workflow/show/workflow.scss
+++ b/ui/src/app/views/workflow/show/workflow.scss
@@ -36,11 +36,9 @@
     }
   }
 
-  sm-menu {
-    .ui.buttons {
-      position: absolute;
-      right: 55px;
-      margin-top: 2px;
-    }
+  .ascodeButton {
+    position: absolute;
+    right: 15px;
+    top: 107px;
   }
 }
diff --git a/ui/src/app/views/workflowv3/run/workflowv3-run-job.component.ts b/ui/src/app/views/workflowv3/run/workflowv3-run-job.component.ts
index 71eca3f37..811ac0b23 100644
--- a/ui/src/app/views/workflowv3/run/workflowv3-run-job.component.ts
+++ b/ui/src/app/views/workflowv3/run/workflowv3-run-job.component.ts
@@ -46,11 +46,11 @@ export class WorkflowV3RunJobComponent implements OnInit, OnChanges {
         private _store: Store
     ) {
         this.tabs = [<Tab>{
-            translate: 'common_logs',
+            title: 'Logs',
             key: 'logs',
             default: true
         }, <Tab>{
-            translate: 'common_variables',
+            title: 'Variables',
             key: 'variables'
         }];
     }
diff --git a/ui/src/app/views/workflowv3/run/workflowv3-run.component.ts b/ui/src/app/views/workflowv3/run/workflowv3-run.component.ts
index 92b810723..fa96e2b40 100644
--- a/ui/src/app/views/workflowv3/run/workflowv3-run.component.ts
+++ b/ui/src/app/views/workflowv3/run/workflowv3-run.component.ts
@@ -58,16 +58,16 @@ export class WorkflowV3RunComponent implements OnInit, OnDestroy {
         private _toast: ToastService
     ) {
         this.tabs = [<Tab>{
-            translate: 'common_problems',
+            title: 'Problems',
             icon: 'exclamation triangle',
             key: 'problems',
             default: true
         }, <Tab>{
-            translate: 'common_infos',
+            title: 'Infos',
             icon: 'exclamation circle',
             key: 'infos'
         }, <Tab>{
-            translate: 'common_results',
+            title: 'Results',
             icon: 'list',
             key: 'results'
         }];
diff --git a/ui/src/assets/i18n/en.json b/ui/src/assets/i18n/en.json
index 03195bef7..b0004cf97 100644
--- a/ui/src/assets/i18n/en.json
+++ b/ui/src/assets/i18n/en.json
@@ -51,8 +51,6 @@
   "action_usage_not_used": "This action is not used by your pipelines or actions.",
   "action_usage_warning_pipeline": "The pipeline will not be able to run this action because of missing group permission.",
   "action_usage_warning_action": "The parent action will not be able to run this action because they are not in the same group.",
-  "action_custom": "Custom",
-  "action_builtin": "Builtin",
   "action_btn_init_from_job": "Init a new action from job",
   "action_help_add_from": "Current action was initialized with data from \"{{jobName}}\" job of \"{{pipelineName}}\" pipeline in \"{{projectKey}}\" project. The data will not be stored until you save the action.",
   "admin_queue_title": "Current CDS jobs queue",
@@ -178,7 +176,6 @@
   "commit_message": "Message",
   "commit_date": "Date",
   "common_id": "ID",
-  "common_action": "Action",
   "common_search": "Search",
   "common_run": "Run",
   "common_created": "Creation date",
@@ -190,24 +187,16 @@
   "common_settings": "Settings",
   "common_admin": "Admin",
   "common_optional": "Optional",
-  "common_audit": "Audit",
-  "common_advanced": "Advanced",
   "common_all": "All",
   "common_read": "Read",
   "common_write": "Write",
   "common_application": "Application",
   "common_application_title": "Application: ",
-  "common_applications": "Applications",
-  "common_artifact": "Artifact",
-  "common_artifacts": "Artifacts",
   "common_close": "Close",
   "common_cancel": "Cancel",
-  "common_commit": "Commit",
-  "common_commits": "Commits",
   "common_date_end": "End",
   "common_date_start": "Start",
   "common_description": "Description",
-  "common_home": "Overview",
   "common_icon": "Icon",
   "common_disabled_f": "Disabled",
   "common_see_documentation": "See documentation",
@@ -218,11 +207,8 @@
   "common_enabled_f": "Enabled",
   "common_environment": "Environment",
   "common_environment_title": "Environment: ",
-  "common_environments": "Environments",
   "common_filter": "Search...",
   "common_format": "Format",
-  "common_keys": "Keys",
-  "common_history": "History",
   "common_loading": "Loading...",
   "common_loading_project": "Loading project...",
   "common_loading_environments": "Loading environments",
@@ -235,20 +221,15 @@
   "common_labels": "Labels",
   "common_value": "Value",
   "common_no": "No",
-  "common_notifications": "Notifications",
+
   "common_parameters": "Parameters",
-  "common_permissions": "Permissions",
   "common_pipeline_start_title": "Start: ",
   "common_pipeline": "Pipeline",
-  "common_pipelines": "Pipelines",
-  "common_integrations": "Integrations",
   "common_integration": "Integration",
   "common_integration_title": "Integration: ",
   "common_project_title": "Project : ",
   "common_requirements": "Requirements",
   "common_status": "Status",
-  "common_test": "Test",
-  "common_tests": "Tests",
   "common_trigger_by": "Triggered by",
   "common_service": "Service",
   "common_component": "Component",
@@ -256,15 +237,12 @@
   "common_session": "Session",
   "common_consumer": "Consumer",
   "common_type": "Type",
-  "common_variables": "Variables",
   "common_version": "Version",
-  "common_warnings": "Warnings",
   "common_workflow": "Workflow",
   "common_workflow_title": "Workflow: ",
   "common_yes": "Yes",
   "common_clone": "Clone",
   "common_click_here": "Click here",
-  "common_workflows": "Workflows",
   "common_stop": "Stop",
   "common_deprecated": "Deprecated",
   "common_no_description": "No description available",
@@ -272,7 +250,6 @@
   "common_no_application": "There is no application linked",
   "common_no_workflow": "There is no workflow linked",
   "common_no_environment": "There is no environment linked",
-  "common_usage": "Usage",
   "common_orientation": "Orientation",
   "common_or": "OR",
   "common_copy_clipboard": "Copy to clipboad",
@@ -295,7 +272,6 @@
   "common_no_favorites": "You didn't add projects or workflows in your bookmarks yet.",
   "common_favorites_all": "All bookmarks",
   "common_file_too_large": "Your file is too large (max 100Ko)",
-  "common_vulnerabilities": "Vulnerabilities",
   "common_no_data": "No data found",
   "common_required": "Required",
   "common_created_by": "Created By",
@@ -310,17 +286,12 @@
   "common_edit_as_code": "Edit as code",
   "common_edit_ui": "Edit with UI",
   "common_migrate": "Migrate",
-  "common_capabilities": "Capabilities",
   "common_help_no_group": "No group can be found, you will not be able to save current resource.",
   "common_find_help": "Maybe you could find some help",
   "common_here": "here",
   "common_deleted": "Deleted with success",
   "common_click_more_informations": "Click here for more information.",
   "common_informations": "Informations",
-  "common_logs": "Logs",
-  "common_problems": "Problems",
-  "common_infos": "Infos",
-  "common_results": "Results",
   "coverage_branches_title": "Covered branches: ",
   "coverage_functions_title": "Covered functions: ",
   "coverage_lines_title": "Covered lines: ",
@@ -917,8 +888,6 @@
   "workflow_template_deleted": "Workflow template deleted",
   "workflow_template": "Workflow template",
   "workflow_templates": "Workflow templates",
-  "workflow_template_instances": "Instances",
-  "workflow_template_instances_with_nb": "Instances ({{nb}})",
   "workflow_template_help_line_1": "Your workflow was not generated from a template.",
   "workflow_template_help_line_2": "A workflow template allows you to quickly create then update a workflow and its components.",
   "workflow_template_help_line_3": "You can init a new template from current workflow.",
diff --git a/ui/src/ngzorro.dark.less b/ui/src/ngzorro.dark.less
new file mode 100644
index 000000000..2398358a8
--- /dev/null
+++ b/ui/src/ngzorro.dark.less
@@ -0,0 +1,3 @@
+@import "node_modules/ng-zorro-antd/ng-zorro-antd.dark.less";
+
+@component-background: #383736;


Test patch:
diff --git a/engine/api/application/dao_application_integration_test.go b/engine/api/application/dao_application_integration_test.go
index f90bf770e..bcd7fd7b7 100644
--- a/engine/api/application/dao_application_integration_test.go
+++ b/engine/api/application/dao_application_integration_test.go
@@ -75,4 +75,23 @@ func Test_LoadAllDeploymentAllApps(t *testing.T) {
 	require.Len(t, deps[app2.ID], 1)
 	require.Equal(t, "secret1", deps[app1.ID][pp.ID]["token"].Value)
 	require.Equal(t, "secret2", deps[app2.ID][pp.ID]["token"].Value)
+
+	pf.AdditionalDefaultConfig = sdk.IntegrationConfig{
+		"token": sdk.IntegrationConfigValue{
+			Type:  sdk.IntegrationConfigTypePassword,
+			Value: "my-secret-token",
+		},
+		"bar": sdk.IntegrationConfigValue{
+			Type:  sdk.IntegrationConfigTypeString,
+			Value: "foo",
+		},
+	}
+
+	test.NoError(t, integration.UpdateModel(context.TODO(), db, &pf))
+
+	st, err := application.LoadDeploymentStrategies(context.TODO(), db, app1.ID, false)
+	require.NoError(t, err)
+	require.Len(t, st, 1)
+	require.Equal(t, st[pf.Name]["token"].Value, "**********")
+	require.Equal(t, st[pf.Name]["bar"].Value, "foo")
 }
diff --git a/engine/api/application_deployment_test.go b/engine/api/application_deployment_test.go
index 5fb5de93d..739139866 100644
--- a/engine/api/application_deployment_test.go
+++ b/engine/api/application_deployment_test.go
@@ -353,7 +353,7 @@ func Test_postApplicationDeploymentStrategyConfigHandlerAsProvider(t *testing.T)
 	})
 	test.NoError(t, err)
 
-	cfg, err := application.LoadDeploymentStrategies(api.mustDB(), app.ID, true)
+	cfg, err := application.LoadDeploymentStrategies(context.TODO(), api.mustDB(), app.ID, true)
 	test.NoError(t, err)
 
 	var assertCfg = func(key string, cfg sdk.IntegrationConfig, expected sdk.IntegrationConfigValue) {
diff --git a/engine/api/bookmark_test.go b/engine/api/bookmark_test.go
index f97310f72..94456b051 100644
--- a/engine/api/bookmark_test.go
+++ b/engine/api/bookmark_test.go
@@ -82,8 +82,8 @@ func Test_postUserFavoriteHandler(t *testing.T) {
 	assert.True(t, pRes.Favorite, "project favorite flag should be set")
 
 	uri = api.Router.GetRoute(http.MethodGet, api.getWorkflowHandler, map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": wkf.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": wkf.Name,
 	})
 	req = assets.NewJWTAuthentifiedRequest(t, jwt, http.MethodGet, uri, nil)
 	w = httptest.NewRecorder()
diff --git a/engine/api/download/download_test.go b/engine/api/download/download_test.go
index a70f711f4..da35f778a 100644
--- a/engine/api/download/download_test.go
+++ b/engine/api/download/download_test.go
@@ -7,6 +7,9 @@ import (
 )
 
 func TestInit(t *testing.T) {
+	if os.Getenv("CI") != "1" {
+		t.Skip("Skip download test when not running on CI")
+	}
 	tmpDir1, _ := os.MkdirTemp(os.TempDir(), "download1")
 	tmpDir2, _ := os.MkdirTemp(os.TempDir(), "download2")
 	defer os.RemoveAll(tmpDir1)
diff --git a/engine/api/project_integration_test.go b/engine/api/project_integration_test.go
index 059995e89..b538041d5 100644
--- a/engine/api/project_integration_test.go
+++ b/engine/api/project_integration_test.go
@@ -49,7 +49,7 @@ func TestAddUpdateAndDeleteProjectIntegration(t *testing.T) {
 	pp.ProjectID = proj.ID
 
 	vars = map[string]string{}
-	vars[permProjectKey] = proj.Key
+	vars["permProjectKeyWithHooksAllowed"] = proj.Key
 	vars["integrationName"] = "kafkaTest"
 	uri = router.GetRoute("PUT", api.putProjectIntegrationHandler, vars)
 	req = assets.NewAuthentifiedRequest(t, u, pass, "PUT", uri, pp)
@@ -60,7 +60,7 @@ func TestAddUpdateAndDeleteProjectIntegration(t *testing.T) {
 
 	// GET integration
 	vars = map[string]string{}
-	vars[permProjectKey] = proj.Key
+	vars["permProjectKeyWithHooksAllowed"] = proj.Key
 	vars["integrationName"] = pp.Name
 	uri = router.GetRoute("GET", api.getProjectIntegrationHandler, vars)
 
@@ -72,7 +72,7 @@ func TestAddUpdateAndDeleteProjectIntegration(t *testing.T) {
 
 	// DELETE integration
 	vars = map[string]string{}
-	vars[permProjectKey] = proj.Key
+	vars["permProjectKeyWithHooksAllowed"] = proj.Key
 	vars["integrationName"] = pp.Name
 	uri = router.GetRoute("DELETE", api.deleteProjectIntegrationHandler, vars)
 	req = assets.NewAuthentifiedRequest(t, u, pass, "DELETE", uri, nil)
diff --git a/engine/api/user_test.go b/engine/api/user_test.go
index bf2a7e049..b38b09051 100644
--- a/engine/api/user_test.go
+++ b/engine/api/user_test.go
@@ -65,7 +65,6 @@ func Test_putUserHandler(t *testing.T) {
 	assets.DeleteAdmins(t, db)
 
 	initial, jwtInitialRaw := assets.InsertLambdaUser(t, db)
-	initialNewUsername := sdk.RandomString(10)
 	initialNewFullname := sdk.RandomString(10)
 	admin1, jwtAdmin1Raw := assets.InsertAdminUser(t, db)
 	admin2, jwtAdmin2Raw := assets.InsertAdminUser(t, db)
@@ -79,43 +78,32 @@ func Test_putUserHandler(t *testing.T) {
 		ExpectedStatus int
 	}{
 		{
-			Name:           "A lambda user can change its username or fullname",
+			Name:           "A lambda user can't change username but can change fullname",
 			JWT:            jwtInitialRaw,
 			TargetUsername: initial.Username,
 			Data: sdk.AuthentifiedUser{
-				Username: initialNewUsername,
+				Username: sdk.RandomString(10),
 				Fullname: initialNewFullname,
 				Ring:     initial.Ring,
 			},
 			Expected: sdk.AuthentifiedUser{
-				Username: initialNewUsername,
+				Username: initial.Username,
 				Fullname: initialNewFullname,
 				Ring:     initial.Ring,
 			},
 			ExpectedStatus: http.StatusOK,
 		},
-		{
-			Name:           "A lambda user can rename to an existing username",
-			JWT:            jwtInitialRaw,
-			TargetUsername: initialNewUsername,
-			Data: sdk.AuthentifiedUser{
-				Username: admin1.Username,
-				Fullname: initialNewFullname,
-				Ring:     initial.Ring,
-			},
-			ExpectedStatus: http.StatusBadRequest,
-		},
 		{
 			Name:           "A lambda user can't change its ring",
 			JWT:            jwtInitialRaw,
-			TargetUsername: initialNewUsername,
+			TargetUsername: initial.Username,
 			Data: sdk.AuthentifiedUser{
-				Username: initialNewUsername,
+				Username: initial.Username,
 				Fullname: initialNewFullname,
 				Ring:     sdk.UserRingAdmin,
 			},
 			Expected: sdk.AuthentifiedUser{
-				Username: initialNewUsername,
+				Username: initial.Username,
 				Fullname: initialNewFullname,
 				Ring:     initial.Ring,
 			},
@@ -124,14 +112,14 @@ func Test_putUserHandler(t *testing.T) {
 		{
 			Name:           "A admin user can change the ring of a user",
 			JWT:            jwtAdmin1Raw,
-			TargetUsername: initialNewUsername,
+			TargetUsername: initial.Username,
 			Data: sdk.AuthentifiedUser{
-				Username: initialNewUsername,
+				Username: initial.Username,
 				Fullname: initialNewFullname,
 				Ring:     sdk.UserRingMaintainer,
 			},
 			Expected: sdk.AuthentifiedUser{
-				Username: initialNewUsername,
+				Username: initial.Username,
 				Fullname: initialNewFullname,
 				Ring:     sdk.UserRingMaintainer,
 			},
@@ -167,15 +155,15 @@ func Test_putUserHandler(t *testing.T) {
 		{
 			Name:           "A lambda user can't change its organization",
 			JWT:            jwtInitialRaw,
-			TargetUsername: initialNewUsername,
+			TargetUsername: initial.Username,
 			Data: sdk.AuthentifiedUser{
-				Username:     initialNewUsername,
+				Username:     initial.Username,
 				Fullname:     initialNewFullname,
 				Ring:         sdk.UserRingMaintainer,
 				Organization: "my-org",
 			},
 			Expected: sdk.AuthentifiedUser{
-				Username:     initialNewUsername,
+				Username:     initial.Username,
 				Fullname:     initialNewFullname,
 				Ring:         sdk.UserRingMaintainer,
 				Organization: "",
@@ -185,15 +173,15 @@ func Test_putUserHandler(t *testing.T) {
 		{
 			Name:           "A admin user can set user organization",
 			JWT:            jwtAdmin2Raw,
-			TargetUsername: initialNewUsername,
+			TargetUsername: initial.Username,
 			Data: sdk.AuthentifiedUser{
-				Username:     initialNewUsername,
+				Username:     initial.Username,
 				Fullname:     initialNewFullname,
 				Ring:         sdk.UserRingMaintainer,
 				Organization: "my-org",
 			},
 			Expected: sdk.AuthentifiedUser{
-				Username:     initialNewUsername,
+				Username:     initial.Username,
 				Fullname:     initialNewFullname,
 				Ring:         sdk.UserRingMaintainer,
 				Organization: "my-org",
@@ -203,9 +191,9 @@ func Test_putUserHandler(t *testing.T) {
 		{
 			Name:           "A admin user can't change user organization",
 			JWT:            jwtAdmin2Raw,
-			TargetUsername: initialNewUsername,
+			TargetUsername: initial.Username,
 			Data: sdk.AuthentifiedUser{
-				Username:     initialNewUsername,
+				Username:     initial.Username,
 				Fullname:     initialNewFullname,
 				Ring:         sdk.UserRingMaintainer,
 				Organization: "my-other-org",
diff --git a/engine/api/workflow_ascode_rename_test.go b/engine/api/workflow_ascode_rename_test.go
index 97e562db8..d65a755f4 100644
--- a/engine/api/workflow_ascode_rename_test.go
+++ b/engine/api/workflow_ascode_rename_test.go
@@ -319,8 +319,8 @@ version: v1.0`),
 
 	// Then we will trigger a run of the workflow wich should trigger an as-code operation with a renamed workflow
 	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": wk.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": wk.Name,
 	}
 	uri = api.Router.GetRoute("POST", api.postWorkflowRunHandler, vars)
 	require.NotEmpty(t, uri)
diff --git a/engine/api/workflow_ascode_test.go b/engine/api/workflow_ascode_test.go
index 196677a22..fa0706d7b 100644
--- a/engine/api/workflow_ascode_test.go
+++ b/engine/api/workflow_ascode_test.go
@@ -866,8 +866,8 @@ version: v1.0`),
 
 	// Then we will trigger a run of the workflow wich should trigger an as-code operation
 	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": wk.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": wk.Name,
 	}
 	uri = api.Router.GetRoute("POST", api.postWorkflowRunHandler, vars)
 	require.NotEmpty(t, uri)
@@ -1519,8 +1519,8 @@ version: v1.0`),
 
 	// Then we will trigger a run of the workflow wich should trigger an as-code operation
 	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": wk.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": wk.Name,
 	}
 	uri = api.Router.GetRoute("POST", api.postWorkflowRunHandler, vars)
 	require.NotEmpty(t, uri)
diff --git a/engine/api/workflow_ascode_with_hooks_test.go b/engine/api/workflow_ascode_with_hooks_test.go
index e4bcec348..2ee2762b4 100644
--- a/engine/api/workflow_ascode_with_hooks_test.go
+++ b/engine/api/workflow_ascode_with_hooks_test.go
@@ -455,8 +455,8 @@ version: v1.0`),
 
 	// Then we will trigger a run of the workflow wich should trigger an as-code operation
 	uri = api.Router.GetRoute("POST", api.postWorkflowRunHandler, map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": wk.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": wk.Name,
 	})
 	require.NotEmpty(t, uri)
 	req = assets.NewAuthentifiedRequest(t, u, pass, "POST", uri, &sdk.WorkflowRunPostHandlerOption{
@@ -786,8 +786,8 @@ version: v1.0`),
 
 	// Then we will trigger a run of the workflow wich should trigger an as-code operation
 	uri = api.Router.GetRoute("POST", api.postWorkflowRunHandler, map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": wk.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": wk.Name,
 	})
 	require.NotEmpty(t, uri)
 	req = assets.NewAuthentifiedRequest(t, u, jwt, "POST", uri, &sdk.WorkflowRunPostHandlerOption{
@@ -1317,8 +1317,8 @@ version: v1.0`),
 	// ======================================================================================
 	// 2. Then we will trigger a run of the workflow wich should trigger an as-code operation
 	uri = api.Router.GetRoute("POST", api.postWorkflowRunHandler, map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": wk.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": wk.Name,
 	})
 	require.NotEmpty(t, uri)
 	req = assets.NewAuthentifiedRequest(t, u, pass, "POST", uri, &sdk.WorkflowRunPostHandlerOption{
@@ -1381,8 +1381,8 @@ version: v1.0`),
 	// ===========================================================================================================
 	// 3. Then we will trigger a run of the workflow wich should trigger an as-code operation with a hook deletion
 	uri = api.Router.GetRoute("POST", api.postWorkflowRunHandler, map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": wk.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": wk.Name,
 	})
 	require.NotEmpty(t, uri)
 
diff --git a/engine/api/workflow_ascode_with_secrets_test.go b/engine/api/workflow_ascode_with_secrets_test.go
index 077f13fab..e8f6a6d11 100644
--- a/engine/api/workflow_ascode_with_secrets_test.go
+++ b/engine/api/workflow_ascode_with_secrets_test.go
@@ -323,8 +323,8 @@ version: v1.0`),
 
 	// RUN IT
 	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w.Name,
 	}
 	uri := router.GetRoute("POST", api.postWorkflowRunHandler, vars)
 	test.NotEmpty(t, uri)
diff --git a/engine/api/workflow_group_test.go b/engine/api/workflow_group_test.go
index 5828e5135..ca458630c 100644
--- a/engine/api/workflow_group_test.go
+++ b/engine/api/workflow_group_test.go
@@ -731,8 +731,8 @@ func Test_PermissionOnWorkflowInferiorOfProject(t *testing.T) {
 
 	// Try to update workflow
 	vars = map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": wfLoaded.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": wfLoaded.Name,
 	}
 	uri = router.GetRoute("PUT", api.putWorkflowHandler, vars)
 	require.NotEmpty(t, uri)
@@ -751,8 +751,8 @@ func Test_PermissionOnWorkflowInferiorOfProject(t *testing.T) {
 
 	// Try to run workflow
 	vars = map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": wfLoaded.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": wfLoaded.Name,
 	}
 	uri = router.GetRoute("POST", api.postWorkflowRunHandler, vars)
 	require.NotEmpty(t, uri)
@@ -792,8 +792,8 @@ func Test_PermissionOnWorkflowInferiorOfProject(t *testing.T) {
 
 	// try to run the workflow with user in read only
 	vars = map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": wfLoaded.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": wfLoaded.Name,
 	}
 	uri = router.GetRoute("POST", api.postWorkflowRunHandler, vars)
 	require.NotEmpty(t, uri)
@@ -839,8 +839,8 @@ func Test_postWorkflowRunHandler_WithRestrictionOnNode(t *testing.T) {
 		Permission: sdk.PermissionReadExecute,
 	}}
 	uri := router.GetRoute(http.MethodPut, api.putWorkflowHandler, map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w.Name,
 	})
 	require.NotEmpty(t, uri)
 	req := assets.NewJWTAuthentifiedRequest(t, jwtLambda1, http.MethodPut, uri, &w)
@@ -860,8 +860,8 @@ func Test_postWorkflowRunHandler_WithRestrictionOnNode(t *testing.T) {
 
 	// User 1 should be able to run the workflow
 	uri = router.GetRoute(http.MethodPost, api.postWorkflowRunHandler, map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w.Name,
 	})
 	require.NotEmpty(t, uri)
 	req = assets.NewJWTAuthentifiedRequest(t, jwtLambda1, http.MethodPost, uri, &sdk.WorkflowRunPostHandlerOption{
@@ -878,8 +878,8 @@ func Test_postWorkflowRunHandler_WithRestrictionOnNode(t *testing.T) {
 
 	// User 2 should not be able to run the workflow
 	uri = router.GetRoute(http.MethodPost, api.postWorkflowRunHandler, map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w.Name,
 	})
 	require.NotEmpty(t, uri)
 	req = assets.NewJWTAuthentifiedRequest(t, jwtLambda2, http.MethodPost, uri, &sdk.WorkflowRunPostHandlerOption{
diff --git a/engine/api/workflow_queue_test.go b/engine/api/workflow_queue_test.go
index 3354a1898..79a14153c 100644
--- a/engine/api/workflow_queue_test.go
+++ b/engine/api/workflow_queue_test.go
@@ -216,8 +216,8 @@ func testRunWorkflowForProject(t *testing.T, api *API, router *Router, proj *sdk
 
 	//Prepare request
 	uri := router.GetRoute("POST", api.postWorkflowRunHandler, map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w1.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w1.Name,
 	})
 	require.NotEmpty(t, uri)
 
@@ -243,9 +243,9 @@ func testRunWorkflowForProject(t *testing.T, api *API, router *Router, proj *sdk
 	cpt := 0
 	for {
 		varsGet := map[string]string{
-			"key":              proj.Key,
-			"permWorkflowName": w1.Name,
-			"number":           fmt.Sprintf("%d", wr.Number),
+			"key":                      proj.Key,
+			"permWorkflowNameAdvanced": w1.Name,
+			"number":                   fmt.Sprintf("%d", wr.Number),
 		}
 		uriGet := router.GetRoute("GET", api.getWorkflowRunHandler, varsGet)
 		require.NotEmpty(t, uriGet)
@@ -765,9 +765,9 @@ func Test_postWorkflowJobResultHandler(t *testing.T) {
 	require.Equal(t, 204, rec.Code)
 
 	uri = router.GetRoute("GET", api.getWorkflowRunHandler, map[string]string{
-		"key":              ctx.project.Key,
-		"permWorkflowName": ctx.workflow.Name,
-		"number":           fmt.Sprintf("%d", ctx.run.Number),
+		"key":                      ctx.project.Key,
+		"permWorkflowNameAdvanced": ctx.workflow.Name,
+		"number":                   fmt.Sprintf("%d", ctx.run.Number),
 	})
 	req = assets.NewJWTAuthentifiedRequest(t, ctx.userToken, "GET", uri+"?withDetails=true", res)
 
diff --git a/engine/api/workflow_run_test.go b/engine/api/workflow_run_test.go
index ab01b4e83..789aa0d80 100644
--- a/engine/api/workflow_run_test.go
+++ b/engine/api/workflow_run_test.go
@@ -262,8 +262,8 @@ func Test_getWorkflowRunsHandler(t *testing.T) {
 
 	//Prepare request
 	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w1.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w1.Name,
 	}
 	uri := router.GetRoute("GET", api.getWorkflowRunsHandler, vars)
 	test.NotEmpty(t, uri)
@@ -282,8 +282,8 @@ func Test_getWorkflowRunsHandler(t *testing.T) {
 
 	//Prepare request
 	vars = map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w1.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w1.Name,
 	}
 	uri = router.GetRoute("GET", api.getWorkflowRunsHandler, vars)
 	test.NotEmpty(t, uri)
@@ -415,8 +415,8 @@ func Test_getWorkflowRunsHandlerWithFilter(t *testing.T) {
 
 	//Prepare request
 	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w1.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w1.Name,
 	}
 	uri := router.GetRoute("GET", api.getWorkflowRunsHandler, vars)
 	test.NotEmpty(t, uri)
@@ -667,9 +667,9 @@ func Test_getWorkflowRunHandler(t *testing.T) {
 
 	//Prepare request
 	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w1.Name,
-		"number":           "9",
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w1.Name,
+		"number":                   "9",
 	}
 	uri := router.GetRoute("GET", api.getWorkflowRunHandler, vars)
 	test.NotEmpty(t, uri)
@@ -1073,8 +1073,8 @@ func Test_postWorkflowRunHandler(t *testing.T) {
 
 	//Prepare request
 	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w1.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w1.Name,
 	}
 	uri := router.GetRoute("POST", api.postWorkflowRunHandler, vars)
 	test.NotEmpty(t, uri)
@@ -1413,8 +1413,8 @@ func Test_postWorkflowRunAsyncFailedHandler(t *testing.T) {
 
 	// Prepare request
 	uri := router.GetRoute("POST", api.postWorkflowRunHandler, map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w1.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w1.Name,
 	})
 	req := assets.NewAuthentifiedRequest(t, u, pass, "POST", uri, &sdk.WorkflowRunPostHandlerOption{})
 	rec := httptest.NewRecorder()
@@ -1429,9 +1429,9 @@ func Test_postWorkflowRunAsyncFailedHandler(t *testing.T) {
 	for {
 		t.Logf("Attempt getWorkflowRunHandler #%d", cpt)
 		varsGet := map[string]string{
-			"key":              proj.Key,
-			"permWorkflowName": w1.Name,
-			"number":           "1",
+			"key":                      proj.Key,
+			"permWorkflowNameAdvanced": w1.Name,
+			"number":                   "1",
 		}
 		uriGet := router.GetRoute("GET", api.getWorkflowRunHandler, varsGet)
 		reqGet := assets.NewAuthentifiedRequest(t, u, pass, "GET", uriGet, nil)
@@ -1557,8 +1557,8 @@ func Test_postWorkflowRunHandlerWithoutRightOnEnvironment(t *testing.T) {
 
 	//Prepare request
 	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w1.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w1.Name,
 	}
 	uri := router.GetRoute("POST", api.postWorkflowRunHandler, vars)
 	test.NotEmpty(t, uri)
@@ -1717,8 +1717,8 @@ func Test_postWorkflowRunHandlerWithoutRightConditionsOnHook(t *testing.T) {
 
 	//Prepare request
 	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w1.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w1.Name,
 	}
 	uri := router.GetRoute("POST", api.postWorkflowRunHandler, vars)
 	test.NotEmpty(t, uri)
@@ -1860,8 +1860,8 @@ func Test_postWorkflowRunHandlerHookWithMutex(t *testing.T) {
 
 	//Prepare request
 	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w1.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w1.Name,
 	}
 	uri := router.GetRoute("POST", api.postWorkflowRunHandler, vars)
 	test.NotEmpty(t, uri)
@@ -1946,8 +1946,8 @@ func Test_postWorkflowRunHandlerMutexRelease(t *testing.T) {
 
 	// Run workflow 1
 	uri := router.GetRoute("POST", api.postWorkflowRunHandler, map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": wkf.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": wkf.Name,
 	})
 	require.NotEmpty(t, uri)
 	req := assets.NewAuthentifiedRequest(t, u, jwt, "POST", uri, sdk.WorkflowRunPostHandlerOption{})
@@ -1969,9 +1969,9 @@ func Test_postWorkflowRunHandlerMutexRelease(t *testing.T) {
 		try++
 		t.Logf("Attempt #%d on getWorkflowRunHandler for run 1", try)
 		uri := router.GetRoute("GET", api.getWorkflowRunHandler, map[string]string{
-			"key":              proj.Key,
-			"permWorkflowName": wkf.Name,
-			"number":           "1",
+			"key":                      proj.Key,
+			"permWorkflowNameAdvanced": wkf.Name,
+			"number":                   "1",
 		})
 		req := assets.NewAuthentifiedRequest(t, u, jwt, "GET", uri, nil)
 		rec := httptest.NewRecorder()
@@ -1992,8 +1992,8 @@ func Test_postWorkflowRunHandlerMutexRelease(t *testing.T) {
 
 	// Run workflow 2
 	uri = router.GetRoute("POST", api.postWorkflowRunHandler, map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": wkf.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": wkf.Name,
 	})
 	require.NotEmpty(t, uri)
 	req = assets.NewAuthentifiedRequest(t, u, jwt, "POST", uri, sdk.WorkflowRunPostHandlerOption{})
@@ -2015,9 +2015,9 @@ func Test_postWorkflowRunHandlerMutexRelease(t *testing.T) {
 		try++
 		t.Logf("Attempt #%d on getWorkflowRunHandler for run 2", try)
 		uri := router.GetRoute("GET", api.getWorkflowRunHandler, map[string]string{
-			"key":              proj.Key,
-			"permWorkflowName": wkf.Name,
-			"number":           "2",
+			"key":                      proj.Key,
+			"permWorkflowNameAdvanced": wkf.Name,
+			"number":                   "2",
 		})
 		req := assets.NewAuthentifiedRequest(t, u, jwt, "GET", uri, nil)
 		rec := httptest.NewRecorder()
@@ -2041,8 +2041,8 @@ func Test_postWorkflowRunHandlerMutexRelease(t *testing.T) {
 
 	// Run workflow 3
 	uri = router.GetRoute("POST", api.postWorkflowRunHandler, map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": wkf.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": wkf.Name,
 	})
 	require.NotEmpty(t, uri)
 	req = assets.NewAuthentifiedRequest(t, u, jwt, "POST", uri, sdk.WorkflowRunPostHandlerOption{})
@@ -2064,9 +2064,9 @@ func Test_postWorkflowRunHandlerMutexRelease(t *testing.T) {
 		try++
 		t.Logf("Attempt #%d on getWorkflowRunHandler for run 3", try)
 		uri := router.GetRoute("GET", api.getWorkflowRunHandler, map[string]string{
-			"key":              proj.Key,
-			"permWorkflowName": wkf.Name,
-			"number":           "3",
+			"key":                      proj.Key,
+			"permWorkflowNameAdvanced": wkf.Name,
+			"number":                   "3",
 		})
 		req := assets.NewAuthentifiedRequest(t, u, jwt, "GET", uri, nil)
 		rec := httptest.NewRecorder()
@@ -2110,9 +2110,9 @@ func Test_postWorkflowRunHandlerMutexRelease(t *testing.T) {
 		try++
 		t.Logf("Attempt #%d on getWorkflowRunHandler for run 1", try)
 		uri := router.GetRoute("GET", api.getWorkflowRunHandler, map[string]string{
-			"key":              proj.Key,
-			"permWorkflowName": wkf.Name,
-			"number":           "1",
+			"key":                      proj.Key,
+			"permWorkflowNameAdvanced": wkf.Name,
+			"number":                   "1",
 		})
 		req := assets.NewAuthentifiedRequest(t, u, jwt, "GET", uri, nil)
 		rec := httptest.NewRecorder()
@@ -2142,9 +2142,9 @@ func Test_postWorkflowRunHandlerMutexRelease(t *testing.T) {
 		try++
 		t.Logf("Attempt #%d on getWorkflowRunHandler for run 2", try)
 		uri := router.GetRoute("GET", api.getWorkflowRunHandler, map[string]string{
-			"key":              proj.Key,
-			"permWorkflowName": wkf.Name,
-			"number":           "2",
+			"key":                      proj.Key,
+			"permWorkflowNameAdvanced": wkf.Name,
+			"number":                   "2",
 		})
 		req := assets.NewAuthentifiedRequest(t, u, jwt, "GET", uri, nil)
 		rec := httptest.NewRecorder()
@@ -2174,9 +2174,9 @@ func Test_postWorkflowRunHandlerMutexRelease(t *testing.T) {
 		try++
 		t.Logf("Attempt #%d on getWorkflowRunHandler for run 3", try)
 		uri := router.GetRoute("GET", api.getWorkflowRunHandler, map[string]string{
-			"key":              proj.Key,
-			"permWorkflowName": wkf.Name,
-			"number":           "3",
+			"key":                      proj.Key,
+			"permWorkflowNameAdvanced": wkf.Name,
+			"number":                   "3",
 		})
 		req := assets.NewAuthentifiedRequest(t, u, jwt, "GET", uri, nil)
 		rec := httptest.NewRecorder()
@@ -2317,8 +2317,8 @@ func Test_postWorkflowRunHandlerHook(t *testing.T) {
 
 	//Prepare request
 	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w1.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w1.Name,
 	}
 	uri := router.GetRoute("POST", api.postWorkflowRunHandler, vars)
 	test.NotEmpty(t, uri)
@@ -2426,8 +2426,8 @@ func Test_postWorkflowRunHandler_Forbidden(t *testing.T) {
 
 	//Prepare request
 	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w.Name,
 	}
 	uri := router.GetRoute("POST", api.postWorkflowRunHandler, vars)
 	test.NotEmpty(t, uri)
@@ -2489,8 +2489,8 @@ func Test_postWorkflowRunHandler_ConditionNotOK(t *testing.T) {
 
 	//Prepare request
 	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w.Name,
 	}
 	uri := router.GetRoute("POST", api.postWorkflowRunHandler, vars)
 	test.NotEmpty(t, uri)
@@ -2586,8 +2586,8 @@ func Test_postWorkflowRunHandler_BadPayload(t *testing.T) {
 
 	//Prepare request
 	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w.Name,
 	}
 	uri := router.GetRoute("POST", api.postWorkflowRunHandler, vars)
 	test.NotEmpty(t, uri)
@@ -2724,152 +2724,6 @@ func initGetWorkflowNodeRunJobTest(t *testing.T, api *API, db gorpmapper.SqlExec
 	return u, pass, proj, w1, lastRun, jobRun
 }
 
-func Test_deleteWorkflowRunsBranchHandler(t *testing.T) {
-	api, db, router := newTestAPI(t)
-
-	u, pass := assets.InsertAdminUser(t, db)
-	key := sdk.RandomString(10)
-	proj := assets.InsertTestProject(t, db, api.Cache, key, key)
-
-	//First pipeline
-	pip := sdk.Pipeline{
-		ProjectID:  proj.ID,
-		ProjectKey: proj.Key,
-		Name:       "pip1",
-	}
-	require.NoError(t, pipeline.InsertPipeline(api.mustDB(), &pip))
-
-	s := sdk.NewStage("stage 1")
-	s.Enabled = true
-	s.PipelineID = pip.ID
-	require.NoError(t, pipeline.InsertStage(api.mustDB(), s))
-	j := &sdk.Job{
-		Enabled: true,
-		Action: sdk.Action{
-			Enabled: true,
-		},
-	}
-	require.NoError(t, pipeline.InsertJob(api.mustDB(), j, s.ID, &pip))
-	s.Jobs = append(s.Jobs, *j)
-
-	pip.Stages = append(pip.Stages, *s)
-
-	//Second pipeline
-	pip2 := sdk.Pipeline{
-		ProjectID:  proj.ID,
-		ProjectKey: proj.Key,
-		Name:       "pip2",
-	}
-	require.NoError(t, pipeline.InsertPipeline(api.mustDB(), &pip2))
-	s = sdk.NewStage("stage 1")
-	s.Enabled = true
-	s.PipelineID = pip2.ID
-	require.NoError(t, pipeline.InsertStage(api.mustDB(), s))
-	j = &sdk.Job{
-		Enabled: true,
-		Action: sdk.Action{
-			Enabled: true,
-		},
-	}
-	require.NoError(t, pipeline.InsertJob(api.mustDB(), j, s.ID, &pip2))
-	s.Jobs = append(s.Jobs, *j)
-
-	w := sdk.Workflow{
-		Name:       "test_1",
-		ProjectID:  proj.ID,
-		ProjectKey: proj.Key,
-		WorkflowData: sdk.WorkflowData{
-			Node: sdk.Node{
-				Name: "root",
-				Type: sdk.NodeTypePipeline,
-				Context: &sdk.NodeContext{
-					PipelineID: pip.ID,
-				},
-				Triggers: []sdk.NodeTrigger{
-					{
-						ChildNode: sdk.Node{
-							Name: "child",
-							Type: sdk.NodeTypePipeline,
-							Context: &sdk.NodeContext{
-								PipelineID: pip.ID,
-							},
-						},
-					},
-				},
-			},
-		},
-	}
-
-	consumer, _ := authentication.LoadConsumerByTypeAndUserID(context.TODO(), db, sdk.ConsumerLocal, u.ID, authentication.LoadConsumerOptions.WithAuthentifiedUser)
-
-	proj2, errP := project.Load(context.TODO(), api.mustDB(), proj.Key, project.LoadOptions.WithPipelines, project.LoadOptions.WithGroups, project.LoadOptions.WithIntegrations)
-	require.NoError(t, errP)
-
-	require.NoError(t, workflow.Insert(context.TODO(), db, api.Cache, *proj2, &w))
-	w1, err := workflow.Load(context.TODO(), api.mustDB(), api.Cache, *proj, "test_1", workflow.LoadOptions{})
-	require.NoError(t, err)
-
-	wr, err := workflow.CreateRun(api.mustDB(), w1, sdk.WorkflowRunPostHandlerOption{AuthConsumerID: consumer.ID})
-	assert.NoError(t, err)
-	wr.Workflow = *w1
-	wr.Tag("git.branch", "master")
-	assert.NoError(t, workflow.UpdateWorkflowRun(context.TODO(), api.mustDB(), wr))
-	_, err = workflow.StartWorkflowRun(context.TODO(), db, api.Cache, *proj, wr, &sdk.WorkflowRunPostHandlerOption{
-		Manual: &sdk.WorkflowNodeRunManual{
-			Username: u.GetUsername(),
-			Payload:  `{"git.branch": "master"}`,
-		},
-	}, *consumer, nil)
-	require.NoError(t, err)
-
-	mockHookService, _ := assets.InsertService(t, db, "Test_deleteWorkflowRunsBranchHandler", sdk.TypeHooks, sdk.AuthConsumerScopeRun)
-	defer func() {
-		_ = services.Delete(db, mockHookService) // nolint
-	}()
-
-	serviceConsumer, err := authentication.LoadConsumerByID(context.TODO(), db, *mockHookService.ConsumerID)
-	require.NoError(t, err)
-
-	session, err := authentication.NewSession(context.TODO(), db, serviceConsumer, 5*time.Minute)
-	require.NoError(t, err)
-
-	jwt, err := authentication.NewSessionJWT(session, "")
-	require.NoError(t, err)
-
-	//Prepare request
-	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w1.Name,
-		"branch":           "master",
-	}
-	uri := router.GetRoute("DELETE", api.deleteWorkflowRunsBranchHandler, vars)
-	require.NotEmpty(t, uri)
-	req := assets.NewAuthentifiedRequest(t, nil, jwt, "DELETE", uri, vars)
-
-	//Do the request
-	rec := httptest.NewRecorder()
-	router.Mux.ServeHTTP(rec, req)
-	require.Equal(t, 200, rec.Code)
-
-	//Prepare request
-	vars = map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w1.Name,
-	}
-	uri = router.GetRoute("GET", api.getWorkflowRunsHandler, vars)
-	require.NotEmpty(t, uri)
-	req = assets.NewAuthentifiedRequest(t, u, pass, "GET", uri, vars)
-
-	//Do the request
-	rec = httptest.NewRecorder()
-	router.Mux.ServeHTTP(rec, req)
-	require.Equal(t, 200, rec.Code)
-
-	var wfRuns []sdk.WorkflowRun
-	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &wfRuns))
-	require.Equal(t, 0, len(wfRuns))
-}
-
 func Test_deleteWorkflowRunHandler(t *testing.T) {
 	api, db, router := newTestAPI(t)
 
@@ -2958,9 +2812,9 @@ func Test_deleteWorkflowRunHandler(t *testing.T) {
 	assert.NoError(t, err)
 	//Prepare request
 	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w1.Name,
-		"number":           fmt.Sprintf("%d", wr.Number),
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w1.Name,
+		"number":                   fmt.Sprintf("%d", wr.Number),
 	}
 	uri := router.GetRoute("DELETE", api.deleteWorkflowRunHandler, vars)
 	test.NotEmpty(t, uri)
@@ -2973,9 +2827,9 @@ func Test_deleteWorkflowRunHandler(t *testing.T) {
 
 	//Prepare request
 	vars = map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w1.Name,
-		"number":           fmt.Sprintf("%d", wr.Number),
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w1.Name,
+		"number":                   fmt.Sprintf("%d", wr.Number),
 	}
 	uri = router.GetRoute("GET", api.getWorkflowRunHandler, vars)
 	test.NotEmpty(t, uri)
@@ -2997,8 +2851,8 @@ func Test_postWorkflowRunHandlerBadResyncOptions(t *testing.T) {
 
 	//Prepare request
 	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w.Name,
 	}
 	uri := router.GetRoute("POST", api.postWorkflowRunHandler, vars)
 	test.NotEmpty(t, uri)
@@ -3079,8 +2933,8 @@ func Test_postWorkflowRunHandlerRestartOnlyFailed(t *testing.T) {
 
 	//Prepare request
 	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w1.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w1.Name,
 	}
 	uri := router.GetRoute("POST", api.postWorkflowRunHandler, vars)
 	test.NotEmpty(t, uri)
@@ -3236,8 +3090,8 @@ func Test_CheckRegionDuringInitWorkflow(t *testing.T) {
 
 	//Prepare request
 	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w1.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w1.Name,
 	}
 	uri := router.GetRoute("POST", api.postWorkflowRunHandler, vars)
 	test.NotEmpty(t, uri)
@@ -3343,8 +3197,8 @@ func Test_postWorkflowRunHandlerRestartResync(t *testing.T) {
 
 	//Prepare request
 	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": w1.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": w1.Name,
 	}
 	uri := router.GetRoute("POST", api.postWorkflowRunHandler, vars)
 	test.NotEmpty(t, uri)
diff --git a/engine/api/workflow_test.go b/engine/api/workflow_test.go
index 9c86df159..b1fb8eae5 100644
--- a/engine/api/workflow_test.go
+++ b/engine/api/workflow_test.go
@@ -273,8 +273,8 @@ func Test_getWorkflowHandler(t *testing.T) {
 	proj := assets.InsertTestProject(t, db, api.Cache, key, key)
 	//Prepare request
 	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": "workflow1",
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": "workflow1",
 	}
 	uri := router.GetRoute("GET", api.getWorkflowHandler, vars)
 	test.NotEmpty(t, uri)
@@ -322,8 +322,8 @@ func Test_getWorkflowHandler_CheckPermission(t *testing.T) {
 	require.NoError(t, workflow.Insert(context.TODO(), db, api.Cache, *proj, &wf))
 
 	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": "workflow1",
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": "workflow1",
 	}
 	uri := api.Router.GetRoute("GET", api.getWorkflowHandler, vars)
 	test.NotEmpty(t, uri)
@@ -456,8 +456,8 @@ func Test_getWorkflowHandler_withUsage(t *testing.T) {
 	proj := assets.InsertTestProject(t, db, api.Cache, key, key)
 	//Prepare request
 	vars := map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": "workflow1",
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": "workflow1",
 	}
 	uri := router.GetRoute("GET", api.getWorkflowHandler, vars)
 	test.NotEmpty(t, uri)
@@ -804,8 +804,8 @@ func Test_putWorkflowHandler(t *testing.T) {
 
 	//Prepare request
 	vars = map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": "Name",
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": "Name",
 	}
 	uri = router.GetRoute("PUT", api.putWorkflowHandler, vars)
 	test.NotEmpty(t, uri)
@@ -925,8 +925,8 @@ func Test_deleteWorkflowEventIntegrationHandler(t *testing.T) {
 
 	//Prepare request
 	vars = map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": "Name",
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": "Name",
 	}
 	uri = router.GetRoute("PUT", api.putWorkflowHandler, vars)
 	test.NotEmpty(t, uri)
@@ -994,7 +994,11 @@ func Test_deleteWorkflowEventIntegrationHandler(t *testing.T) {
 	assert.NotNil(t, workflow1.Integrations)
 	assert.Equal(t, len(workflow1.Integrations), 1)
 
-	vars["integrationID"] = fmt.Sprintf("%d", projInt.ID)
+	vars = map[string]string{
+		"key":              proj.Key,
+		"permWorkflowName": "Name",
+		"integrationID":    fmt.Sprintf("%d", projInt.ID),
+	}
 	uri = router.GetRoute("DELETE", api.deleteWorkflowEventsIntegrationHandler, vars)
 	req = assets.NewAuthentifiedRequest(t, u, pass, "DELETE", uri, nil)
 
@@ -1121,8 +1125,8 @@ func Test_postWorkflowRollbackHandler(t *testing.T) {
 
 	//Prepare request
 	vars = map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": "Name",
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": "Name",
 	}
 	uri = router.GetRoute("PUT", api.putWorkflowHandler, vars)
 	test.NotEmpty(t, uri)
@@ -1345,8 +1349,8 @@ func Test_deleteWorkflowHandler(t *testing.T) {
 	test.NoError(t, json.Unmarshal(w.Body.Bytes(), &wkf))
 
 	vars = map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": "Name",
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": "Name",
 	}
 	uri = router.GetRoute("DELETE", api.deleteWorkflowHandler, vars)
 	test.NotEmpty(t, uri)
@@ -1617,8 +1621,8 @@ func Test_putWorkflowShouldNotCallHOOKSIfHookDoesNotChange(t *testing.T) {
 
 	// Load the workflow
 	uri = router.GetRoute(http.MethodGet, api.getWorkflowHandler, map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": wf.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": wf.Name,
 	})
 	require.NotEmpty(t, uri)
 	req = assets.NewJWTAuthentifiedRequest(t, jwtLambda, http.MethodGet, uri, nil)
@@ -1631,8 +1635,8 @@ func Test_putWorkflowShouldNotCallHOOKSIfHookDoesNotChange(t *testing.T) {
 	// Then call the PUT handler, it should not trigger /task/bulk on hooks service
 	// Update the workflow
 	uri = router.GetRoute(http.MethodPut, api.putWorkflowHandler, map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": wf.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": wf.Name,
 	})
 	require.NotEmpty(t, uri)
 	req = assets.NewJWTAuthentifiedRequest(t, jwtLambda, http.MethodPut, uri, &wf)
@@ -1725,8 +1729,8 @@ func Test_putWorkflowWithDuplicateHooksShouldRaiseAnError(t *testing.T) {
 
 	// Load the workflow
 	uri = router.GetRoute("GET", api.getWorkflowHandler, map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": wf.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": wf.Name,
 	})
 	require.NotEmpty(t, uri)
 	req = assets.NewAuthentifiedRequest(t, u, pass, "GET", uri, nil)
@@ -1750,8 +1754,8 @@ func Test_putWorkflowWithDuplicateHooksShouldRaiseAnError(t *testing.T) {
 
 	// Update the workflow
 	uri = router.GetRoute("PUT", api.putWorkflowHandler, map[string]string{
-		"key":              proj.Key,
-		"permWorkflowName": wf.Name,
+		"key":                      proj.Key,
+		"permWorkflowNameAdvanced": wf.Name,
 	})
 	require.NotEmpty(t, uri)
 	req = assets.NewAuthentifiedRequest(t, u, pass, "PUT", uri, &wf)
diff --git a/engine/vcs/bitbucketserver/bitbucketserver_test.go b/engine/vcs/bitbucketserver/bitbucketserver_test.go
index 8a6a51eef..82f6835ae 100644
--- a/engine/vcs/bitbucketserver/bitbucketserver_test.go
+++ b/engine/vcs/bitbucketserver/bitbucketserver_test.go
@@ -2,6 +2,7 @@ package bitbucketserver
 
 import (
 	"context"
+	"os"
 	"testing"
 
 	"github.com/ovh/cds/engine/cache"
@@ -13,7 +14,6 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-// TestNew needs githubClientID and githubClientSecret
 func TestNewClient(t *testing.T) {
 	ghConsummer := getNewConsumer(t)
 	assert.NotNil(t, ghConsummer)
@@ -56,7 +56,7 @@ func getAuthorizedClient(t *testing.T) sdk.VCSAuthorizedClient {
 	redisPassword := cfg["redisPassword"]
 
 	if consumerKey == "" && privateKey == "" {
-		t.Logf("Unable to read github configuration. Skipping this tests.")
+		t.Logf("Unable to read bitbucket configuration. Skipping this tests.")
 		t.SkipNow()
 	}
 
@@ -71,7 +71,7 @@ func getAuthorizedClient(t *testing.T) sdk.VCSAuthorizedClient {
 	return cli
 }
 
-func TestClientAuthorizeToken(t *testing.T) {
+func TestClientAuthorizeRedirect(t *testing.T) {
 	consumer := getNewConsumer(t)
 	token, url, err := consumer.AuthorizeRedirect(context.Background())
 	t.Logf("token: %s", token)
@@ -85,6 +85,24 @@ func TestClientAuthorizeToken(t *testing.T) {
 	require.NoError(t, err)
 }
 
+func TestClientAuthorizeToken(t *testing.T) {
+	token := os.Getenv("TOKEN")
+	verifier := os.Getenv("VERIFIER")
+
+	if token == "" || verifier == "" {
+		t.SkipNow()
+	}
+
+	consumer := getNewConsumer(t)
+	accesstoken, accesstokenSecret, err := consumer.AuthorizeToken(context.Background(), token, verifier)
+	require.NoError(t, err)
+	assert.NotEmpty(t, accesstoken)
+	assert.NotEmpty(t, accesstokenSecret)
+
+	t.Logf("accesstoken: %s", accesstoken)
+	t.Logf("accesstokenSecret: %s", accesstokenSecret)
+}
+
 func TestAuthorizedClient(t *testing.T) {
 	bitbucketClient := getAuthorizedClient(t)
 	assert.NotNil(t, bitbucketClient)
diff --git a/sdk/common_test.go b/sdk/common_test.go
index 55d672c74..1fbe69b51 100644
--- a/sdk/common_test.go
+++ b/sdk/common_test.go
@@ -46,3 +46,83 @@ func TestPathIsAbs(t *testing.T) {
 	assert.True(t, PathIsAbs(`/tmp`))
 	assert.False(t, PathIsAbs(`tmp`))
 }
+
+func TestCleanPath(t *testing.T) {
+	type args struct {
+		path string
+	}
+	tests := []struct {
+		name string
+		args args
+		want string
+	}{
+		{
+			name: "test1",
+			args: args{
+				path: "../../foobar",
+			},
+			want: "foobar",
+		},
+		{
+			name: "test1",
+			args: args{
+				path: "./foobar",
+			},
+			want: "foobar",
+		},
+		{
+			name: "test2",
+			args: args{
+				path: "/foo/bar/biz",
+			},
+			want: "/foo/bar/biz",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := CleanPath(tt.args.path); got != tt.want {
+				t.Errorf("CleanPath() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestNoPath(t *testing.T) {
+	type args struct {
+		path string
+	}
+	tests := []struct {
+		name string
+		args args
+		want string
+	}{
+		{
+			name: "test1",
+			args: args{
+				path: "/foo/bar/biz",
+			},
+			want: "biz",
+		},
+		{
+			name: "test2",
+			args: args{
+				path: "",
+			},
+			want: "",
+		},
+		{
+			name: "test3",
+			args: args{
+				path: ".",
+			},
+			want: ".",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := NoPath(tt.args.path); got != tt.want {
+				t.Errorf("NoPath() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/tests/03_clictl_project_vcs.yml b/tests/03_clictl_project_vcs.yml
new file mode 100644
index 000000000..740a994a0
--- /dev/null
+++ b/tests/03_clictl_project_vcs.yml
@@ -0,0 +1,27 @@
+name: Project VCS Command TestSuite
+testcases:
+- name: prepare test
+  steps:
+  - script: {{.cdsctl}} -f {{.cdsctl.config}} project remove --force ITCLIPRJVCS
+  - script: {{.cdsctl}} -f {{.cdsctl.config}} group remove ITCLIPRJVCS --force
+  - script: {{.cdsctl}} -f {{.cdsctl.config}} group add ITCLIPRJVCS
+  - script: {{.cdsctl}} -f {{.cdsctl.config}} project create ITCLIPRJVCS "Test Project" ITCLIPRJVCS
+
+- name: import / export vcs server
+  steps:
+  - script: {{.cdsctl}} -f {{.cdsctl.config}} experimental project vcs import ITCLIPRJVCS ./fixtures/my_vcs_project.yml
+  - script: {{.cdsctl}} -f {{.cdsctl.config}} experimental project vcs list ITCLIPRJVCS --format json
+    assertions:
+    - result.code ShouldEqual 0
+    - result.systemoutjson.systemoutjson0.name ShouldEqual "my_vcs_server"
+  - script: {{.cdsctl}} -f {{.cdsctl.config}} experimental project vcs export ITCLIPRJVCS my_vcs_server > my_vcs_project_exported.yml
+  - script: grep 'my_vcs_server' my_vcs_project_exported.yml
+
+- name: delete vcs project
+  steps:
+  - script: rm my_vcs_project_exported.yml
+  - script: {{.cdsctl}} -f {{.cdsctl.config}} experimental project vcs delete ITCLIPRJVCS my_vcs_server --force
+  - script: {{.cdsctl}} -f {{.cdsctl.config}} experimental project vcs list ITCLIPRJVCS --format json
+    assertions:
+    - result.code ShouldEqual 0
+    - result.systemoutjson.systemoutjson0.name ShouldNotEqual "my_vcs_server"
diff --git a/tests/05_sc_workflow_event_kafka.yml b/tests/05_sc_workflow_event_kafka.yml
index 71712b72a..7d965beb1 100644
--- a/tests/05_sc_workflow_event_kafka.yml
+++ b/tests/05_sc_workflow_event_kafka.yml
@@ -30,6 +30,7 @@ testcases:
   - script: kafkacat -b localhost:9092 -t test.hook -T -P -l ./fixtures/ITSCWRKFLW18/input-kafka.json
     retry: 10
     delay: 10
+    timeout: 100
 
 - name: check event in topic test.eventsproject
   steps:
@@ -37,10 +38,11 @@ testcases:
     info: "kafka message is: {{.result.systemout}}"
     assertions:
     - result.code ShouldEqual 0
+    - 'result.systemout MustNotContainSubstring "Leader not available"'
     - "result.systemoutjson.id ShouldNotBeEmpty"
     retry: 10
     delay: 10
-    timeout: 300
+    timeout: 100
 
 - name: check event in topic test.eventspublic
   steps:
diff --git a/tests/fixtures/04SCWorkflowRunSimplePlugin/go.sum b/tests/fixtures/04SCWorkflowRunSimplePlugin/go.sum
index 13f877387..2f2f4b6ec 100644
--- a/tests/fixtures/04SCWorkflowRunSimplePlugin/go.sum
+++ b/tests/fixtures/04SCWorkflowRunSimplePlugin/go.sum
@@ -111,7 +111,6 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
 github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
-github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
 github.com/buger/goterm v0.0.0-20170918171949-d443b9114f9c/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U=
 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
diff --git a/tests/fixtures/my_vcs_project.yml b/tests/fixtures/my_vcs_project.yml
new file mode 100644
index 000000000..c0a15e2b9
--- /dev/null
+++ b/tests/fixtures/my_vcs_project.yml
@@ -0,0 +1,8 @@
+version: v1.0
+name: my_vcs_server
+type: bitbucketserver
+description: "it's the test vcs server on project"
+url: "http://my-vcs-server.localhost"
+auth:
+    user: the-username
+    password: the-password
\ No newline at end of file