Repository: ovh/cds
Pull request #5577 (2020-12-03 15:04:02)

fix(api): do not erase worker private key
1. Description
1. Related issues
1. About tests
1. Mentions

@ovh/cds


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

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


Patch:
diff --git a/cli/cdsctl/application.go b/cli/cdsctl/application.go
index 4f7ae572e..0bc053698 100644
--- a/cli/cdsctl/application.go
+++ b/cli/cdsctl/application.go
@@ -121,6 +121,12 @@ func applicationDeleteRun(v cli.Values) error {
 var applicationImportCmd = cli.Command{
 	Name:  "import",
 	Short: "Import an application with a local filepath or an URL",
+	Long: `PATH: Path or URL of application to import
+
+Without --force, CDS won't update an existing application.
+With --force, CDS will allow you to update an existing application. If this application is managed 'as-code', CDS will
+override it. This application will be detached from the repository, until it is re-imported again following a commit on the repo.
+`,
 	Ctx: []cli.Arg{
 		{Name: _ProjectKey},
 	},
diff --git a/cli/cdsctl/environment.go b/cli/cdsctl/environment.go
index f45af76c9..012cc6a21 100644
--- a/cli/cdsctl/environment.go
+++ b/cli/cdsctl/environment.go
@@ -90,6 +90,12 @@ func environmentDeleteRun(v cli.Values) error {
 var environmentImportCmd = cli.Command{
 	Name:  "import",
 	Short: "Import an environment with local filepath or URL",
+	Long: `PATH: Path or URL of environment to import
+
+Without --force, CDS won't update an existing environment.
+With --force, CDS will allow you to update an existing environment. If this environment is managed 'as-code', CDS will
+override it. This environment will be detached from the repository, until it is re-imported again following a commit on the repo.
+`,
 	Ctx: []cli.Arg{
 		{Name: _ProjectKey},
 	},
diff --git a/cli/cdsctl/workflow_import.go b/cli/cdsctl/workflow_import.go
index e91e5fef5..52a321be6 100644
--- a/cli/cdsctl/workflow_import.go
+++ b/cli/cdsctl/workflow_import.go
@@ -16,6 +16,10 @@ In case you want to import just your workflow. Instead of use a local file you c
 
 If you want to update also dependencies likes pipelines, applications or environments at same time you have to use workflow push instead workflow import.
 
+Without --force, CDS won't update an existing workflow.
+With --force, CDS will allow you to update an existing workflow. If this workflow is managed 'as-code', CDS will
+override it. This workflow and its components (applications, pipelines, environments) will be detached from the 
+repository, until it is re-imported again following a commit on the repo.
 	`,
 	Ctx: []cli.Arg{
 		{Name: _ProjectKey},
diff --git a/docker-compose.yml b/docker-compose.yml
index e1d552ae1..27b6f2ebf 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,7 +6,7 @@ version: '3'
 
 services:
   cds-db:
-    image: postgres:9.6.2
+    image: postgres:9.6
     environment:
       POSTGRES_PASSWORD: cds
       POSTGRES_USER: cds
diff --git a/docs/content/hosting/ready-to-run/docker-compose/full-example.md b/docs/content/hosting/ready-to-run/docker-compose/full-example.md
index c2e331774..316eb0664 100644
--- a/docs/content/hosting/ready-to-run/docker-compose/full-example.md
+++ b/docs/content/hosting/ready-to-run/docker-compose/full-example.md
@@ -274,7 +274,7 @@ fe2fcbee96aa        ovhcom/cds-engine:latest                              "sh -c
 f2eb7b8c4329        ovhcom/cds-engine:latest                              "sh -c '/app/cds-eng…"   35 seconds ago      Up 33 seconds (healthy)                              debian_cds-elasticsearch_1
 22dc66a1b2a2        ovhcom/cds-engine:latest                              "sh -c '/app/cds-eng…"   35 seconds ago      Up 33 seconds (healthy)                              debian_cds-hatchery-swarm_1
 958ab1703f16        ovhcom/cds-engine:latest                              "sh -c '/app/cds-eng…"   39 seconds ago      Up 39 seconds (healthy)   0.0.0.0:8081->8081/tcp     debian_cds-api_1
-9223395500ab        postgres:9.6.2                                        "docker-entrypoint.s…"   2 minutes ago       Up About a minute         5432/tcp                   debian_cds-db_1
+9223395500ab        postgres:9.6                                        "docker-entrypoint.s…"   2 minutes ago       Up About a minute         5432/tcp                   debian_cds-db_1
 c9b58ce83003        docker.elastic.co/elasticsearch/elasticsearch:6.7.2   "/usr/local/bin/dock…"   2 minutes ago       Up About a minute         9200/tcp, 9300/tcp         debian_elasticsearch_1
 08cfe15c3e2c        bobrik/socat                                          "socat TCP4-LISTEN:2…"   2 minutes ago       Up About a minute         127.0.0.1:2375->2375/tcp   debian_dockerhost_1
 fc2ac075c000        redis:alpine                                          "docker-entrypoint.s…"   2 minutes ago       Up About a minute         6379/tcp                   debian_cds-cache_1
diff --git a/engine/api/api_routes.go b/engine/api/api_routes.go
index 123a9b87e..27b8415be 100644
--- a/engine/api/api_routes.go
+++ b/engine/api/api_routes.go
@@ -274,6 +274,7 @@ func (api *API) InitRouter() {
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/runs/{number}/nodes/{nodeRunID}/job/{runJobID}/info", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowNodeRunJobSpawnInfosHandler))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/nodes/{nodeRunID}/job/{runJobID}/service/{serviceName}/link", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowNodeRunJobServiceLinkHandler))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/nodes/{nodeRunID}/job/{runJobID}/service/{serviceName}/log", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowNodeRunJobServiceLogHandler))
+	r.Handle("/project/{key}/workflows/{permWorkflowName}/nodes/{nodeRunID}/job/{runJobID}/links", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowNodeRunJobStepLinksHandler))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/nodes/{nodeRunID}/job/{runJobID}/step/{stepOrder}/link", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowNodeRunJobStepLinkHandler))
 	r.Handle("/project/{key}/workflows/{workflowName}/log/access", ScopeNone(), r.GET(api.getWorkflowLogAccessHandler))
 	r.Handle("/project/{key}/workflows/{permWorkflowName}/nodes/{nodeRunID}/job/{runJobID}/step/{stepOrder}/log", Scope(sdk.AuthConsumerScopeRun), r.GET(api.getWorkflowNodeRunJobStepLogHandler))
diff --git a/engine/api/application/application_parser.go b/engine/api/application/application_parser.go
index 6239b4671..212be732d 100644
--- a/engine/api/application/application_parser.go
+++ b/engine/api/application/application_parser.go
@@ -6,8 +6,9 @@ import (
 	"strings"
 	"sync"
 
-	"github.com/ovh/cds/engine/cache"
+	"github.com/ovh/cds/engine/api/ascode"
 	"github.com/ovh/cds/engine/api/keys"
+	"github.com/ovh/cds/engine/cache"
 	"github.com/ovh/cds/engine/gorpmapper"
 	"github.com/ovh/cds/sdk"
 	"github.com/ovh/cds/sdk/exportentities"
@@ -32,7 +33,7 @@ func ParseAndImport(ctx context.Context, db gorpmapper.SqlExecutorWithTx, cache
 		return nil, nil, msgList, sdk.WrapError(sdk.ErrInvalidApplicationPattern, "application name %s do not respect pattern %s", eapp.Name, sdk.NamePattern)
 	}
 
-	//Check if app exist
+	//Check if app exists
 	oldApp, err := LoadByName(db, proj.Key, eapp.Name,
 		LoadOptions.WithVariablesWithClearPassword,
 		LoadOptions.WithClearKeys,
@@ -42,13 +43,23 @@ func ParseAndImport(ctx context.Context, db gorpmapper.SqlExecutorWithTx, cache
 		return nil, nil, msgList, sdk.WrapError(err, "unable to load application")
 	}
 
-	//If the application exist and we don't want to force, raise an error
+	//If the application exists and we don't want to force, raise an error
 	if oldApp != nil && !opts.Force {
 		return nil, nil, msgList, sdk.WithStack(sdk.ErrApplicationExist)
 	}
 
-	if oldApp != nil && oldApp.FromRepository != "" && opts.FromRepository != oldApp.FromRepository {
-		return nil, nil, msgList, sdk.NewErrorFrom(sdk.ErrApplicationAsCodeOverride, "unable to update existing ascode application from %s", oldApp.FromRepository)
+	if oldApp != nil {
+		if opts.Force && opts.FromRepository == "" {
+			if oldApp.FromRepository != "" {
+				if err := ascode.DeleteEventsApplicationOnlyFromRepoName(ctx, db, oldApp.FromRepository, oldApp.ID, oldApp.Name); err != nil {
+					return nil, nil, msgList, sdk.WrapError(err, "unable to delete as_code_event for %s on repo %s", oldApp.Name, oldApp.FromRepository)
+				}
+				msgList = append(msgList, sdk.NewMessage(sdk.MsgApplicationDetached, eapp.Name, oldApp.FromRepository))
+			}
+			log.Debug("ParseAndImport>> force import application %s in project %s without fromRepository", eapp.Name, proj.Key)
+		} else if oldApp.FromRepository != "" && opts.FromRepository != oldApp.FromRepository {
+			return nil, nil, msgList, sdk.NewErrorFrom(sdk.ErrApplicationAsCodeOverride, "unable to update existing ascode application from %s", oldApp.FromRepository)
+		}
 	}
 
 	//Craft the application
diff --git a/engine/api/application/dao.go b/engine/api/application/dao.go
index 6767e8aec..d232240f1 100644
--- a/engine/api/application/dao.go
+++ b/engine/api/application/dao.go
@@ -327,3 +327,34 @@ func LoadIcon(db gorp.SqlExecutor, appID int64) (string, error) {
 	icon, err := db.SelectStr("SELECT icon FROM application WHERE id = $1", appID)
 	return icon, sdk.WithStack(err)
 }
+
+// LoadAllNamesByFromRepository returns all application names for a repository
+func LoadAllNamesByFromRepository(db gorp.SqlExecutor, projID int64, fromRepository string) (sdk.IDNames, error) {
+	if fromRepository == "" {
+		return nil, sdk.WithData(sdk.ErrUnknownError, "could not call LoadAllNamesByFromRepository with empty fromRepository")
+	}
+	query := `SELECT application.id, application.name
+			  FROM application
+			  WHERE project_id = $1 AND from_repository = $2
+			  ORDER BY application.name`
+
+	var res sdk.IDNames
+	if _, err := db.Select(&res, query, projID, fromRepository); err != nil {
+		if err == sql.ErrNoRows {
+			return res, nil
+		}
+		return nil, sdk.WrapError(err, "application.LoadAllNamesByFromRepository")
+	}
+
+	return res, nil
+}
+
+// ResetFromRepository reset fromRepository for all applications using the same fromRepository in a given project
+func ResetFromRepository(db gorp.SqlExecutor, projID int64, fromRepository string) error {
+	if fromRepository == "" {
+		return sdk.WithData(sdk.ErrUnknownError, "could not call LoadAllNamesByFromRepository with empty fromRepository")
+	}
+	query := `UPDATE application SET from_repository='' WHERE project_id = $1 AND from_repository = $2`
+	_, err := db.Exec(query, projID, fromRepository)
+	return sdk.WithStack(err)
+}
diff --git a/engine/api/application_import.go b/engine/api/application_import.go
index 4855b50d1..41b9fbdcf 100644
--- a/engine/api/application_import.go
+++ b/engine/api/application_import.go
@@ -63,13 +63,13 @@ func (api *API) postApplicationImportHandler() service.Handler {
 		defer tx.Rollback() // nolint
 
 		newApp, _, msgList, globalError := application.ParseAndImport(ctx, tx, api.Cache, *proj, eapp, application.ImportOptions{Force: force}, project.DecryptWithBuiltinKey, getAPIConsumer(ctx))
-		msgListString := translate(r, msgList)
+		msgListString := translate(msgList)
 		if globalError != nil {
 			globalError = sdk.WrapError(globalError, "Unable to import application %s", eapp.Name)
 			if sdk.ErrorIsUnknown(globalError) {
 				return globalError
 			}
-			sdkErr := sdk.ExtractHTTPError(globalError, r.Header.Get("Accept-Language"))
+			sdkErr := sdk.ExtractHTTPError(globalError)
 			return service.WriteJSON(w, append(msgListString, sdkErr.Message), sdkErr.Status)
 		}
 
diff --git a/engine/api/ascode.go b/engine/api/ascode.go
index 2b7b5be83..3e10e65d3 100644
--- a/engine/api/ascode.go
+++ b/engine/api/ascode.go
@@ -206,7 +206,7 @@ func (api *API) postPerformImportAsCodeHandler() service.Handler {
 		if err := workflowtemplate.UpdateTemplateInstanceWithWorkflow(ctx, api.mustDB(), *wrkflw, *consumer, wti); err != nil {
 			return err
 		}
-		msgListString := translate(r, allMsg)
+		msgListString := translate(allMsg)
 
 		tx, err := api.mustDB().Begin()
 		if err != nil {
diff --git a/engine/api/ascode/dao.go b/engine/api/ascode/dao.go
index 88781c785..043594dcc 100644
--- a/engine/api/ascode/dao.go
+++ b/engine/api/ascode/dao.go
@@ -4,9 +4,8 @@ import (
 	"context"
 	"database/sql"
 
-	"github.com/lib/pq"
-
 	"github.com/go-gorp/gorp"
+	"github.com/lib/pq"
 
 	"github.com/ovh/cds/engine/api/database/gorpmapping"
 	"github.com/ovh/cds/sdk"
@@ -30,6 +29,7 @@ func LoadEventByWorkflowIDAndPullRequest(ctx context.Context, db gorp.SqlExecuto
 	return &event, nil
 }
 
+// LoadEventsByWorkflowIDs loads all events attached to a workflow_id
 func LoadEventsByWorkflowIDs(ctx context.Context, db gorp.SqlExecutor, workflowIDs []int64) ([]sdk.AsCodeEvent, error) {
 	query := gorpmapping.NewQuery("SELECT * FROM as_code_events where workflow_id = ANY($1)").Args(pq.Int64Array(workflowIDs))
 	var events []dbAsCodeEvents
@@ -95,3 +95,45 @@ func deleteEvent(db gorp.SqlExecutor, event *sdk.AsCodeEvent) error {
 	}
 	return nil
 }
+
+// DeleteAsCodeEventByWorkflowID removes all as_code_event from workflow_id
+func DeleteAsCodeEventByWorkflowID(db gorp.SqlExecutor, id int64) error {
+	_, err := db.Exec("DELETE FROM as_code_events WHERE workflow_id = $1", id)
+	return sdk.WrapError(err, "unable to delete as_code_events with workflow_id %d", id)
+}
+
+func DeleteEventsPipelineOnlyFromRepoName(ctx context.Context, db gorp.SqlExecutor, fromRepository string, pipID int64, pipName string) error {
+	query := `DELETE FROM as_code_events 
+	WHERE from_repository=$1
+	AND (data -> 'pipelines' ->> $2)::text = $3
+	AND (data -> 'workflows')::text = 'null'
+	AND (data -> 'environments')::text = 'null'
+	AND (data -> 'applications')::text = 'null'
+	`
+	_, err := db.Exec(query, fromRepository, pipID, pipName)
+	return sdk.WithStack(err)
+}
+
+func DeleteEventsApplicationOnlyFromRepoName(ctx context.Context, db gorp.SqlExecutor, fromRepository string, appID int64, appName string) error {
+	query := `DELETE FROM as_code_events 
+	WHERE from_repository=$1
+	AND (data -> 'applications' ->> $2)::text = $3
+	AND (data -> 'workflows')::text = 'null'
+	AND (data -> 'environments')::text = 'null'
+	AND (data -> 'pipelines')::text = 'null'
+	`
+	_, err := db.Exec(query, fromRepository, appID, appName)
+	return sdk.WithStack(err)
+}
+
+func DeleteEventsEnvironmentOnlyFromRepoName(ctx context.Context, db gorp.SqlExecutor, fromRepository string, envID int64, envName string) error {
+	query := `DELETE FROM as_code_events 
+	WHERE from_repository=$1
+	AND (data -> 'environments' ->> $2)::text = $3
+	AND (data -> 'workflows')::text = 'null'
+	AND (data -> 'pipelines')::text = 'null'
+	AND (data -> 'applications')::text = 'null'
+	`
+	_, err := db.Exec(query, fromRepository, envID, envName)
+	return sdk.WithStack(err)
+}
diff --git a/engine/api/auth_local.go b/engine/api/auth_local.go
index 3e6cb554f..5e2f4605a 100644
--- a/engine/api/auth_local.go
+++ b/engine/api/auth_local.go
@@ -131,6 +131,10 @@ func initBuiltinConsumersFromStartupConfig(ctx context.Context, tx gorpmapper.Sq
 			scopes = sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeService, sdk.AuthConsumerScopeWorker, sdk.AuthConsumerScopeRunExecution)
 		case StartupConfigConsumerTypeCDNStorageCDS:
 			scopes = sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeService, sdk.AuthConsumerScopeWorker, sdk.AuthConsumerScopeRunExecution)
+		case StartupConfigConsumerTypeVCS:
+			scopes = sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeProject, sdk.AuthConsumerScopeRun, sdk.AuthConsumerScopeService)
+		case StartupConfigConsumerTypeRepositories:
+			scopes = sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeProject, sdk.AuthConsumerScopeRun, sdk.AuthConsumerScopeService)
 		default:
 			scopes = sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeProject, sdk.AuthConsumerScopeRun)
 		}
diff --git a/engine/api/environment/environment.go b/engine/api/environment/environment.go
index 430e9bde1..889bd9591 100644
--- a/engine/api/environment/environment.go
+++ b/engine/api/environment/environment.go
@@ -283,3 +283,34 @@ func LoadAllNames(db gorp.SqlExecutor, projID int64) (sdk.IDNames, error) {
 
 	return res, nil
 }
+
+// LoadAllNamesByFromRepository returns all environment names for a repository
+func LoadAllNamesByFromRepository(db gorp.SqlExecutor, projID int64, fromRepository string) (sdk.IDNames, error) {
+	if fromRepository == "" {
+		return nil, sdk.WithData(sdk.ErrUnknownError, "could not call LoadAllNamesByFromRepository with empty fromRepository")
+	}
+	query := `SELECT environment.id, environment.name
+			  FROM environment
+			  WHERE project_id = $1 AND from_repository = $2
+			  ORDER BY environment.name`
+
+	var res sdk.IDNames
+	if _, err := db.Select(&res, query, projID, fromRepository); err != nil {
+		if err == sql.ErrNoRows {
+			return res, nil
+		}
+		return nil, sdk.WrapError(err, "environment.LoadAllNamesByFromRepository")
+	}
+
+	return res, nil
+}
+
+// ResetFromRepository reset fromRepository for all environments using the same fromRepository in a given project
+func ResetFromRepository(db gorp.SqlExecutor, projID int64, fromRepository string) error {
+	if fromRepository == "" {
+		return sdk.WithData(sdk.ErrUnknownError, "could not call LoadAllNamesByFromRepository with empty fromRepository")
+	}
+	query := `UPDATE environment SET from_repository='' WHERE project_id = $1 AND from_repository = $2`
+	_, err := db.Exec(query, projID, fromRepository)
+	return sdk.WithStack(err)
+}
diff --git a/engine/api/environment/environment_parser.go b/engine/api/environment/environment_parser.go
index 1aee04b3f..be444dec4 100644
--- a/engine/api/environment/environment_parser.go
+++ b/engine/api/environment/environment_parser.go
@@ -1,10 +1,12 @@
 package environment
 
 import (
+	"context"
 	"fmt"
 	"strings"
 	"sync"
 
+	"github.com/ovh/cds/engine/api/ascode"
 	"github.com/ovh/cds/engine/api/keys"
 	"github.com/ovh/cds/engine/gorpmapper"
 	"github.com/ovh/cds/sdk"
@@ -19,10 +21,12 @@ type ImportOptions struct {
 }
 
 // ParseAndImport parse an exportentities.Environment and insert or update the environment in database
-func ParseAndImport(db gorpmapper.SqlExecutorWithTx, proj sdk.Project, eenv exportentities.Environment, opts ImportOptions, decryptFunc keys.DecryptFunc, u sdk.Identifiable) (*sdk.Environment, []sdk.Variable, []sdk.Message, error) {
+func ParseAndImport(ctx context.Context, db gorpmapper.SqlExecutorWithTx, proj sdk.Project, eenv exportentities.Environment, opts ImportOptions, decryptFunc keys.DecryptFunc, u sdk.Identifiable) (*sdk.Environment, []sdk.Variable, []sdk.Message, error) {
 	log.Debug("ParseAndImport>> Import environment %s in project %s (force=%v)", eenv.Name, proj.Key, opts.Force)
 	log.Debug("ParseAndImport>> Env: %+v", eenv)
 
+	msgList := []sdk.Message{}
+
 	// Check valid application name
 	rx := sdk.NamePatternRegex
 	if !rx.MatchString(eenv.Name) {
@@ -44,8 +48,18 @@ func ParseAndImport(db gorpmapper.SqlExecutorWithTx, proj sdk.Project, eenv expo
 		exist = true
 	}
 
-	if oldEnv != nil && oldEnv.FromRepository != "" && opts.FromRepository != oldEnv.FromRepository {
-		return nil, nil, nil, sdk.NewErrorFrom(sdk.ErrEnvironmentAsCodeOverride, "unable to update existing ascode environment from %s", oldEnv.FromRepository)
+	if oldEnv != nil {
+		if opts.Force && opts.FromRepository == "" {
+			if oldEnv.FromRepository != "" {
+				if err := ascode.DeleteEventsEnvironmentOnlyFromRepoName(ctx, db, oldEnv.FromRepository, oldEnv.ID, oldEnv.Name); err != nil {
+					return nil, nil, msgList, sdk.WrapError(err, "unable to delete as_code_event for %s on repo %s", oldEnv.Name, oldEnv.FromRepository)
+				}
+				msgList = append(msgList, sdk.NewMessage(sdk.MsgEnvironmentDetached, eenv.Name, oldEnv.FromRepository))
+			}
+			log.Debug("ParseAndImport>> Force import environment %s in project %s without fromRepository", eenv.Name, proj.Key)
+		} else if oldEnv.FromRepository != "" && opts.FromRepository != oldEnv.FromRepository {
+			return nil, nil, nil, sdk.NewErrorFrom(sdk.ErrEnvironmentAsCodeOverride, "unable to update existing ascode environment from %s", oldEnv.FromRepository)
+		}
 	}
 
 	env := new(sdk.Environment)
@@ -142,7 +156,7 @@ func ParseAndImport(db gorpmapper.SqlExecutorWithTx, proj sdk.Project, eenv expo
 	done := new(sync.WaitGroup)
 	done.Add(1)
 	msgChan := make(chan sdk.Message)
-	msgList := []sdk.Message{}
+
 	go func(array *[]sdk.Message) {
 		defer done.Done()
 		for m := range msgChan {
diff --git a/engine/api/environment_import.go b/engine/api/environment_import.go
index f902e7f17..41f351d0d 100644
--- a/engine/api/environment_import.go
+++ b/engine/api/environment_import.go
@@ -58,14 +58,14 @@ func (api *API) postEnvironmentImportHandler() service.Handler {
 		}
 		defer tx.Rollback() // nolint
 
-		_, _, msgList, globalError := environment.ParseAndImport(tx, *proj, data, environment.ImportOptions{Force: force}, project.DecryptWithBuiltinKey, getAPIConsumer(ctx))
-		msgListString := translate(r, msgList)
+		_, _, msgList, globalError := environment.ParseAndImport(ctx, tx, *proj, data, environment.ImportOptions{Force: force}, project.DecryptWithBuiltinKey, getAPIConsumer(ctx))
+		msgListString := translate(msgList)
 		if globalError != nil {
 			globalError = sdk.WrapError(globalError, "Unable to import environment %s", data.Name)
 			if sdk.ErrorIsUnknown(globalError) {
 				return globalError
 			}
-			sdkErr := sdk.ExtractHTTPError(globalError, r.Header.Get("Accept-Language"))
+			sdkErr := sdk.ExtractHTTPError(globalError)
 			return service.WriteJSON(w, append(msgListString, sdkErr.Message), sdkErr.Status)
 		}
 
@@ -136,7 +136,7 @@ func (api *API) importNewEnvironmentHandler() service.Handler {
 		close(msgChan)
 		<-done
 
-		msgListString := translate(r, allMsg)
+		msgListString := translate(allMsg)
 
 		if err := tx.Commit(); err != nil {
 			return sdk.WithStack(err)
@@ -209,7 +209,7 @@ func (api *API) importIntoEnvironmentHandler() service.Handler {
 		close(msgChan)
 		<-done
 
-		msgListString := translate(r, allMsg)
+		msgListString := translate(allMsg)
 
 		if err := tx.Commit(); err != nil {
 			return sdk.WithStack(err)
diff --git a/engine/api/pipeline/pipeline.go b/engine/api/pipeline/pipeline.go
index 29d499d06..d92d760fa 100644
--- a/engine/api/pipeline/pipeline.go
+++ b/engine/api/pipeline/pipeline.go
@@ -312,7 +312,7 @@ func LoadAllNames(db gorp.SqlExecutor, projID int64) (sdk.IDNames, error) {
 		if err == sql.ErrNoRows {
 			return res, nil
 		}
-		return nil, sdk.WrapError(err, "application.loadpipelinenames")
+		return nil, sdk.WrapError(err, "pipeline.loadpipelinenames")
 	}
 
 	return res, nil
@@ -384,3 +384,34 @@ func ExistPipeline(db gorp.SqlExecutor, projectID int64, name string) (bool, err
 	}
 	return false, nil
 }
+
+// LoadAllNamesByFromRepository returns all pipeline names for a repository
+func LoadAllNamesByFromRepository(db gorp.SqlExecutor, projID int64, fromRepository string) (sdk.IDNames, error) {
+	if fromRepository == "" {
+		return nil, sdk.WithData(sdk.ErrUnknownError, "could not call LoadAllNamesByFromRepository with empty fromRepository")
+	}
+	query := `SELECT pipeline.id, pipeline.name
+			  FROM pipeline
+			  WHERE project_id = $1 AND from_repository = $2
+			  ORDER BY pipeline.name`
+
+	var res sdk.IDNames
+	if _, err := db.Select(&res, query, projID, fromRepository); err != nil {
+		if err == sql.ErrNoRows {
+			return res, nil
+		}
+		return nil, sdk.WrapError(err, "pipeline.LoadAllNamesByFromRepository")
+	}
+
+	return res, nil
+}
+
+// ResetFromRepository reset fromRepository for all pipelines using the same fromRepository in a given project
+func ResetFromRepository(db gorp.SqlExecutor, projID int64, fromRepository string) error {
+	if fromRepository == "" {
+		return sdk.WithData(sdk.ErrUnknownError, "could not call LoadAllNamesByFromRepository with empty fromRepository")
+	}
+	query := `UPDATE pipeline SET from_repository='' WHERE project_id = $1 AND from_repository = $2`
+	_, err := db.Exec(query, projID, fromRepository)
+	return sdk.WithStack(err)
+}
diff --git a/engine/api/pipeline/pipeline_importer.go b/engine/api/pipeline/pipeline_importer.go
index 808ede182..4db8ec193 100644
--- a/engine/api/pipeline/pipeline_importer.go
+++ b/engine/api/pipeline/pipeline_importer.go
@@ -7,6 +7,7 @@ import (
 	"github.com/go-gorp/gorp"
 
 	"github.com/ovh/cds/engine/api/action"
+	"github.com/ovh/cds/engine/api/ascode"
 	"github.com/ovh/cds/engine/api/group"
 	"github.com/ovh/cds/engine/cache"
 	"github.com/ovh/cds/sdk"
@@ -26,7 +27,15 @@ func ImportUpdate(ctx context.Context, db gorp.SqlExecutor, proj sdk.Project, pi
 		return sdk.WrapError(err, "Unable to load pipeline %s %s", proj.Key, pip.Name)
 	}
 
-	if !opts.Force && oldPipeline.FromRepository != "" && pip.FromRepository != oldPipeline.FromRepository {
+	if opts.Force && opts.FromRepository == "" {
+		if oldPipeline.FromRepository != "" {
+			if err := ascode.DeleteEventsPipelineOnlyFromRepoName(ctx, db, oldPipeline.FromRepository, oldPipeline.ID, oldPipeline.Name); err != nil {
+				return sdk.WrapError(err, "unable to delete as_code_event for %s on repo %s", pip.Name, pip.FromRepository)
+			}
+			msgChan <- sdk.NewMessage(sdk.MsgPipelineDetached, pip.Name, oldPipeline.FromRepository)
+		}
+		log.Debug("ImportUpdate>> Force import pipeline %s in project %s without fromRepository", pip.Name, proj.Key)
+	} else if oldPipeline.FromRepository != "" && pip.FromRepository != oldPipeline.FromRepository {
 		return sdk.WrapError(sdk.ErrPipelineAsCodeOverride, "unable to update as code pipeline %s/%s.", oldPipeline.FromRepository, pip.FromRepository)
 	}
 
diff --git a/engine/api/pipeline_import.go b/engine/api/pipeline_import.go
index 55d4a0714..ce4689b15 100644
--- a/engine/api/pipeline_import.go
+++ b/engine/api/pipeline_import.go
@@ -87,13 +87,13 @@ func (api *API) importPipelineHandler() service.Handler {
 
 		pip, allMsg, globalError := pipeline.ParseAndImport(ctx, tx, api.Cache, *proj, data, getAPIConsumer(ctx),
 			pipeline.ImportOptions{Force: force})
-		msgListString := translate(r, allMsg)
+		msgListString := translate(allMsg)
 		if globalError != nil {
 			globalError = sdk.WrapError(globalError, "unable to import pipeline")
 			if sdk.ErrorIsUnknown(globalError) {
 				return globalError
 			}
-			sdkErr := sdk.ExtractHTTPError(globalError, r.Header.Get("Accept-Language"))
+			sdkErr := sdk.ExtractHTTPError(globalError)
 			return service.WriteJSON(w, append(msgListString, sdkErr.Message), sdkErr.Status)
 		}
 
@@ -147,7 +147,7 @@ func (api *API) putImportPipelineHandler() service.Handler {
 		defer tx.Rollback() // nolint
 
 		pip, allMsg, err := pipeline.ParseAndImport(ctx, tx, api.Cache, *proj, data, getAPIConsumer(ctx), pipeline.ImportOptions{Force: true, PipelineName: pipelineName})
-		msgListString := translate(r, allMsg)
+		msgListString := translate(allMsg)
 		if err != nil {
 			return sdk.NewErrorWithStack(err, sdk.NewErrorFrom(sdk.ErrInvalidPipeline, "unable to parse and import pipeline"))
 		}
diff --git a/engine/api/purge/purge_run.go b/engine/api/purge/purge_run.go
index 23e967211..51d8ffafa 100644
--- a/engine/api/purge/purge_run.go
+++ b/engine/api/purge/purge_run.go
@@ -151,7 +151,7 @@ func applyRetentionPolicyOnRun(ctx context.Context, db *gorp.DbMap, wf sdk.Workf
 	}
 
 	if err := luacheck.Perform(wf.RetentionPolicy); err != nil {
-		return true, sdk.NewErrorFrom(sdk.ErrWrongRequest, "%v", err)
+		return true, sdk.NewErrorFrom(sdk.ErrWrongRequest, "unable to apply retention policy on workflow %s/%s: %v", wf.ProjectKey, wf.Name, err)
 	}
 
 	if luacheck.Result {
diff --git a/engine/api/router_util.go b/engine/api/router_util.go
index 3e740cd84..f5ac83236 100644
--- a/engine/api/router_util.go
+++ b/engine/api/router_util.go
@@ -162,11 +162,10 @@ func requestVarInt(r *http.Request, s string) (int64, error) {
 	return id, nil
 }
 
-func translate(r *http.Request, msgList []sdk.Message) []string {
-	al := r.Header.Get("Accept-Language")
+func translate(msgList []sdk.Message) []string {
 	msgListString := []string{}
 	for _, m := range msgList {
-		s := m.String(al)
+		s := m.String()
 		if s != "" {
 			msgListString = append(msgListString, s)
 		}
diff --git a/engine/api/templates.go b/engine/api/templates.go
index 72638e156..d5f833181 100644
--- a/engine/api/templates.go
+++ b/engine/api/templates.go
@@ -480,7 +480,7 @@ func (api *API) postTemplateApplyHandler() service.Handler {
 			return err
 		}
 
-		msgStrings := translate(r, msgs)
+		msgStrings := translate(msgs)
 
 		log.Debug("postTemplateApplyHandler> importing the workflow %s from template %s", wkf.Name, wt.Slug)
 
@@ -976,7 +976,7 @@ func (api *API) postTemplatePushHandler() service.Handler {
 		w.Header().Add(sdk.ResponseTemplateGroupNameHeader, wt.Group.Name)
 		w.Header().Add(sdk.ResponseTemplateSlugHeader, wt.Slug)
 
-		return service.WriteJSON(w, translate(r, msgs), http.StatusOK)
+		return service.WriteJSON(w, translate(msgs), http.StatusOK)
 	}
 }
 
diff --git a/engine/api/worker/dao.go b/engine/api/worker/dao.go
index 74ce6b263..88f2b9746 100644
--- a/engine/api/worker/dao.go
+++ b/engine/api/worker/dao.go
@@ -103,13 +103,13 @@ func LoadByConsumerID(ctx context.Context, db gorp.SqlExecutor, id string) (*sdk
 	return get(ctx, db, query)
 }
 
-func LoadByID(ctx context.Context, db gorp.SqlExecutor, id string) (*sdk.Worker, error) {
+func LoadByID(ctx context.Context, db gorp.SqlExecutor, id string, opts ...gorpmapping.GetOptionFunc) (*sdk.Worker, error) {
 	query := gorpmapping.NewQuery(`
     SELECT *
     FROM worker
     WHERE id = $1
   `).Args(id)
-	return get(ctx, db, query)
+	return get(ctx, db, query, opts...)
 }
 
 func LoadAll(ctx context.Context, db gorp.SqlExecutor) ([]sdk.Worker, error) {
@@ -144,7 +144,7 @@ func LoadDeadWorkers(ctx context.Context, db gorp.SqlExecutor, timeout float64,
 
 // SetStatus sets job_run_id and status to building on given worker
 func SetStatus(ctx context.Context, db gorpmapper.SqlExecutorWithTx, workerID string, status string) error {
-	w, err := LoadByID(ctx, db, workerID)
+	w, err := LoadByID(ctx, db, workerID, gorpmapping.GetOptions.WithDecryption)
 	if err != nil {
 		return err
 	}
diff --git a/engine/api/workflow.go b/engine/api/workflow.go
index 6fae69f9c..0391b7aee 100644
--- a/engine/api/workflow.go
+++ b/engine/api/workflow.go
@@ -199,8 +199,7 @@ func (api *API) postWorkflowRetentionPolicyDryRun() service.Handler {
 		api.GoRoutines.Exec(api.Router.Background, "workflow-retention-dryrun", func(ctx context.Context) {
 			if err := purge.ApplyRetentionPolicyOnWorkflow(ctx, api.Cache, api.mustDBWithCtx(ctx), *wf, purge.MarkAsDeleteOptions{DryRun: true}, u.AuthentifiedUser); err != nil {
 				log.ErrorWithFields(ctx, log.Fields{"stack_trace": fmt.Sprintf("%+v", err)}, "%s", err)
-				al := r.Header.Get("Accept-Language")
-				httpErr := sdk.ExtractHTTPError(err, al)
+				httpErr := sdk.ExtractHTTPError(err)
 				event.PublishWorkflowRetentionDryRun(ctx, key, name, "ERROR", httpErr.Error(), nil, 0, u.AuthentifiedUser)
 			}
 		})
diff --git a/engine/api/workflow/dao.go b/engine/api/workflow/dao.go
index 8cd60dafc..4a4383765 100644
--- a/engine/api/workflow/dao.go
+++ b/engine/api/workflow/dao.go
@@ -1135,7 +1135,7 @@ func Push(ctx context.Context, db *gorp.DbMap, store cache.Store, proj *sdk.Proj
 		if opts != nil {
 			fromRepo = opts.FromRepository
 		}
-		envDB, envsSecrets, msgList, err := environment.ParseAndImport(tx, *proj, env, environment.ImportOptions{Force: true, FromRepository: fromRepo}, decryptFunc, u)
+		envDB, envsSecrets, msgList, err := environment.ParseAndImport(ctx, tx, *proj, env, environment.ImportOptions{Force: true, FromRepository: fromRepo}, decryptFunc, u)
 		allMsg = append(allMsg, msgList...)
 		if err != nil {
 			return allMsg, nil, nil, nil, sdk.ErrorWithFallback(err, sdk.ErrWrongRequest, "unable to import environment %s/%s", proj.Key, env.Name)
diff --git a/engine/api/workflow/dao_node_run.go b/engine/api/workflow/dao_node_run.go
index b8bb6c2dd..b3a7a601c 100644
--- a/engine/api/workflow/dao_node_run.go
+++ b/engine/api/workflow/dao_node_run.go
@@ -58,18 +58,18 @@ workflow_node_run.callback
 const nodeRunTestsField string = ", workflow_node_run.tests"
 const withLightNodeRunTestsField string = ", json_build_object('ko', workflow_node_run.tests->'ko', 'ok', workflow_node_run.tests->'ok', 'skipped', workflow_node_run.tests->'skipped', 'total', workflow_node_run.tests->'total') AS tests"
 
-func LoadNodeRunIDs(db gorp.SqlExecutor, wIDs []int64) ([]sdk.WorkflowNodeRunIdentifiers, error) {
+func LoadNodeRunIDs(db gorp.SqlExecutor, wIDs []int64, status []string) ([]sdk.WorkflowNodeRunIdentifiers, error) {
 	query := `
 		SELECT workflow_run.id, workflow_run.workflow_id, workflow.name, workflow_run.num, workflow_node_run.id as node_run_id
 		FROM workflow_run
 		JOIN workflow ON workflow.id = workflow_run.workflow_id
 		JOIN workflow_node_run ON workflow_node_run.workflow_run_id = workflow_run.id
-		WHERE workflow_run.workflow_id = ANY($1)
+		WHERE workflow_run.workflow_id = ANY($1) AND workflow_node_run.status = ANY($2)
 		ORDER BY workflow_id, id, node_run_id;
 	`
 
 	var ids []sdk.WorkflowNodeRunIdentifiers
-	if _, err := db.Select(&ids, query, pq.Int64Array(wIDs)); err != nil {
+	if _, err := db.Select(&ids, query, pq.Int64Array(wIDs), pq.StringArray(status)); err != nil {
 		return nil, err
 	}
 	return ids, nil
diff --git a/engine/api/workflow/dao_run.go b/engine/api/workflow/dao_run.go
index 392c849b3..edea56d4a 100644
--- a/engine/api/workflow/dao_run.go
+++ b/engine/api/workflow/dao_run.go
@@ -54,7 +54,6 @@ type LoadRunOptions struct {
 	WithVulnerabilities     bool
 	WithDeleted             bool
 	DisableDetailledNodeRun bool
-	Language                string
 }
 
 // insertWorkflowRun inserts in table "workflow_run""
@@ -955,11 +954,7 @@ func syncNodeRuns(db gorp.SqlExecutor, wr *sdk.WorkflowRun, loadOpts LoadRunOpti
 			}
 			wnr.Coverage = cov
 		}
-		var l = loadOpts.Language
-		if l == "" {
-			l = "en"
-		}
-		wnr.Translate(l)
+		wnr.Translate()
 		wr.WorkflowNodeRuns[wnr.WorkflowNodeID] = append(wr.WorkflowNodeRuns[wnr.WorkflowNodeID], *wnr)
 	}
 
diff --git a/engine/api/workflow/execute_node_run.go b/engine/api/workflow/execute_node_run.go
index d30ba7519..9713a5091 100644
--- a/engine/api/workflow/execute_node_run.go
+++ b/engine/api/workflow/execute_node_run.go
@@ -523,7 +523,7 @@ jobLoop:
 				msg := sdk.SpawnMsg{
 					ID: sdk.MsgSpawnInfoJobError.ID,
 				}
-				msg.Args = []interface{}{sdk.ExtractHTTPError(e, "").Error()}
+				msg.Args = []interface{}{sdk.ExtractHTTPError(e).Error()}
 				wjob.SpawnInfos = append(wjob.SpawnInfos, sdk.SpawnInfo{
 					APITime:     time.Now(),
 					Message:     msg,
diff --git a/engine/api/workflow/process_node.go b/engine/api/workflow/process_node.go
index f945af74a..05d186f8b 100644
--- a/engine/api/workflow/process_node.go
+++ b/engine/api/workflow/process_node.go
@@ -36,7 +36,7 @@ func processNodeTriggers(ctx context.Context, db gorpmapper.SqlExecutorWithTx, s
 			r1, _, errPwnr := processNodeRun(ctx, db, store, proj, wr, mapNodes, &t.ChildNode, int(parentSubNumber), parentNodeRun, nil, nil)
 			if errPwnr != nil {
 				log.Error(ctx, "processWorkflowRun> Unable to process node ID=%d: %s", t.ChildNode.ID, errPwnr)
-				AddWorkflowRunInfo(wr, sdk.SpawnMsgNew(*sdk.MsgWorkflowError, sdk.ExtractHTTPError(errPwnr, "")))
+				AddWorkflowRunInfo(wr, sdk.SpawnMsgNew(*sdk.MsgWorkflowError, sdk.ExtractHTTPError(errPwnr)))
 			}
 			report.Merge(ctx, r1)
 			continue
@@ -242,7 +242,7 @@ func processNode(ctx context.Context, db gorpmapper.SqlExecutorWithTx, store cac
 
 		vcsInf, errVcs = getVCSInfos(ctx, db, store, proj.Key, vcsServer, currentJobGitValues, app.Name, app.VCSServer, app.RepositoryFullname)
 		if errVcs != nil {
-			AddWorkflowRunInfo(wr, sdk.SpawnMsgNew(*sdk.MsgWorkflowError, sdk.ExtractHTTPError(errVcs, "")))
+			AddWorkflowRunInfo(wr, sdk.SpawnMsgNew(*sdk.MsgWorkflowError, sdk.ExtractHTTPError(errVcs)))
 			return nil, false, sdk.WrapError(errVcs, "unable to get git informations")
 		}
 	}
@@ -479,7 +479,7 @@ func computePayload(n *sdk.Node, hookEvent *sdk.WorkflowNodeRunHookEvent, manual
 func computeNodeContextBuildParameters(ctx context.Context, proj sdk.Project, wr *sdk.WorkflowRun, run *sdk.WorkflowNodeRun, n *sdk.Node, runContext nodeRunContext) {
 	nodeRunParams, errParam := getNodeRunBuildParameters(ctx, proj, wr, run, runContext)
 	if errParam != nil {
-		AddWorkflowRunInfo(wr, sdk.SpawnMsgNew(*sdk.MsgWorkflowError, sdk.ExtractHTTPError(errParam, "")))
+		AddWorkflowRunInfo(wr, sdk.SpawnMsgNew(*sdk.MsgWorkflowError, sdk.ExtractHTTPError(errParam)))
 		// if there an error -> display it in workflowRunInfo and not stop the launch
 		log.Error(ctx, "processNode> getNodeRunBuildParameters failed. Project:%s [#%d.%d]%s.%d with payload %v err:%v", proj.Name, wr.Number, run.SubNumber, wr.Workflow.Name, n.ID, run.Payload, errParam)
 	}
diff --git a/engine/api/workflow/workflow_importer.go b/engine/api/workflow/workflow_importer.go
index d2e12dacc..1163dd6ce 100644
--- a/engine/api/workflow/workflow_importer.go
+++ b/engine/api/workflow/workflow_importer.go
@@ -4,10 +4,15 @@ import (
 	"context"
 
 	"github.com/go-gorp/gorp"
+	"github.com/ovh/cds/engine/api/application"
+	"github.com/ovh/cds/engine/api/ascode"
+	"github.com/ovh/cds/engine/api/environment"
 	"github.com/ovh/cds/engine/api/group"
+	"github.com/ovh/cds/engine/api/pipeline"
 	"github.com/ovh/cds/engine/cache"
 	"github.com/ovh/cds/engine/gorpmapper"
 	"github.com/ovh/cds/sdk"
+	"github.com/ovh/cds/sdk/log"
 	"github.com/ovh/cds/sdk/telemetry"
 )
 
@@ -36,7 +41,6 @@ func Import(ctx context.Context, db gorpmapper.SqlExecutorWithTx, store cache.St
 		if msgChan != nil {
 			msgChan <- sdk.NewMessage(sdk.MsgWorkflowImportedInserted, w.Name)
 		}
-
 		return nil
 	}
 
@@ -48,6 +52,14 @@ func Import(ctx context.Context, db gorpmapper.SqlExecutorWithTx, store cache.St
 		return sdk.NewErrorFrom(sdk.ErrAlreadyExist, "workflow exists")
 	}
 
+	if force && oldW != nil && oldW.FromRepository != "" && w.FromRepository == "" {
+		if err := detachResourceFromRepository(db, proj.ID, oldW, msgChan); err != nil {
+			return err
+		}
+		msgChan <- sdk.NewMessage(sdk.MsgWorkflowDetached, oldW.Name, oldW.FromRepository)
+		log.Debug("workflow.Import>> Force import workflow %s in project %s without fromRepository", oldW.Name, proj.Key)
+	}
+
 	// Retrieve existing hook
 	oldHooksByRef := oldW.WorkflowData.GetHooksMapRef()
 	for i := range w.WorkflowData.Node.Hooks {
@@ -91,6 +103,56 @@ func Import(ctx context.Context, db gorpmapper.SqlExecutorWithTx, store cache.St
 	return nil
 }
 
+func detachResourceFromRepository(db gorp.SqlExecutor, projectID int64, oldW *sdk.Workflow, msgChan chan<- sdk.Message) error {
+	// delete ascode event if exists on this workflow
+	if err := ascode.DeleteAsCodeEventByWorkflowID(db, oldW.ID); err != nil {
+		return err
+	}
+	// reset fromRepository for all pipeline using it
+	pips, err := pipeline.LoadAllNamesByFromRepository(db, projectID, oldW.FromRepository)
+	if err != nil {
+		return err
+	}
+
+	if err := pipeline.ResetFromRepository(db, projectID, oldW.FromRepository); err != nil {
+		return sdk.WrapError(err, "could not reset fromRepository %s from pipelines", oldW.FromRepository)
+	}
+
+	for _, pip := range pips {
+		msgChan <- sdk.NewMessage(sdk.MsgPipelineDetached, pip.Name, oldW.FromRepository)
+	}
+
+	// reset fromRepository for all app using it
+	apps, err := application.LoadAllNamesByFromRepository(db, projectID, oldW.FromRepository)
+	if err != nil {
+		return err
+	}
+
+	if err := application.ResetFromRepository(db, projectID, oldW.FromRepository); err != nil {
+		return sdk.WrapError(err, "could not reset fromRepository %s from applications", oldW.FromRepository)
+	}
+
+	for _, app := range apps {
+		msgChan <- sdk.NewMessage(sdk.MsgApplicationDetached, app.Name, oldW.FromRepository)
+	}
+
+	// reset fromRepository for all env using it
+	envs, err := environment.LoadAllNamesByFromRepository(db, projectID, oldW.FromRepository)
+	if err != nil {
+		return err
+	}
+
+	if err := environment.ResetFromRepository(db, projectID, oldW.FromRepository); err != nil {
+		return sdk.WrapError(err, "could not reset fromRepository %s from environments", oldW.FromRepository)
+	}
+
+	for _, env := range envs {
+		msgChan <- sdk.NewMessage(sdk.MsgEnvironmentDetached, env.Name, oldW.FromRepository)
+	}
+
+	return nil
+}
+
 func importWorkflowGroups(db gorp.SqlExecutor, w *sdk.Workflow) error {
 	if len(w.Groups) > 0 {
 		if err := group.DeleteAllWorkflowGroups(db, w.ID); err != nil {
diff --git a/engine/api/workflow_application.go b/engine/api/workflow_application.go
index 675936c00..f00c5c7ea 100644
--- a/engine/api/workflow_application.go
+++ b/engine/api/workflow_application.go
@@ -64,7 +64,7 @@ func (api *API) releaseApplicationWorkflowHandler() service.Handler {
 		app := workflowRun.Workflow.Applications[node.Context.ApplicationID]
 
 		if app.VCSServer == "" {
-			return sdk.WithStack(sdk.ErrNoReposManager)
+			return sdk.NewErrorFrom(sdk.ErrNoReposManager, "app.VCSServer is empty")
 		}
 
 		tx, err := api.mustDB().Begin()
diff --git a/engine/api/workflow_import.go b/engine/api/workflow_import.go
index 38ce57306..ef8109531 100644
--- a/engine/api/workflow_import.go
+++ b/engine/api/workflow_import.go
@@ -138,10 +138,10 @@ func (api *API) postWorkflowImportHandler() service.Handler {
 		}
 
 		wrkflw, msgList, globalError := workflow.ParseAndImport(ctx, tx, api.Cache, *proj, wf, ew, getAPIConsumer(ctx), workflow.ImportOptions{Force: force})
-		msgListString := translate(r, msgList)
+		msgListString := translate(msgList)
 		if globalError != nil {
 			if len(msgListString) != 0 {
-				sdkErr := sdk.ExtractHTTPError(globalError, r.Header.Get("Accept-Language"))
+				sdkErr := sdk.ExtractHTTPError(globalError)
 				return service.WriteJSON(w, append(msgListString, sdkErr.Message), sdkErr.Status)
 			}
 			return sdk.WrapError(globalError, "Unable to import workflow %s", ew.GetName())
@@ -222,10 +222,10 @@ func (api *API) putWorkflowImportHandler() service.Handler {
 		defer tx.Rollback() //nolint
 
 		wrkflw, msgList, globalError := workflow.ParseAndImport(ctx, tx, api.Cache, *proj, wf, ew, u, workflow.ImportOptions{Force: true, WorkflowName: wfName})
-		msgListString := translate(r, msgList)
+		msgListString := translate(msgList)
 		if globalError != nil {
 			if len(msgListString) != 0 {
-				sdkErr := sdk.ExtractHTTPError(globalError, r.Header.Get("Accept-Language"))
+				sdkErr := sdk.ExtractHTTPError(globalError)
 				return service.WriteJSON(w, append(msgListString, sdkErr.Error()), sdkErr.Status)
 			}
 			return sdk.WrapError(globalError, "unable to import workflow %s", ew.GetName())
@@ -325,7 +325,7 @@ func (api *API) postWorkflowPushHandler() service.Handler {
 			return err
 		}
 
-		msgListString := translate(r, allMsg)
+		msgListString := translate(allMsg)
 
 		if wrkflw != nil {
 			w.Header().Add(sdk.ResponseWorkflowIDHeader, fmt.Sprintf("%d", wrkflw.ID))
diff --git a/engine/api/workflow_queue.go b/engine/api/workflow_queue.go
index d1f851cbe..c6c40d20c 100644
--- a/engine/api/workflow_queue.go
+++ b/engine/api/workflow_queue.go
@@ -716,7 +716,7 @@ func (api *API) postWorkflowJobStepStatusHandler() service.Handler {
 			log.Warning(ctx, "postWorkflowJobStepStatusHandler> Unable to load workflow run for event: %v", err)
 			return nil
 		}
-		nodeRun.Translate(r.Header.Get("Accept-Language"))
+		nodeRun.Translate()
 		eventsNotifs := notification.GetUserWorkflowEvents(ctx, api.mustDB(), api.Cache, wr.Workflow.ProjectID, wr.Workflow.ProjectKey, work.Name, wr.Workflow.Notifications, nil, nodeRun)
 		event.PublishWorkflowNodeRun(context.Background(), nodeRun, wr.Workflow, eventsNotifs)
 		return nil
diff --git a/engine/api/workflow_run.go b/engine/api/workflow_run.go
index 13dbe5d36..9f81a4d61 100644
--- a/engine/api/workflow_run.go
+++ b/engine/api/workflow_run.go
@@ -47,7 +47,7 @@ func (api *API) getWorkflowsRunsAndNodesIDshandler() service.Handler {
 			return err
 		}
 
-		ids, err := workflow.LoadNodeRunIDs(api.mustDB(), p.WorkflowNames.IDs())
+		ids, err := workflow.LoadNodeRunIDs(api.mustDB(), p.WorkflowNames.IDs(), []string{sdk.StatusFail, sdk.StatusStopped, sdk.StatusSuccess})
 		if err != nil {
 			return err
 		}
@@ -277,7 +277,7 @@ func (api *API) getLatestWorkflowRunHandler() service.Handler {
 		if err != nil {
 			return sdk.WrapError(err, "Unable to load last workflow run")
 		}
-		run.Translate(r.Header.Get("Accept-Language"))
+		run.Translate()
 		return service.WriteJSON(w, run, http.StatusOK)
 	}
 }
@@ -304,7 +304,6 @@ func (api *API) getWorkflowRunHandler() service.Handler {
 				WithArtifacts:           true,
 				WithLightTests:          true,
 				DisableDetailledNodeRun: !isService && withDetailledNodeRun != "true",
-				Language:                r.Header.Get("Accept-Language"),
 			},
 		)
 		if err != nil {
@@ -325,7 +324,7 @@ func (api *API) getWorkflowRunHandler() service.Handler {
 			}
 		}
 
-		run.Translate(r.Header.Get("Accept-Language"))
+		run.Translate()
 
 		return service.WriteJSON(w, run, http.StatusOK)
 	}
@@ -806,7 +805,7 @@ func (api *API) getWorkflowNodeRunHandler() service.Handler {
 			return sdk.WrapError(err, "Unable to load last workflow run")
 		}
 
-		run.Translate(r.Header.Get("Accept-Language"))
+		run.Translate()
 		return service.WriteJSON(w, run, http.StatusOK)
 	}
 }
@@ -1275,7 +1274,7 @@ func failInitWorkflowRun(ctx context.Context, db *gorp.DbMap, wfRun *sdk.Workflo
 			wfRun.Status = sdk.StatusNeverBuilt
 		}
 	} else {
-		httpErr := sdk.ExtractHTTPError(err, "")
+		httpErr := sdk.ExtractHTTPError(err)
 		isErrWithStack := sdk.IsErrorWithStack(err)
 		fields := log.Fields{}
 		if isErrWithStack {
@@ -1471,11 +1470,10 @@ func (api *API) getWorkflowNodeRunJobSpawnInfosHandler() service.Handler {
 			return sdk.WrapError(err, "cannot load spawn infos for node run job id %d", runJobID)
 		}
 
-		l := r.Header.Get("Accept-Language")
 		for ki, info := range spawnInfos {
 			if _, ok := sdk.Messages[info.Message.ID]; ok {
 				m := sdk.NewMessage(sdk.Messages[info.Message.ID], info.Message.Args...)
-				spawnInfos[ki].UserMessage = m.String(l)
+				spawnInfos[ki].UserMessage = m.String()
 			}
 		}
 		return service.WriteJSON(w, spawnInfos, http.StatusOK)
diff --git a/engine/api/workflow_run_log.go b/engine/api/workflow_run_log.go
index ada4ca21a..dbab84fbc 100644
--- a/engine/api/workflow_run_log.go
+++ b/engine/api/workflow_run_log.go
@@ -114,12 +114,117 @@ func (api *API) getWorkflowNodeRunJobServiceLinkHandler() service.Handler {
 	}
 }
 
+func (api *API) getWorkflowNodeRunJobStepLinksHandler() service.Handler {
+	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+		return api.getWorkflowNodeRunJobLogLinksHandler(ctx, w, r, sdk.CDNTypeItemStepLog)
+	}
+}
+
 func (api *API) getWorkflowNodeRunJobStepLinkHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 		return api.getWorkflowNodeRunJobLogLinkHandler(ctx, w, r, sdk.CDNTypeItemStepLog)
 	}
 }
 
+func (api *API) getWorkflowNodeRunJobLogLinksHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, itemType sdk.CDNItemType) error {
+	vars := mux.Vars(r)
+
+	projectKey := vars["key"]
+	enabled := featureflipping.IsEnabled(ctx, gorpmapping.Mapper, api.mustDB(), "cdn-job-logs", map[string]string{
+		"project_key": projectKey,
+	})
+	if !enabled {
+		return sdk.NewErrorFrom(sdk.ErrNotFound, "cdn is not enable for project %s", projectKey)
+	}
+
+	workflowName := vars["permWorkflowName"]
+	nodeRunID, err := requestVarInt(r, "nodeRunID")
+	if err != nil {
+		return sdk.NewErrorFrom(err, "invalid node run id")
+	}
+	runJobID, err := requestVarInt(r, "runJobID")
+	if err != nil {
+		return sdk.NewErrorFrom(err, "invalid node job id")
+	}
+
+	httpURL, err := services.GetCDNPublicHTTPAdress(ctx, api.mustDB())
+	if err != nil {
+		return err
+	}
+
+	nodeRun, err := workflow.LoadNodeRun(api.mustDB(), projectKey, workflowName, nodeRunID, workflow.LoadRunOptions{DisableDetailledNodeRun: true})
+	if err != nil {
+		return sdk.WrapError(err, "cannot find nodeRun %d for workflow %s in project %s", nodeRunID, workflowName, projectKey)
+	}
+	var runJob *sdk.WorkflowNodeJobRun
+	for _, s := range nodeRun.Stages {
+		for _, rj := range s.RunJobs {
+			if rj.ID == runJobID {
+				runJob = &rj
+				break
+			}
+		}
+		if runJob != nil {
+			break
+		}
+	}
+	if runJob == nil {
+		return sdk.NewErrorFrom(sdk.ErrNotFound, "cannot find run job for id %d", runJobID)
+	}
+
+	refs := make([]sdk.CDNLogAPIRef, 0)
+	apiRef := sdk.CDNLogAPIRef{
+		ProjectKey:     projectKey,
+		WorkflowName:   workflowName,
+		WorkflowID:     nodeRun.WorkflowID,
+		RunID:          nodeRun.WorkflowRunID,
+		NodeRunName:    nodeRun.WorkflowNodeName,
+		NodeRunID:      nodeRun.ID,
+		NodeRunJobName: runJob.Job.Action.Name,
+		NodeRunJobID:   runJob.ID,
+	}
+
+	if itemType == sdk.CDNTypeItemServiceLog {
+		for _, r := range runJob.Job.Action.Requirements {
+			ref := apiRef
+			ref.RequirementServiceID = r.ID
+			ref.RequirementServiceName = r.Name
+			refs = append(refs, ref)
+		}
+	} else {
+		for _, s := range runJob.Job.StepStatus {
+			ref := apiRef
+			ref.StepName = runJob.Job.Action.Actions[int64(s.StepOrder)].Name
+			if runJob.Job.Action.Actions[int64(s.StepOrder)].StepName != "" {
+				ref.StepName = runJob.Job.Action.Actions[int64(s.StepOrder)].StepName
+			}
+			ref.StepOrder = int64(s.StepOrder)
+			refs = append(refs, ref)
+		}
+	}
+
+	datas := make([]sdk.CDNLogLinkData, 0, len(refs))
+
+	for _, r := range refs {
+		apiRefHashU, err := hashstructure.Hash(r, nil)
+		if err != nil {
+			return sdk.WithStack(err)
+		}
+		apiRefHash := strconv.FormatUint(apiRefHashU, 10)
+		datas = append(datas, sdk.CDNLogLinkData{
+			APIRef:        apiRefHash,
+			StepOrder:     r.StepOrder,
+			RequirementID: r.RequirementServiceID,
+		})
+	}
+
+	return service.WriteJSON(w, sdk.CDNLogLinks{
+		CDNURL:   httpURL,
+		ItemType: itemType,
+		Data:     datas,
+	}, http.StatusOK)
+}
+
 func (api *API) getWorkflowNodeRunJobLogLinkHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, itemType sdk.CDNItemType) error {
 	vars := mux.Vars(r)
 
diff --git a/engine/cdn/cdn_item.go b/engine/cdn/cdn_item.go
index 2705a4943..5d5ece4b0 100644
--- a/engine/cdn/cdn_item.go
+++ b/engine/cdn/cdn_item.go
@@ -124,6 +124,37 @@ type getItemLogOptions struct {
 	cacheSource string
 }
 
+func (s *Service) getItemLogLinesCount(ctx context.Context, t sdk.CDNItemType, apiRefHash string) (int64, error) {
+	it, err := item.LoadByAPIRefHashAndType(ctx, s.Mapper, s.mustDBWithCtx(ctx), apiRefHash, t)
+	if err != nil {
+		return 0, err
+	}
+
+	itemUnit, err := storage.LoadItemUnitByUnit(ctx, s.Mapper, s.mustDBWithCtx(ctx), s.Units.Buffer.ID(), it.ID)
+	if err != nil && !sdk.ErrorIs(err, sdk.ErrNotFound) {
+		return 0, err
+	}
+
+	// If item is in Buffer, get from it
+	if itemUnit != nil {
+		log.Debug("getItemLogLines> Getting logs from buffer")
+		lines, err := s.Units.Buffer.Card(*itemUnit)
+		return int64(lines), err
+	}
+
+	// Get from cache
+	if ok, _ := s.LogCache.Exist(it.ID); !ok {
+		log.Debug("getItemLogLines> Getting logs from storage and push to cache")
+		// Retrieve item and push it into the cache
+		if err := s.pushItemLogIntoCache(ctx, *it, ""); err != nil {
+			return 0, err
+		}
+	}
+
+	linesCount, err := s.LogCache.Card(it.ID)
+	return int64(linesCount), err
+}
+
 func (s *Service) getItemLogValue(ctx context.Context, t sdk.CDNItemType, apiRefHash string, opts getItemLogOptions) (*sdk.CDNItem, int64, io.ReadCloser, string, error) {
 	it, err := item.LoadByAPIRefHashAndType(ctx, s.Mapper, s.mustDBWithCtx(ctx), apiRefHash, t)
 	if err != nil {
diff --git a/engine/cdn/cdn_log_engine.go b/engine/cdn/cdn_log_engine.go
index 0bdbd7372..52f159a43 100644
--- a/engine/cdn/cdn_log_engine.go
+++ b/engine/cdn/cdn_log_engine.go
@@ -46,7 +46,7 @@ func (s *Service) waitingJobs(ctx context.Context) {
 				keyParts := strings.Split(k, ":")
 				queueIdentifier := keyParts[len(keyParts)-1]
 
-				jobQueueKey, err := s.canDequeue(queueIdentifier)
+				jobQueueKey, err := s.canDequeue(ctx, queueIdentifier)
 				if err != nil {
 					err = sdk.WrapError(err, "unable to check canDequeue %s", jobQueueKey)
 					log.ErrorWithFields(ctx, log.Fields{"stack_trace": fmt.Sprintf("%+v", err)}, "%s", err)
@@ -69,13 +69,13 @@ func (s *Service) waitingJobs(ctx context.Context) {
 
 // Run dequeue of a job log
 func (s *Service) dequeueMessages(ctx context.Context, jobLogsQueueKey string, queueIdentifier string) error {
-	log.Info(ctx, "dequeueJobMessages: Dequeue %s", jobLogsQueueKey)
+	log.Info(ctx, "dequeueJobMessages: %s BEGIN dequeue", jobLogsQueueKey)
 	var t0 = time.Now()
 	var t1 = time.Now()
 	var nbMessages int
 	defer func() {
 		delta := t1.Sub(t0)
-		log.Info(ctx, "dequeueJobMessages: processLogs[%s] - %d messages received in %v", jobLogsQueueKey, nbMessages, delta)
+		log.Info(ctx, "dequeueJobMessages: %s END dequeue - %d messages received in %v", jobLogsQueueKey, nbMessages, delta)
 	}()
 
 	defer func() {
@@ -150,9 +150,9 @@ func (s *Service) dequeueMessages(ctx context.Context, jobLogsQueueKey string, q
 }
 
 // Return queue name if jobID need to be dequeue or empty
-func (s *Service) canDequeue(jobID string) (string, error) {
+func (s *Service) canDequeue(ctx context.Context, jobID string) (string, error) {
 	jobQueueKey := cache.Key(keyJobLogQueue, jobID)
-	heatbeatKey := cache.Key(keyJobHearbeat, jobID)
+	heartbeatKey := cache.Key(keyJobHearbeat, jobID)
 
 	// Take a lock
 	lockKey := cache.Key(keyJobLock, jobID)
@@ -167,7 +167,7 @@ func (s *Service) canDequeue(jobID string) (string, error) {
 		return "", nil
 	}
 
-	exist, err := s.Cache.Exist(heatbeatKey)
+	exist, err := s.Cache.Exist(heartbeatKey)
 	if err != nil {
 		return "", err
 	}
@@ -177,7 +177,7 @@ func (s *Service) canDequeue(jobID string) (string, error) {
 	}
 
 	//hearbeat
-	heartbeatKey := cache.Key(keyJobHearbeat, jobID)
+	log.Info(ctx, "heartbeat: take job %s", jobQueueKey)
 	if err := s.Cache.SetWithTTL(heartbeatKey, true, 30); err != nil {
 		return "", err
 	}
diff --git a/engine/cdn/cdn_router.go b/engine/cdn/cdn_router.go
index 355f3a34f..848ed96c1 100644
--- a/engine/cdn/cdn_router.go
+++ b/engine/cdn/cdn_router.go
@@ -27,6 +27,7 @@ func (s *Service) initRouter(ctx context.Context) {
 	r.Handle("/bulk/item/delete", nil, r.POST(s.bulkDeleteItemsHandler))
 
 	r.Handle("/item/stream", nil, r.GET(s.getItemLogsStreamHandler, service.OverrideAuth(s.validJWTMiddleware)))
+	r.Handle("/item/{type}/lines", nil, r.GET(s.getItemsAllLogsLinesHandler, service.OverrideAuth(s.validJWTMiddleware)))
 	r.Handle("/item/{type}/{apiRef}", nil, r.GET(s.getItemHandler, service.OverrideAuth(s.itemAccessMiddleware)), r.DELETE(s.deleteItemHandler))
 	r.Handle("/item/{type}/{apiRef}/checksync", nil, r.GET(s.getItemCheckSyncHandler, service.OverrideAuth(s.itemAccessMiddleware)))
 	r.Handle("/item/{type}/{apiRef}/download", nil, r.GET(s.getItemDownloadHandler, service.OverrideAuth(s.itemAccessMiddleware)))
diff --git a/engine/cdn/item_logs_handler.go b/engine/cdn/item_logs_handler.go
index 68f894aea..3215eaf1a 100644
--- a/engine/cdn/item_logs_handler.go
+++ b/engine/cdn/item_logs_handler.go
@@ -155,6 +155,31 @@ func (s *Service) sendLogsToWSClient(ctx context.Context, wsClient websocket.Cli
 	return nil
 }
 
+func (s *Service) getItemsAllLogsLinesHandler() service.Handler {
+	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+		vars := mux.Vars(r)
+		itemType := sdk.CDNItemType(vars["type"])
+		if !itemType.IsLog() {
+			return sdk.NewErrorFrom(sdk.ErrWrongRequest, "invalid item log type")
+		}
+		refsHash := r.URL.Query()["apiRefHash"]
+
+		resp := make([]sdk.CDNLogsLines, 0)
+
+		for _, hash := range refsHash {
+			linesCount, err := s.getItemLogLinesCount(ctx, itemType, hash)
+			if err != nil && !sdk.ErrorIs(err, sdk.ErrNotFound) {
+				return err
+			}
+			if err != nil && sdk.ErrorIs(err, sdk.ErrNotFound) {
+				break
+			}
+			resp = append(resp, sdk.CDNLogsLines{APIRef: hash, LinesCount: linesCount})
+		}
+		return service.WriteJSON(w, resp, http.StatusOK)
+	}
+}
+
 func (s *Service) getItemLogsLinesHandler() service.Handler {
 	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
 		vars := mux.Vars(r)
diff --git a/engine/cdn/redis/reader.go b/engine/cdn/redis/reader.go
index 23a0d21d7..f3d728a5a 100644
--- a/engine/cdn/redis/reader.go
+++ b/engine/cdn/redis/reader.go
@@ -163,6 +163,10 @@ func (r *Reader) loadMoreLines() error {
 }
 
 func (r *Reader) Read(p []byte) (n int, err error) {
+	if r.readEOF {
+		return 0, io.EOF
+	}
+
 	lengthToRead := len(p)
 
 	// If we don't have enough bytes in current buffer we will load some line from Redis
diff --git a/engine/cdn/redis/writer.go b/engine/cdn/redis/writer.go
index f502f0756..cda10e63d 100644
--- a/engine/cdn/redis/writer.go
+++ b/engine/cdn/redis/writer.go
@@ -2,6 +2,7 @@ package redis
 
 import (
 	"context"
+	"fmt"
 	"io"
 	"strconv"
 	"strings"
@@ -17,6 +18,7 @@ type Writer struct {
 	PrefixKey     string
 	currentScore  uint
 	currentBuffer []byte
+	closed        bool
 }
 
 // Add new item in cache + update last usage
@@ -30,6 +32,10 @@ func (w *Writer) add(score uint, value string) error {
 }
 
 func (w *Writer) Write(p []byte) (int, error) {
+	if w.closed {
+		return 0, fmt.Errorf("writer is closed")
+	}
+
 	// Append cnew buffer to current one
 	w.currentBuffer = append(w.currentBuffer, p...)
 
@@ -56,6 +62,7 @@ func (w *Writer) Write(p []byte) (int, error) {
 
 // Close will write the end of the buffer to store in case the last line is not ended by \n
 func (w *Writer) Close() error {
+	w.closed = true
 	if len(w.currentBuffer) > 0 {
 		if err := w.add(w.currentScore, string(w.currentBuffer)+"\n"); err != nil {
 			return err
diff --git a/engine/hatchery/swarm/swarm_util_create.go b/engine/hatchery/swarm/swarm_util_create.go
index e45662ee5..bb8a48832 100644
--- a/engine/hatchery/swarm/swarm_util_create.go
+++ b/engine/hatchery/swarm/swarm_util_create.go
@@ -136,7 +136,7 @@ checkImage:
 			next()
 			hatchery.SendSpawnInfo(ctx, h, spawnArgs.JobID, sdk.SpawnMsg{
 				ID:   sdk.MsgSpawnInfoHatcheryEndDockerPullErr.ID,
-				Args: []interface{}{h.Name(), cArgs.image, sdk.ExtractHTTPError(err, "").Error()},
+				Args: []interface{}{h.Name(), cArgs.image, sdk.ExtractHTTPError(err).Error()},
 			})
 			return sdk.WrapError(err, "Unable to pull image %s on %s", cArgs.image, dockerClient.name)
 		}
diff --git a/engine/repositories/fs.go b/engine/repositories/fs.go
index 231b34ad4..3b04864f9 100644
--- a/engine/repositories/fs.go
+++ b/engine/repositories/fs.go
@@ -13,7 +13,7 @@ import (
 func (s *Service) checkOrCreateRootFS() error {
 	fi, err := os.Stat(s.Cfg.Basedir)
 	if os.IsNotExist(err) {
-		return os.MkdirAll(s.Cfg.Basedir, os.FileMode(0700))
+		return sdk.WrapError(os.MkdirAll(s.Cfg.Basedir, os.FileMode(0700)), "unable to create directory %q", s.Cfg.Basedir)
 	}
 	if fi.IsDir() {
 		return nil
@@ -28,7 +28,7 @@ func (s *Service) checkOrCreateFS(r *sdk.OperationRepo) error {
 	path := filepath.Join(s.Cfg.Basedir, r.ID())
 	fi, err := os.Stat(path)
 	if os.IsNotExist(err) {
-		return sdk.WrapError(os.MkdirAll(path, os.FileMode(0700)), "unable to create directory %s", path)
+		return sdk.WrapError(os.MkdirAll(path, os.FileMode(0700)), "unable to create directory %q", path)
 	}
 	if fi.IsDir() {
 		return nil
diff --git a/engine/service/http.go b/engine/service/http.go
index d65136426..6c0f7171e 100644
--- a/engine/service/http.go
+++ b/engine/service/http.go
@@ -110,8 +110,7 @@ type ErrorResponse struct {
 
 // WriteError is a helper function to return error in a language the called understand
 func WriteError(ctx context.Context, w http.ResponseWriter, r *http.Request, err error) {
-	al := r.Header.Get("Accept-Language")
-	httpErr := sdk.ExtractHTTPError(err, al)
+	httpErr := sdk.ExtractHTTPError(err)
 	isErrWithStack := sdk.IsErrorWithStack(err)
 
 	fields := log.Fields{}
diff --git a/engine/worker/cmd_cds_version_set.go b/engine/worker/cmd_cds_version_set.go
index 10ba25a18..cfc6982a3 100644
--- a/engine/worker/cmd_cds_version_set.go
+++ b/engine/worker/cmd_cds_version_set.go
@@ -85,7 +85,7 @@ func cdsVersionSetCmd() func(cmd *cobra.Command, args []string) {
 
 		if err := f(); err != nil {
 			if sdk.IsErrorWithStack(err) {
-				httpErr := sdk.ExtractHTTPError(err, "")
+				httpErr := sdk.ExtractHTTPError(err)
 				sdk.Exit("%v", httpErr.Error())
 			} else {
 				sdk.Exit("%v", err)
diff --git a/engine/worker/cmd_run.go b/engine/worker/cmd_run.go
index d2670e157..968df776a 100644
--- a/engine/worker/cmd_run.go
+++ b/engine/worker/cmd_run.go
@@ -68,7 +68,7 @@ func runCmd() func(cmd *cobra.Command, args []string) {
 			fields := log.Fields{}
 			if isErrWithStack {
 				fields["stack_trace"] = fmt.Sprintf("%+v", err)
-				fields["request_id"] = sdk.ExtractHTTPError(err, "").RequestID
+				fields["request_id"] = sdk.ExtractHTTPError(err).RequestID
 			}
 			log.ErrorWithFields(ctx, fields, "%v", err)
 			time.Sleep(2 * time.Second)
diff --git a/engine/worker/internal/http_server.go b/engine/worker/internal/http_server.go
index bd10e7b5f..35f5057fe 100644
--- a/engine/worker/internal/http_server.go
+++ b/engine/worker/internal/http_server.go
@@ -99,7 +99,6 @@ func writeJSON(w http.ResponseWriter, data interface{}, status int) {
 }
 
 func writeError(w http.ResponseWriter, r *http.Request, err error) {
-	al := r.Header.Get("Accept-Language")
-	sdkErr := sdk.ExtractHTTPError(err, al)
+	sdkErr := sdk.ExtractHTTPError(err)
 	writeJSON(w, sdkErr, sdkErr.Status)
 }
diff --git a/sdk/cdn.go b/sdk/cdn.go
index dbffd807b..5defbd5ad 100644
--- a/sdk/cdn.go
+++ b/sdk/cdn.go
@@ -43,6 +43,23 @@ type CDNUnit struct {
 	Config  ServiceConfig `json:"config" db:"config"`
 }
 
+type CDNLogsLines struct {
+	APIRef     string `json:"api_ref"`
+	LinesCount int64  `json:"lines_count"`
+}
+
+type CDNLogLinks struct {
+	CDNURL   string           `json:"cdn_url,omitempty"`
+	ItemType CDNItemType      `json:"item_type"`
+	Data     []CDNLogLinkData `json:"datas"`
+}
+
+type CDNLogLinkData struct {
+	APIRef        string `json:"api_ref"`
+	StepOrder     int64  `json:"step_order"`
+	RequirementID int64  `json:"requirement_id,omitempty"`
+}
+
 type CDNLogLink struct {
 	CDNURL   string      `json:"cdn_url,omitempty"`
 	ItemType CDNItemType `json:"item_type"`
diff --git a/sdk/error.go b/sdk/error.go
index c81d06179..7ff54162e 100644
--- a/sdk/error.go
+++ b/sdk/error.go
@@ -10,7 +10,6 @@ import (
 	"strings"
 
 	"github.com/pkg/errors"
-	"golang.org/x/text/language"
 )
 
 // Existing CDS errors
@@ -386,189 +385,6 @@ var errorsAmericanEnglish = map[int]string{
 	ErrWebsocketUpgrade.ID:                              "Websocket upgrade required",
 }
 
-var errorsFrench = map[int]string{
-	ErrUnknownError.ID:                                  "erreur interne",
-	ErrActionAlreadyUpdated.ID:                          "le status de l'action a déjà été mis à jour",
-	ErrNoAction.ID:                                      "l'action n'existe pas",
-	ErrActionLoop.ID:                                    "la définition de l'action contient une boucle récursive",
-	ErrInvalidID.ID:                                     "l'ID doit être un nombre entier",
-	ErrInvalidProject.ID:                                "projet manquant",
-	ErrInvalidProjectKey.ID:                             "la clef de project doit uniquement contenir des lettres majuscules et des chiffres",
-	ErrProjectHasPipeline.ID:                            "le project contient un pipeline",
-	ErrProjectHasApplication.ID:                         "le project contient une application",
-	ErrUnauthorized.ID:                                  "authentification invalide",
-	ErrForbidden.ID:                                     "accès refusé",
-	ErrPipelineNotFound.ID:                              "le pipeline n'existe pas",
-	ErrPipelineNotAttached.ID:                           "le pipeline n'est pas lié à l'application",
-	ErrNoEnvironmentProvided.ID:                         "les pipelines de déploiement et de tests requièrent un environnement",
-	ErrEnvironmentProvided.ID:                           "une pipeline de build ne nécessite pas d'environnement",
-	ErrUnknownEnv.ID:                                    "environnement inconnu",
-	ErrEnvironmentExist.ID:                              "l'environnement existe",
-	ErrNoPipelineBuild.ID:                               "ce build n'existe pas",
-	ErrInvalidUsername.ID:                               "nom d'utilisateur invalide",
-	ErrUsernamePresent.ID:                               "le nom d'utilisateur est déjà présent",
-	ErrInvalidEmail.ID:                                  "addresse email invalide",
-	ErrGroupPresent.ID:                                  "le groupe est déjà présent",
-	ErrInvalidName.ID:                                   "le nom est invalide",
-	ErrInvalidUser.ID:                                   "mauvaise combinaison compte/mot de passe utilisateur",
-	ErrBuildArchived.ID:                                 "impossible de relancer ce build car il a été archivé",
-	ErrNoEnvironment.ID:                                 "l'environement n'existe pas",
-	ErrModelNameExist.ID:                                "le nom du modèle de worker est déjà utilisé",
-	ErrNoProject.ID:                                     "le projet n'existe pas",
-	ErrVariableExists.ID:                                "la variable existe déjà",
-	ErrInvalidGroupPattern.ID:                           "nom de groupe invalide '^[a-zA-Z0-9.-_-]{1,}$'",
-	ErrGroupExists.ID:                                   "le groupe existe déjà",
-	ErrNotEnoughAdmin.ID:                                "pas assez d'admin restant",
-	ErrInvalidProjectName.ID:                            "nom de project vide non autorisé",
-	ErrInvalidApplicationPattern.ID:                     "nom de l'application invalide '^[a-zA-Z0-9.-_-]{1,}$'",
-	ErrInvalidWorkerModelNamePattern.ID:                 "nom du worker model invalide '^[a-zA-Z0-9.-_-]{1,}$'",
-	ErrInvalidPipelinePattern.ID:                        "nom du pipeline invalide '^[a-zA-Z0-9.-_-]{1,}$'",
-	ErrNotFound.ID:                                      "la ressource n'existe pas",
-	ErrNoHook.ID:                                        "le hook n'existe pas",
-	ErrNoAttachedPipeline.ID:                            "le pipeline n'est pas lié à l'application",
-	ErrNoReposManager.ID:                                "le gestionnaire de dépôt n'existe pas",
-	ErrNoReposManagerAuth.ID:                            "connexion de CDS au gestionnaire de dépôt refusée, merci de contacter l'administrateur",
-	ErrNoReposManagerClientAuth.ID:                      "connexion au gestionnaire de dépôts refusée, merci de détacher et ré-attacher le repository manager sur votre projet CDS",
-	ErrRepoNotFound.ID:                                  "le dépôt n'existe pas",
-	ErrSecretStoreUnreachable.ID:                        "impossible de contacter vault",
-	ErrSecretKeyFetchFailed.ID:                          "erreur pendnat la récuperation de la clef de chiffrement",
-	ErrCommitsFetchFailed.ID:                            "impossible de retrouver les changements",
-	ErrInvalidGoPath.ID:                                 "le gopath n'est pas valide",
-	ErrInvalidSecretFormat.ID:                           "impossibe de dechiffrer le secret, format invalide",
-	ErrSessionNotFound.ID:                               "session invalide",
-	ErrNoPreviousSuccess.ID:                             "il n'y a aucune précédente version en succès pour ce pipeline",
-	ErrNoPermExecution.ID:                               "vous n'avez pas les droits d'éxécution",
-	ErrInvalidSecretValue.ID:                            "valeur du secret non specifiée",
-	ErrPipelineHasApplication.ID:                        "le pipeline est utilisé par une application",
-	ErrNoDirectSecretUse.ID:                             "l'utilisation du type de paramêtre 'password' est impossible",
-	ErrNoBranch.ID:                                      "la branche est introuvable dans le dépôt",
-	ErrLDAPConn.ID:                                      "erreur de connexion au serveur LDAP",
-	ErrServiceUnavailable.ID:                            "service temporairement indisponible ou en maintenance",
-	ErrParseUserNotification.ID:                         "notification non reconnue",
-	ErrNotSupportedUserNotification.ID:                  "notification non supportée",
-	ErrGroupNeedAdmin.ID:                                "il faut au moins 1 administrateur",
-	ErrGroupNeedWrite.ID:                                "il faut au moins 1 groupe avec les droits d'écriture",
-	ErrNoVariable.ID:                                    "la variable n'existe pas",
-	ErrPluginInvalid.ID:                                 "plugin non valide",
-	ErrApplicationExist.ID:                              "une application du même nom existe déjà",
-	ErrBranchNameNotProvided.ID:                         "le paramètre git.branch ou git.tag est obligatoire",
-	ErrInfiniteTriggerLoop.ID:                           "création d'une boucle de trigger infinie interdite",
-	ErrInvalidResetUser.ID:                              "mauvaise combinaison compte/mail utilisateur",
-	ErrUserConflict.ID:                                  "cet utilisateur existe deja",
-	ErrWrongRequest.ID:                                  "la requête est incorrecte",
-	ErrAlreadyExist.ID:                                  "conflit",
-	ErrInvalidType.ID:                                   "type non valide",
-	ErrParentApplicationAndPipelineMandatory.ID:         "application et pipeline parents obligatoires",
-	ErrNoParentBuildFound.ID:                            "aucun build parent n'a pu être trouvé",
-	ErrParameterExists.ID:                               "le paramètre existe déjà",
-	ErrNoHatchery.ID:                                    "La hatchery n'existe pas",
-	ErrInvalidWorkerStatus.ID:                           "Le status du worker est incorrect",
-	ErrInvalidToken.ID:                                  "Token non valide",
-	ErrAppBuildingPipelines.ID:                          "Impossible de supprimer l'application, il y a pipelines en cours",
-	ErrInvalidTimezone.ID:                               "Fuseau horaire invalide",
-	ErrEnvironmentCannotBeDeleted.ID:                    "L'environement ne peut etre supprimé. Il est encore utilisé.",
-	ErrInvalidPipeline.ID:                               "Pipeline invalide",
-	ErrKeyNotFound.ID:                                   "Clé introuvable",
-	ErrPipelineAlreadyExists.ID:                         "Le pipeline existe déjà",
-	ErrJobAlreadyBooked.ID:                              "Le job est déjà réservé",
-	ErrPipelineBuildNotFound.ID:                         "Le pipeline build n'a pu être trouvé",
-	ErrAlreadyTaken.ID:                                  "Ce job est déjà en cours de traitement par un autre worker",
-	ErrWorkflowNodeNotFound.ID:                          "Noeud de Workflow introuvable",
-	ErrWorkflowInvalidRoot.ID:                           "Racine de Workflow invalide",
-	ErrWorkflowNodeRef.ID:                               "Référence de noeud de workflow invalide",
-	ErrWorkflowInvalid.ID:                               "Workflow invalide",
-	ErrWorkflowNodeJoinNotFound.ID:                      "Jointure introuvable",
-	ErrInvalidJobRequirement.ID:                         "Pré-requis de Job invalide",
-	ErrNotImplemented.ID:                                "La fonctionnalité n'est pas implémentée",
-	ErrParameterNotExists.ID:                            "Ce paramètre n'existe pas",
-	ErrUnknownKeyType.ID:                                "Le type de clé n'est pas connu",
-	ErrInvalidKeyPattern.ID:                             "le nom de la clé doit respecter le pattern suivant; '^[a-zA-Z0-9.-_-]{1,}$'",
-	ErrWebhookConfigDoesNotMatch.ID:                     "la configuration du webhook ne correspond pas",
-	ErrPipelineUsedByWorkflow.ID:                        "Le pipeline est utilisé par un workflow",
-	ErrMethodNotAllowed.ID:                              "La méthode n'est pas autorisée",
-	ErrInvalidNodeNamePattern.ID:                        "Le nom du noeud du workflow doit respecter le pattern suivant; '^[a-zA-Z0-9.-_-]{1,}$'",
-	ErrWorkflowNodeParentNotRun.ID:                      "Il est interdit de lancer un noeuds si ses parents n'ont jamais été lancés",
-	ErrDefaultGroupPermission.ID:                        "Le groupe par défaut ne peut être utilisé qu'en lecture seule",
-	ErrLastGroupWithWriteRole.ID:                        "Le dernier groupe doit avoir les droits d'écriture",
-	ErrInvalidEmailDomain.ID:                            "Domaine invalide",
-	ErrWorkflowNodeRunJobNotFound.ID:                    "Job non trouvé",
-	ErrBuiltinKeyNotFound.ID:                            "Clé de chiffrage introuvable",
-	ErrStepNotFound.ID:                                  "Step introuvable",
-	ErrWorkerModelAlreadyBooked.ID:                      "Le modèle de worker est déjà réservé",
-	ErrConditionsNotOk.ID:                               "Impossible de démarrer ce pipeline car les conditions de lancement ne sont pas respectées",
-	ErrDownloadInvalidOS.ID:                             "OS invalide. L'OS doit être linux, darwin, freebsd ou windows",
-	ErrDownloadInvalidArch.ID:                           "Architecture invalide. L'architecture doit être 386, i386, i686, amd64, x86_64 ou arm (dépendant de l'OS)",
-	ErrDownloadInvalidName.ID:                           "Nom invalide",
-	ErrDownloadDoesNotExist.ID:                          "Le fichier n'existe pas",
-	ErrTokenNotFound.ID:                                 "Le token n'existe pas",
-	ErrWorkflowNotificationNodeRef.ID:                   "Une référence de noeud de workflow est invalide dans vos notifications (si vous souhaitez supprimer un pipeline vérifiez qu'il ne soit plus référencé dans la liste de vos notifications)",
-	ErrInvalidJobRequirementDuplicateModel.ID:           "Pré-requis de job invalides: vous ne pouvez pas séléctionnez plusieurs modèles de worker",
-	ErrInvalidJobRequirementDuplicateHostname.ID:        "Pré-requis de job invalides: vous ne pouvez pas séléctionnez plusieurs hostname",
-	ErrInvalidKeyName.ID:                                "Nom de clé invalide. Les clés d'application doivent être préfixées par 'app-', les clés d'environnement doivent être préfixées par 'env-'",
-	ErrRepoOperationTimeout.ID:                          "L'analyse du dépôt a pris trop de temps",
-	ErrInvalidGitBranch.ID:                              "Valeur git.branch invalide, vous ne pouvez pas avoir de valeur git.branch avec une string vide dans votre payload par défaut",
-	ErrInvalidFavoriteType.ID:                           "Type de favori invalide: doit être 'projet' ou 'workflow'",
-	ErrUnsupportedOSArchPlugin.ID:                       "OS/Architecture non supporté pour ce plugin",
-	ErrNoBroadcast.ID:                                   "Information invalide",
-	ErrBroadcastNotFound.ID:                             "Information non trouvée",
-	ErrInvalidPatternModel.ID:                           "Pattern de modèle de worker invalide: le nom, type et commande principale sont requis",
-	ErrWorkerModelNoAdmin.ID:                            "Accès refusé: vous n'êtes ni un administrateur CDS ni un administrateur du groupe pour lequel vous tentez de créer votre modèle",
-	ErrWorkerModelNoPattern.ID:                          "Accès refusé: vous devez obligatoirement sélectionner un pattern de script de configuration. Si vous souhaitez ajouter un pattern particulier, veuillez contacter un administrateur CDS",
-	ErrJobNotBooked.ID:                                  "Le job est déjà libéré",
-	ErrUserNotFound.ID:                                  "Utilisateur non trouvé",
-	ErrInvalidNumber.ID:                                 "Nombre non valide",
-	ErrKeyAlreadyExist.ID:                               "La clé existe déjà",
-	ErrPipelineNameImport.ID:                            "Le nom du pipeline dans le code ne correspond pas au nom du pipeline que vous voulez éditer",
-	ErrWorkflowNameImport.ID:                            "Le nom du workflow dans le code ne correspond pas au nom du workflow que vous voulez éditer",
-	ErrIconBadFormat.ID:                                 "Mauvais format d'icône, doit être une image",
-	ErrIconBadSize.ID:                                   "Taille de l'icône trop importante. (max 100Ko)",
-	ErrWorkflowConditionBadOperator.ID:                  "Opérateur de condition de lancement incorrect",
-	ErrColorBadFormat.ID:                                "Format de la couleur incorrect. Vous devez utiliser le format hexadécimal (exemple: #FFFF)",
-	ErrInvalidHookConfiguration.ID:                      "Configuration de hook invalide",
-	ErrInvalidData.ID:                                   "Impossible de valider les données",
-	ErrInvalidGroupAdmin.ID:                             "L'utilisateur n'est pas administrateur du groupe",
-	ErrInvalidGroupMember.ID:                            "L'utilisateur n'est pas membre du groupe",
-	ErrWorkflowNotGenerated.ID:                          "Le workflow n'a pas été généré par un template",
-	ErrInvalidNodeDefaultPayload.ID:                     "Le workflow est incorrect. Un payload par défaut ne peut pas être sur un pipeline autre que le premier du workflow",
-	ErrInvalidApplicationRepoStrategy.ID:                "La stratégie de dépôt (vcs) de l'application n'est pas correcte",
-	ErrWorkflowNodeRootUpdate.ID:                        "Impossible de mettre à jour ou supprimer le noeud racine du workflow",
-	ErrWorkflowAlreadyAsCode.ID:                         "Le workflow est déjà as-code ou il y a déjà une pull-request pour le transformer",
-	ErrNoDBMigrationID.ID:                               "Cet id n'existe pas dans la table gorp_migrations",
-	ErrCannotParseTemplate.ID:                           "Impossible de parser le modèle de workflow",
-	ErrGroupNotFoundInProject.ID:                        "Impossible d'ajouter ce groupe dans vos permissions de workflow car ce groupe n'est pas présent dans les permissions de votre projet",
-	ErrGroupNotFoundInWorkflow.ID:                       "Impossible d'ajouter ce groupe dans vos permissions de noeud du workflow car ce groupe n'est pas présent dans les permissions de votre workflow",
-	ErrWorkflowPermInsufficient.ID:                      "Impossible d'ajouter ce groupe dans vos permissions du workflow car ce groupe a des droits inférieurs (< RWX) à celui du workflow",
-	ErrApplicationUsedByWorkflow.ID:                     "L'application est utilisée par un workflow",
-	ErrLocked.ID:                                        "La ressource est verrouillée",
-	ErrInvalidJobRequirementWorkerModelPermission.ID:    "Pré-requis de job invalide: Modèle de worker inutilisable en raison des permissions",
-	ErrInvalidJobRequirementWorkerModelCapabilitites.ID: "Pré-requis de job invalide: Le modèle de worker ne dispose pas de binaires suffisants",
-	ErrMalformattedStep.ID:                              "Étape malformée",
-	ErrVCSUsedByApplication.ID:                          "Le gestionnaire de dépot est encore utilisé par une application",
-	ErrApplicationAsCodeOverride.ID:                     "Vous ne pouvez pas importer l'application depuis ce dépôt",
-	ErrPipelineAsCodeOverride.ID:                        "Vous ne pouvez pas importer le pipeline depuis ce dépôt",
-	ErrEnvironmentAsCodeOverride.ID:                     "Vous ne pouvez pas importer l'environment depuis ce dépôt",
-	ErrWorkflowAsCodeOverride.ID:                        "Vous ne pouvez pas importer le workflow depuis ce dépôt",
-	ErrProjectSecretDataUnknown.ID:                      "Donnée chiffrée non valide",
-	ErrApplicationMandatoryOnWorkflowAsCode.ID:          "Une application liée à un dépôt git est obligatoire à la racine du workflow",
-	ErrInvalidPayloadVariable.ID:                        "Le payload du workflow ne peut pas contenir de clés nommées cds.*",
-	ErrInvalidPassword.ID:                               "Votre valeur de type mot de passe n'est pas correct",
-	ErrRepositoryUsedByHook.ID:                          "Il y a encore un repository webhook sur ce dépôt",
-	ErrResourceNotInProject.ID:                          "La ressource n'est pas lié au projet",
-	ErrEnvironmentNotFound.ID:                           "L'environnement n'existe pas",
-	ErrIntegrationtNotFound.ID:                          "L'intégration n'existe pas",
-	ErrSignupDisabled.ID:                                "La création de compte est désactivée pour ce mode d'authentification.",
-	ErrBadBrokerConfiguration.ID:                        "Impossible de se connecter à votre intégration de type évènement. Veuillez vérifier votre configuration",
-	ErrInvalidJobRequirementNetworkAccess.ID:            "Pré-requis de job invalide: Le pré-requis network doit contenir un ':'. Exemple: golang.org:http, golang.org:443",
-	ErrWorkflowAsCodeResync.ID:                          "Impossible de resynchroniser un workflow en mode as-code",
-	ErrWorkflowNodeNameDuplicate.ID:                     "Vous ne pouvez pas avoir plusieurs fois le même nom de pipeline dans votre workflow",
-	ErrUnsupportedMediaType.ID:                          "Le format de la requête est invalide",
-	ErrNothingToPush.ID:                                 "Aucune modification à pousser",
-	ErrWorkerErrorCommand.ID:                            "Commande du worker en erreur",
-	ErrRepoAnalyzeFailed.ID:                             "L'analyse du repository a echoué",
-	ErrConflictData.ID:                                  "Donnée en conflit",
-	ErrWebsocketUpgrade.ID:                              "Websocket upgrade requis",
-}
-
 // Error type.
 type Error struct {
 	ID         int         `json:"id"`
@@ -610,24 +426,8 @@ func (e Error) printLight() string {
 	return message
 }
 
-func (e Error) Translate(al string) string {
-	acceptedLanguages, _, err := language.ParseAcceptLanguage(al)
-	if err != nil {
-		acceptedLanguages = []language.Tag{language.AmericanEnglish}
-	}
-
-	// try to get error message for accepted language and error ID, else use unknown error message
-	tag, _, _ := matcher.Match(acceptedLanguages...)
-	var msg string
-	var ok bool
-	switch tag {
-	case language.French:
-		msg, ok = errorsFrench[e.ID]
-	case language.AmericanEnglish:
-		msg, ok = errorsAmericanEnglish[e.ID]
-	default:
-		msg, ok = errorsAmericanEnglish[e.ID]
-	}
+func (e Error) Translate() string {
+	msg, ok := errorsAmericanEnglish[e.ID]
 	if !ok {
 		return errorsAmericanEnglish[ErrUnknownError.ID]
 	}
@@ -646,7 +446,7 @@ func NewErrorWithStack(root error, err error) error {
 	case Error:
 		errWithStack.httpError = e
 	default:
-		errWithStack.httpError = ExtractHTTPError(err, "")
+		errWithStack.httpError = ExtractHTTPError(err)
 	}
 
 	return errWithStack
@@ -756,7 +556,7 @@ func NewError(httpError Error, err error) error {
 	if e, ok := err.(*MultiError); ok {
 		var ss []string
 		for i := range *e {
-			ss = append(ss, ExtractHTTPError((*e)[i], "").printLight())
+			ss = append(ss, ExtractHTTPError((*e)[i]).printLight())
 		}
 		httpError.From = strings.Join(ss, ", ")
 	} else {
@@ -816,7 +616,7 @@ func WrapError(err error, format string, args ...interface{}) error {
 	if e, ok := err.(*MultiError); ok {
 		var ss []string
 		for i := range *e {
-			ss = append(ss, ExtractHTTPError((*e)[i], "").printLight())
+			ss = append(ss, ExtractHTTPError((*e)[i]).printLight())
 		}
 		httpError.From = strings.Join(ss, ", ")
 	}
@@ -848,7 +648,7 @@ func WithStack(err error) error {
 	// if it's a Error wrap it in error with stack
 	if e, ok := err.(Error); ok {
 		return errorWithStack{
-			root:      errors.New(e.Translate("")),
+			root:      errors.New(e.Translate()),
 			stack:     callers(),
 			httpError: e,
 		}
@@ -871,7 +671,7 @@ func WithData(err error, data interface{}) error {
 
 // ExtractHTTPError tries to recognize given error and return http error
 // with message in a language matching Accepted-Language.
-func ExtractHTTPError(source error, al string) Error {
+func ExtractHTTPError(source error) Error {
 	var httpError Error
 
 	// try to recognize http error from source
@@ -880,7 +680,7 @@ func ExtractHTTPError(source error, al string) Error {
 		httpError = ErrUnknownError
 		var ss []string
 		for i := range *e {
-			ss = append(ss, ExtractHTTPError((*e)[i], al).printLight())
+			ss = append(ss, ExtractHTTPError((*e)[i]).printLight())
 		}
 		httpError.Message = strings.Join(ss, ", ")
 	case errorWithStack:
@@ -899,7 +699,7 @@ func ExtractHTTPError(source error, al string) Error {
 	// if error's message is not empty do not override (custom message)
 	// else set message for given accepted languages.
 	if httpError.Message == "" {
-		httpError.Message = httpError.Translate(al)
+		httpError.Message = httpError.Translate()
 	}
 
 	return httpError
diff --git a/sdk/hatchery/starter.go b/sdk/hatchery/starter.go
index 7c3017584..70dfc8935 100644
--- a/sdk/hatchery/starter.go
+++ b/sdk/hatchery/starter.go
@@ -206,7 +206,7 @@ func spawnWorkerForJob(ctx context.Context, h Interface, j workerStarterRequest)
 		ctxSendSpawnInfo, next = telemetry.Span(ctxJob, "hatchery.QueueJobSendSpawnInfo", telemetry.Tag("status", "errSpawn"), telemetry.Tag("msg", sdk.MsgSpawnInfoHatcheryErrorSpawn.ID))
 		SendSpawnInfo(ctxSendSpawnInfo, h, j.id, sdk.SpawnMsg{
 			ID:   sdk.MsgSpawnInfoHatcheryErrorSpawn.ID,
-			Args: []interface{}{h.Service().Name, modelName, sdk.Round(time.Since(start), time.Second).String(), sdk.ExtractHTTPError(errSpawn, "").Error()},
+			Args: []interface{}{h.Service().Name, modelName, sdk.Round(time.Since(start), time.Second).String(), sdk.ExtractHTTPError(errSpawn).Error()},
 		})
 		log.Error(ctx, "hatchery %s cannot spawn worker %s for job %d: %v", h.Service().Name, modelName, j.id, errSpawn)
 		next()
diff --git a/sdk/messages.go b/sdk/messages.go
index 751f215fb..dabe230e3 100644
--- a/sdk/messages.go
+++ b/sdk/messages.go
@@ -54,6 +54,10 @@ var (
 	MsgPipelineJobUpdated                   = &Message{"MsgPipelineJobUpdated", trad{FR: "Le job %s du stage %s a été mis à jour", EN: "Job %s in stage %s updated"}, nil, RunInfoTypInfo}
 	MsgPipelineJobAdded                     = &Message{"MsgPipelineJobAdded", trad{FR: "Le job %s du stage %s a été ajouté", EN: "Job %s in stage %s added"}, nil, RunInfoTypInfo}
 	MsgPipelineJobDeleted                   = &Message{"MsgPipelineJobDeleted", trad{FR: "Le job %s du stage %s a été supprimé", EN: "Job %s in stage %s deleted"}, nil, RunInfoTypInfo}
+	MsgPipelineDetached                     = &Message{"MsgPipelineDetached", trad{FR: "Le pipeline %s est détaché du repository %s", EN: "The pipeline %s is detached from repository %s"}, nil, RunInfoTypInfo}
+	MsgApplicationDetached                  = &Message{"MsgApplicationDetached", trad{FR: "L'application %s est détachée du repository %s", EN: "The application %s is detached from repository %s"}, nil, RunInfoTypInfo}
+	MsgEnvironmentDetached                  = &Message{"MsgEnvironmentDetached", trad{FR: "L'environnement %s est détaché du repository %s", EN: "The environment %s is detached from repository %s"}, nil, RunInfoTypInfo}
+	MsgWorkflowDetached                     = &Message{"MsgWorkflowDetached", trad{FR: "Le workflow %s est détaché du repository %s", EN: "The workflow %s is detached from repository %s"}, nil, RunInfoTypInfo}
 	MsgSpawnInfoHatcheryStarts              = &Message{"MsgSpawnInfoHatcheryStarts", trad{FR: "La Hatchery %s a démarré le lancement du worker avec le modèle %s", EN: "Hatchery %s starts spawn worker with model %s"}, nil, RunInfoTypInfo}
 	MsgSpawnInfoHatcheryErrorSpawn          = &Message{"MsgSpawnInfoHatcheryErrorSpawn", trad{FR: "Une erreur est survenue lorsque la Hatchery %s a démarré un worker avec le modèle %s après %s, err:%s", EN: "Error while Hatchery %s spawn worker with model %s after %s, err:%s"}, nil, RunInfoTypeError}
 	MsgSpawnInfoHatcheryStartsSuccessfully  = &Message{"MsgSpawnInfoHatcheryStartsSuccessfully", trad{FR: "La Hatchery %s a démarré le worker %s avec succès en %s", EN: "Hatchery %s spawn worker %s successfully in %s"}, nil, RunInfoTypInfo}
@@ -128,6 +132,10 @@ var Messages = map[string]*Message{
 	MsgPipelineJobUpdated.ID:                   MsgPipelineJobUpdated,
 	MsgPipelineJobAdded.ID:                     MsgPipelineJobAdded,
 	MsgPipelineJobDeleted.ID:                   MsgPipelineJobDeleted,
+	MsgPipelineDetached.ID:                     MsgPipelineDetached,
+	MsgApplicationDetached.ID:                  MsgApplicationDetached,
+	MsgEnvironmentDetached.ID:                  MsgEnvironmentDetached,
+	MsgWorkflowDetached.ID:                     MsgWorkflowDetached,
 	MsgSpawnInfoHatcheryStarts.ID:              MsgSpawnInfoHatcheryStarts,
 	MsgSpawnInfoHatcheryErrorSpawn.ID:          MsgSpawnInfoHatcheryErrorSpawn,
 	MsgSpawnInfoHatcheryStartsSuccessfully.ID:  MsgSpawnInfoHatcheryStartsSuccessfully,
@@ -198,19 +206,8 @@ var (
 )
 
 //String returns formated string for the specified language
-func (m *Message) String(al string) string {
-	acceptedLanguages, _, err := language.ParseAcceptLanguage(al)
-	if err != nil {
-		return fmt.Sprintf(m.Format[EN], m.Args...)
-	}
-
-	t, _, _ := matcher.Match(acceptedLanguages...)
-	switch t {
-	case language.French, language.AmericanEnglish:
-		return fmt.Sprintf(m.Format[lang(t)], m.Args...)
-	default:
-		return fmt.Sprintf(m.Format[EN], m.Args...)
-	}
+func (m *Message) String() string {
+	return fmt.Sprintf(m.Format[EN], m.Args...)
 }
 
 // MessagesToError returns a translated slices of messages as an error
@@ -220,14 +217,14 @@ func MessagesToError(messages []Message) error {
 		if i != 0 {
 			s += "; "
 		}
-		s += err.String(language.AmericanEnglish.String())
+		s += err.String()
 	}
 	return errors.New(s)
 }
 
 // ErrorToMessage returns message from an error if possible
 func ErrorToMessage(err error) (Message, bool) {
-	cdsError := ExtractHTTPError(err, "EN")
+	cdsError := ExtractHTTPError(err)
 	switch cdsError.ID {
 	case ErrPipelineNotFound.ID:
 		return NewMessage(MsgWorkflowErrorBadPipelineName, cdsError.Data), true
diff --git a/sdk/repositories_operation.go b/sdk/repositories_operation.go
index 578a87801..3ccc6b8ef 100644
--- a/sdk/repositories_operation.go
+++ b/sdk/repositories_operation.go
@@ -40,7 +40,7 @@ func ToOperationError(err error) *OperationError {
 	if err == nil {
 		return nil
 	}
-	sdkError := ExtractHTTPError(err, "")
+	sdkError := ExtractHTTPError(err)
 	return &OperationError{
 		ID:         sdkError.ID,
 		Status:     sdkError.Status,
diff --git a/sdk/vcs.go b/sdk/vcs.go
index 3ae654d51..d5331a887 100644
--- a/sdk/vcs.go
+++ b/sdk/vcs.go
@@ -100,8 +100,8 @@ var (
 		"project_column",
 		"project",
 		"public",
-		"pull-request_review_comment",
-		"pull-request_review",
+		"pull_request_review_comment",
+		"pull_request_review",
 		"pull_request",
 		"repository",
 		"repository_import",
diff --git a/sdk/workflow_run.go b/sdk/workflow_run.go
index 13948b73c..c703791ff 100644
--- a/sdk/workflow_run.go
+++ b/sdk/workflow_run.go
@@ -198,11 +198,11 @@ func (w WorkflowRunVersion) IsValid() error {
 }
 
 // Translate translates messages in WorkflowNodeRun
-func (r *WorkflowRun) Translate(lang string) {
+func (r *WorkflowRun) Translate() {
 	for ki, info := range r.Infos {
 		if _, ok := Messages[info.Message.ID]; ok {
 			m := NewMessage(Messages[info.Message.ID], info.Message.Args...)
-			r.Infos[ki].UserMessage = m.String(lang)
+			r.Infos[ki].UserMessage = m.String()
 		}
 	}
 }
@@ -428,10 +428,10 @@ type WorkflowNodeTriggerRun struct {
 }
 
 // Translate translates messages in WorkflowNodeRun
-func (nr *WorkflowNodeRun) Translate(lang string) {
+func (nr *WorkflowNodeRun) Translate() {
 	for ks := range nr.Stages {
 		for kj := range nr.Stages[ks].RunJobs {
-			nr.Stages[ks].RunJobs[kj].Translate(lang)
+			nr.Stages[ks].RunJobs[kj].Translate()
 		}
 	}
 }
@@ -550,11 +550,11 @@ type WorkflowNodeJobRunBooked struct {
 }
 
 // Translate translates messages in WorkflowNodeJobRun
-func (wnjr *WorkflowNodeJobRun) Translate(lang string) {
+func (wnjr *WorkflowNodeJobRun) Translate() {
 	for ki, info := range wnjr.SpawnInfos {
 		if _, ok := Messages[info.Message.ID]; ok {
 			m := NewMessage(Messages[info.Message.ID], info.Message.Args...)
-			wnjr.SpawnInfos[ki].UserMessage = m.String(lang)
+			wnjr.SpawnInfos[ki].UserMessage = m.String()
 		}
 	}
 }
diff --git a/ui/src/app/app.component.spec.ts b/ui/src/app/app.component.spec.ts
index a2049bd8f..39339cf76 100644
--- a/ui/src/app/app.component.spec.ts
+++ b/ui/src/app/app.component.spec.ts
@@ -26,7 +26,6 @@ import { ApplicationService } from './service/application/application.service';
 import { AuthenticationService } from './service/authentication/authentication.service';
 import { BroadcastService } from './service/broadcast/broadcast.service';
 import { BroadcastStore } from './service/broadcast/broadcast.store';
-import { LanguageStore } from './service/language/language.store';
 import { NavbarService } from './service/navbar/navbar.service';
 import { NotificationService } from './service/notification/notification.service';
 import { PipelineService } from './service/pipeline/pipeline.service';
@@ -62,7 +61,6 @@ describe('App: CDS', () => {
                 ProjectStore,
                 EnvironmentService,
                 AuthenticationService,
-                LanguageStore,
                 ThemeStore,
                 NotificationService,
                 AppService,
diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts
index 7a6df4584..7b5c873aa 100644
--- a/ui/src/app/app.component.ts
+++ b/ui/src/app/app.component.ts
@@ -16,7 +16,6 @@ import { Subscription } from 'rxjs/Subscription';
 import * as format from 'string-format-obj';
 import { AppService } from './app.service';
 import { AuthentifiedUser } from './model/user.model';
-import { LanguageStore } from './service/language/language.store';
 import { NotificationService } from './service/notification/notification.service';
 import { HelpService, MonitoringService } from './service/services.module';
 import { ThemeStore } from './service/theme/theme.store';
@@ -39,7 +38,6 @@ export class AppComponent implements OnInit, OnDestroy {
     heartbeatToken: number;
     zone: NgZone;
     showUIUpdatedBanner: boolean;
-    languageSubscriber: Subscription;
     themeSubscriber: Subscription;
     versionWorkerSubscription: Subscription;
     _routerSubscription: Subscription;
@@ -59,7 +57,6 @@ export class AppComponent implements OnInit, OnDestroy {
 
     constructor(
         _translate: TranslateService,
-        private _language: LanguageStore,
         private _theme: ThemeStore,
         private _activatedRoute: ActivatedRoute,
         private _titleService: Title,
@@ -83,14 +80,6 @@ export class AppComponent implements OnInit, OnDestroy {
         _translate.use(browserLang.match(/en|fr/) ? browserLang : 'en');
         registerLocaleData(browserLang.match(/fr/) ? localeFR : localeEN);
 
-        this.languageSubscriber = this._language.get().subscribe(l => {
-            if (l) {
-                _translate.use(l);
-            } else {
-                _language.set(browserLang.match(/en|fr/) ? browserLang : 'en');
-            }
-        });
-
         this.themeSubscriber = this._theme.get().subscribe(t => {
             if (t) {
                 document.body.className = t;
diff --git a/ui/src/app/model/pipeline.model.ts b/ui/src/app/model/pipeline.model.ts
index c4711510e..540dd2da7 100644
--- a/ui/src/app/model/pipeline.model.ts
+++ b/ui/src/app/model/pipeline.model.ts
@@ -199,6 +199,23 @@ export class CDNLogLink {
     api_ref: string;
 }
 
+export class CDNLogsLines {
+    api_ref: string
+    lines_count: number
+}
+
+export class CDNLogLinks {
+    item_type: string;
+    datas: Array<CDNLogLinkData>;
+}
+
+
+export class CDNLogLinkData {
+    api_ref: string
+    step_order: number
+    requirement_id: number
+}
+
 export class CDNLinesResponse {
     totalCount: number;
     lines: Array<CDNLine>;
diff --git a/ui/src/app/service/language/language.interceptor.ts b/ui/src/app/service/language/language.interceptor.ts
deleted file mode 100644
index 8da94f444..000000000
--- a/ui/src/app/service/language/language.interceptor.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-import { Observable } from 'rxjs';
-import { LanguageStore } from './../language/language.store';
-
-@Injectable()
-export class LanguageInterceptor implements HttpInterceptor {
-  languageHeader: string;
-
-  constructor(
-    private _language: LanguageStore
-  ) {
-    this.languageHeader = 'en-US';
-
-    this._language.get().subscribe(l => {
-      if (l) {
-        this.languageHeader = l;
-      }
-    });
-  }
-
-  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
-    if (req.url.indexOf('assets/i18n') !== -1) {
-      return next.handle(req.clone());
-    }
-
-    return next.handle(req.clone({
-      setHeaders: { 'Accept-Language': this.languageHeader }
-    }));
-  }
-}
diff --git a/ui/src/app/service/language/language.store.ts b/ui/src/app/service/language/language.store.ts
deleted file mode 100644
index 08b3dbf79..000000000
--- a/ui/src/app/service/language/language.store.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Injectable } from '@angular/core';
-import * as moment from 'moment';
-import { BehaviorSubject, Observable } from 'rxjs';
-
-@Injectable()
-export class LanguageStore {
-    private _language: BehaviorSubject<string> = new BehaviorSubject(null);
-
-    constructor() {
-        let previousLanguage = localStorage.getItem('CDS-Language');
-        if (previousLanguage) {
-            this._language.next(previousLanguage);
-        }
-    }
-
-    get() {
-        return new Observable<string>(fn => this._language.subscribe(fn));
-    }
-
-    set(l: string) {
-        moment.locale(l);
-        localStorage.setItem('CDS-Language', l);
-        this._language.next(l);
-    }
-}
diff --git a/ui/src/app/service/services.module.ts b/ui/src/app/service/services.module.ts
index 3bf6ff6f3..f5a8dec06 100644
--- a/ui/src/app/service/services.module.ts
+++ b/ui/src/app/service/services.module.ts
@@ -34,8 +34,6 @@ import { HookService } from './hook/hook.service';
 import { ImportAsCodeService } from './import-as-code/import.service';
 import { IntegrationService } from './integration/integration.service';
 import { KeyService } from './keys/keys.service';
-import { LanguageInterceptor } from './language/language.interceptor';
-import { LanguageStore } from './language/language.store';
 import { MonitoringService } from './monitoring/monitoring.service';
 import { NavbarService } from './navbar/navbar.service';
 import { NotificationService } from './notification/notification.service';
@@ -104,7 +102,6 @@ export class ServicesModule {
                 BroadcastService,
                 BroadcastStore,
                 KeyService,
-                LanguageStore,
                 ThemeStore,
                 NavbarService,
                 NotificationService,
@@ -143,11 +140,6 @@ export class ServicesModule {
                     useClass: XSRFInterceptor,
                     multi: true
                 },
-                {
-                    provide: HTTP_INTERCEPTORS,
-                    useClass: LanguageInterceptor,
-                    multi: true
-                },
                 {
                     provide: HTTP_INTERCEPTORS,
                     useClass: LogoutInterceptor,
@@ -180,7 +172,6 @@ export {
     ImportAsCodeService,
     BroadcastStore,
     KeyService,
-    LanguageStore,
     ThemeStore,
     ParameterService,
     MonitoringService,
diff --git a/ui/src/app/service/workflow/workflow.service.ts b/ui/src/app/service/workflow/workflow.service.ts
index a4dc7c382..338b0e659 100644
--- a/ui/src/app/service/workflow/workflow.service.ts
+++ b/ui/src/app/service/workflow/workflow.service.ts
@@ -1,7 +1,16 @@
 import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 import { Operation } from 'app/model/operation.model';
-import { BuildResult, CDNLine, CDNLinesResponse, CDNLogLink, ServiceLog, SpawnInfo } from 'app/model/pipeline.model';
+import {
+    BuildResult,
+    CDNLine,
+    CDNLinesResponse,
+    CDNLogLink,
+    CDNLogLinks,
+    CDNLogsLines,
+    ServiceLog,
+    SpawnInfo
+} from 'app/model/pipeline.model';
 import { WorkflowRetentoinDryRunResponse } from 'app/model/purge.model';
 import { Workflow, WorkflowPull, WorkflowTriggerConditionCache } from 'app/model/workflow.model';
 import { Observable } from 'rxjs';
@@ -65,6 +74,19 @@ export class WorkflowService {
         return this._http.get<CDNLogLink>(`/project/${projectKey}/workflows/${workflowName}/nodes/${nodeRunID}/job/${jobRunID}/step/${stepOrder}/link`);
     }
 
+    getAllStepsLinks(projectKey: string, workflowName: string, nodeRunID: number, jobRunID: number): Observable<CDNLogLinks> {
+        return this._http.get<CDNLogLinks>(`/project/${projectKey}/workflows/${workflowName}/nodes/${nodeRunID}/job/${jobRunID}/links`);
+    }
+
+    getLogsLinesCount(links: CDNLogLinks, limit: number): Observable<Array<CDNLogsLines>> {
+        let params = new HttpParams();
+        links.datas.map(l => l.api_ref).forEach(ref => {
+            params = params.append('apiRefHash', ref)
+        })
+        params.append('limit', limit.toString());
+        return this._http.get<Array<CDNLogsLines>>(`./cdscdn/item/${links.item_type}/lines`, { params })
+    }
+
     getLogLines(link: CDNLogLink, params?: { [key: string]: string }): Observable<CDNLinesResponse> {
         return this._http.get(`./cdscdn/item/${link.item_type}/${link.api_ref}/lines`, { params, observe: 'response' })
             .map(res => {
diff --git a/ui/src/app/service/workflow/workflow.store.ts b/ui/src/app/service/workflow/workflow.store.ts
index ce32c4a17..ed7aee619 100644
--- a/ui/src/app/service/workflow/workflow.store.ts
+++ b/ui/src/app/service/workflow/workflow.store.ts
@@ -69,6 +69,9 @@ export class WorkflowStore {
     }
 
     setDirection(key: string, name: string, o: string) {
+        if (!key || !name) {
+            return;
+        }
         let ls = localStorage.getItem(this.WORKFLOW_ORIENTATION_KEY);
         let j = {};
         if (ls) {
diff --git a/ui/src/app/views/navbar/navbar.component.ts b/ui/src/app/views/navbar/navbar.component.ts
index d6ab46c05..50535812e 100644
--- a/ui/src/app/views/navbar/navbar.component.ts
+++ b/ui/src/app/views/navbar/navbar.component.ts
@@ -10,8 +10,6 @@ import { Project } from 'app/model/project.model';
 import { AuthentifiedUser } from 'app/model/user.model';
 import { ApplicationStore } from 'app/service/application/application.store';
 import { BroadcastStore } from 'app/service/broadcast/broadcast.store';
-import { HelpService } from 'app/service/help/help.service';
-import { LanguageStore } from 'app/service/language/language.store';
 import { NavbarService } from 'app/service/navbar/navbar.service';
 import { RouterService } from 'app/service/router/router.service';
 import { ProjectStore } from 'app/service/services.module';
@@ -47,7 +45,6 @@ export class NavbarComponent implements OnInit, OnDestroy {
     previousBroadcastsToDisplay: Array<Broadcast> = new Array<Broadcast>();
     loading = true;
     listWorkflows: List<NavbarRecentData>;
-    currentCountry: string;
     langSubscription: Subscription;
     navbarSubscription: Subscription;
     userSubscription: Subscription;
@@ -75,7 +72,6 @@ export class NavbarComponent implements OnInit, OnDestroy {
         private _workflowStore: WorkflowStore,
         private _broadcastStore: BroadcastStore,
         private _router: Router,
-        private _language: LanguageStore,
         private _theme: ThemeStore,
         private _routerService: RouterService,
         private _cd: ChangeDetectorRef
@@ -85,11 +81,6 @@ export class NavbarComponent implements OnInit, OnDestroy {
             this._cd.markForCheck();
         });
 
-        this.langSubscription = this._language.get().subscribe(l => {
-            this.currentCountry = l;
-            this._cd.markForCheck();
-        });
-
         this.themeSubscription = this._theme.get().subscribe(t => {
             this.themeSwitch.setValue(t === 'night');
             this._cd.markForCheck();
@@ -113,10 +104,6 @@ export class NavbarComponent implements OnInit, OnDestroy {
 
     ngOnDestroy(): void {} // Should be set to use @AutoUnsubscribe with AOT
 
-    changeCountry() {
-        this._language.set(this.currentCountry);
-    }
-
     changeTheme() {
         let darkActive = !!this.themeSwitch.value;
         this._theme.set(darkActive ? 'night' : 'light');
diff --git a/ui/src/app/views/navbar/navbar.html b/ui/src/app/views/navbar/navbar.html
index d55c8f742..be331cc81 100644
--- a/ui/src/app/views/navbar/navbar.html
+++ b/ui/src/app/views/navbar/navbar.html
@@ -311,12 +311,6 @@
             </div>
         </ng-template>
 
-        <sm-select class="languageSelect" [options]="{'fullTextSearch': true}" [(model)]="currentCountry"
-            (modelChange)="changeCountry()">
-            <option value="fr"><i class="france flag"></i></option>
-            <option value="en"><i class="gb flag"></i></option>
-        </sm-select>
-
         <sm-dropdown icon="settings" *ngIf="currentUser">
             <a sm-item href="#"
                 [routerLink]="['settings', 'user', currentUser.username]">{{ 'navbar_profile' | translate }}</a>
diff --git a/ui/src/app/views/workflow/add/workflow.add.html b/ui/src/app/views/workflow/add/workflow.add.html
index b09efec1c..f3ef728dc 100644
--- a/ui/src/app/views/workflow/add/workflow.add.html
+++ b/ui/src/app/views/workflow/add/workflow.add.html
@@ -211,7 +211,8 @@
                                                             </app-vcs-strategy>
                                                         </div>
                                                         <div class="right aligned field"
-                                                            *ngIf="!pollingResponse && selectedRepo">
+                                                            *ngIf="!pollingResponse && selectedRepo ||
+                                                            (selectedRepo && pollingResponse?.error_details?.message !== '')">
                                                             <button class="ui green button" type="button"
                                                                 [class.loading]="loading" [disabled]="loading"
                                                                 (click)="createWorkflowFromRepo()">{{
@@ -225,7 +226,7 @@
                                                             </ng-container>
 
                                                             <div class="ui error message"
-                                                                *ngIf="pollingResponse.status === 3">
+                                                                *ngIf="pollingResponse.status === 3 && !loading">
                                                                 {{pollingResponse.error_details.message}} - 
                                                                 {{pollingResponse.error_details.from}}                                                            </div>
                                                             <ng-container
diff --git a/ui/src/app/views/workflow/graph/workflow.graph.component.ts b/ui/src/app/views/workflow/graph/workflow.graph.component.ts
index 4b2a80310..c3b2f2453 100644
--- a/ui/src/app/views/workflow/graph/workflow.graph.component.ts
+++ b/ui/src/app/views/workflow/graph/workflow.graph.component.ts
@@ -41,7 +41,7 @@ export class WorkflowGraphComponent implements AfterViewInit, OnDestroy {
     @Input('direction')
     set direction(data: string) {
         this._direction = data;
-        this._workflowStore.setDirection(this.project.key, this.workflow.name, this.direction);
+        this._workflowStore.setDirection(this.project?.key, this.workflow?.name, this.direction);
         this.changeDisplay();
     }
     get direction() { return this._direction; }
@@ -87,6 +87,9 @@ export class WorkflowGraphComponent implements AfterViewInit, OnDestroy {
     }
 
     initWorkflow() {
+        if (!this.workflow) {
+            return;
+        }
         // https://github.com/cpettitt/dagre/wiki#configuring-the-layout
         this.g = new dagreD3.graphlib.Graph().setGraph({ rankdir: this.direction, nodesep: 10, ranksep: 15, edgesep: 5 });
         // Create all nodes
diff --git a/ui/src/app/views/workflow/run/node/artifact/artifact.list.spec.ts b/ui/src/app/views/workflow/run/node/artifact/artifact.list.spec.ts
index fa5ceca4a..96141f690 100644
--- a/ui/src/app/views/workflow/run/node/artifact/artifact.list.spec.ts
+++ b/ui/src/app/views/workflow/run/node/artifact/artifact.list.spec.ts
@@ -1,10 +1,10 @@
 /* tslint:disable:no-unused-variable */
-import {TestBed, fakeAsync} from '@angular/core/testing';
+import {fakeAsync, TestBed} from '@angular/core/testing';
 import {RouterTestingModule} from '@angular/router/testing';
-import {TranslateService, TranslateLoader, TranslateParser, TranslateModule} from '@ngx-translate/core';
+import {TranslateLoader, TranslateModule, TranslateParser, TranslateService} from '@ngx-translate/core';
 import {SharedModule} from '../../../../../shared/shared.module';
-import {WorkflowRunArtifactListComponent} from './artifact.list.component';
 import {WorkflowModule} from '../../../workflow.module';
+import {WorkflowRunArtifactListComponent} from './artifact.list.component';
 
 describe('CDS: Artifact List', () => {
 
diff --git a/ui/src/app/views/workflow/run/node/pipeline/workflow-run-job/workflow-run-job.component.ts b/ui/src/app/views/workflow/run/node/pipeline/workflow-run-job/workflow-run-job.component.ts
index 7feb77a52..0edaffdf0 100644
--- a/ui/src/app/views/workflow/run/node/pipeline/workflow-run-job/workflow-run-job.component.ts
+++ b/ui/src/app/views/workflow/run/node/pipeline/workflow-run-job/workflow-run-job.component.ts
@@ -1,5 +1,5 @@
 import { OnDestroy, Output, ViewChild } from '@angular/core';
-import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit } from '@angular/core';
 import { Router } from '@angular/router';
 import { Store } from '@ngxs/store';
 import { CDNLine, CDNLogLink, CDNStreamFilter, PipelineStatus, SpawnInfo } from 'app/model/pipeline.model';
@@ -215,34 +215,24 @@ export class WorkflowRunJobComponent implements OnInit, OnDestroy {
             return;
         }
 
-        for (let i = 1; i < this.steps.length; i++) {
-            // We want to load initial data (first 5 and last 5 lines) for ended steps never loaded
-            if (PipelineStatus.isActive(this.nodeJobRun.job.step_status[i - 1].status)) {
-                break;
-            }
-            if (this.steps[i].link) {
-                continue;
-            }
-            if (this.steps[i].disabled) {
-                continue;
-            }
+        let links = await this._workflowService
+            .getAllStepsLinks(projectKey, workflowName, this.nodeJobRun.workflow_node_run_id, this.nodeJobRun.id).toPromise();
+        let results = await this._workflowService.getLogsLinesCount(links, this.initLoadLinesCount).toPromise();
 
-            this.steps[i].link = await this._workflowService
-                .getStepLink(projectKey, workflowName, this.nodeJobRun.workflow_node_run_id, this.nodeJobRun.id, i - 1)
-                .toPromise();
-            let results = await Promise.all([
-                this._workflowService.getLogLines(this.steps[i].link, { limit: `${this.initLoadLinesCount}` }).toPromise(),
-                this._workflowService.getLogLines(this.steps[i].link, { offset: `-${this.initLoadLinesCount}` }).toPromise()
-            ]);
-            this.steps[i].lines = results[0].lines;
-            this.steps[i].endLines = results[1].lines.filter(l => !results[0].lines.find(line => line.number === l.number));
-            this.steps[i].totalLinesCount = results[0].totalCount;
-            this.steps[i].open = true;
-            this.steps[i].loading = false;
+        if (results) {
+            results.forEach(r => {
+                let steporder = links?.datas?.find(d => d.api_ref === r.api_ref)?.step_order + 1;
+                if (!steporder) {
+                    return
+                }
+                this.steps[steporder].link = <CDNLogLink>{api_ref: r.api_ref, item_type: links.item_type}
+                this.steps[steporder].totalLinesCount = r.lines_count;
+                this.steps[steporder].open = false;
+                this.steps[steporder].loading = false;
+            })
         }
 
         this.computeStepFirstLineNumbers();
-
         this._cd.markForCheck();
     }
 
@@ -274,7 +264,7 @@ export class WorkflowRunJobComponent implements OnInit, OnDestroy {
         this._cd.markForCheck();
     }
 
-    trackStepElement(index: number, element: LogBlock) { return index; }
+    trackStepElement(index: number, _: LogBlock) { return index; }
 
     trackLineElement(index: number, element: CDNLine) { return element ? element.number : null; }
 
@@ -490,4 +480,23 @@ export class WorkflowRunJobComponent implements OnInit, OnDestroy {
             this.jobVariable.show();
         }
     }
+
+    async clickOpen(step: LogBlock) {
+        if (step?.lines?.length > 0 || step.open) {
+            step.clickOpen();
+            return
+        }
+
+        step.loading = true;
+        let results = await Promise.all([
+            this._workflowService.getLogLines(step.link, { limit: `${this.initLoadLinesCount}` }).toPromise(),
+            this._workflowService.getLogLines(step.link, { offset: `-${this.initLoadLinesCount}` }).toPromise()
+        ]);
+        step.lines = results[0].lines;
+        step.endLines = results[1].lines.filter(l => !results[0].lines.find(line => line.number === l.number));
+        step.totalLinesCount = results[0].totalCount;
+        step.open = true;
+        step.loading = false;
+        this._cd.markForCheck()
+    }
 }
diff --git a/ui/src/app/views/workflow/run/node/pipeline/workflow-run-job/workflow-run-job.html b/ui/src/app/views/workflow/run/node/pipeline/workflow-run-job/workflow-run-job.html
index a09a7abd9..dbac72cc4 100644
--- a/ui/src/app/views/workflow/run/node/pipeline/workflow-run-job/workflow-run-job.html
+++ b/ui/src/app/views/workflow/run/node/pipeline/workflow-run-job/workflow-run-job.html
@@ -17,7 +17,7 @@
     <!-- Display job steps -->
     <ng-container *ngIf="currentTabIndex === 0">
         <div class="step" *ngFor="let step of steps; trackBy: trackStepElement; let i = index">
-            <div class="line info" (click)="step.clickOpen()">
+            <div class="line info" (click)="clickOpen(step)">
                 <div class="number">
                     <i *ngIf="step.lines.length > 0" class="caret icon" [class.down]="step.open"
                         [class.right]="!step.open"></i>
diff --git a/ui/src/assets/i18n/fr.json b/ui/src/assets/i18n/fr.json
deleted file mode 100644
index 93ee6ead0..000000000
--- a/ui/src/assets/i18n/fr.json
+++ /dev/null
@@ -1,977 +0,0 @@
-{
-  "auth_password_too_weak": "Le mot de passe est trop faible. Essayez d'ajouter des caractères spéciaux, des lettres majuscules et minuscules ou encore des chiffres.",
-  "auth_password_too_long": "Le mot de passe est trop long. Il doit contenir au maximum 256 caractères.",
-  "auth_button_signin_with": "Se connecter avec {{consumer}}",
-  "auth_value_copied": "La valeur a été copiée",
-  "auth_message_init_token": "Si vous venez de générer la configuration de CDS, vous pouvez fournir la clé d'initialisation falculative.",
-  "auth_message_command": "Commande",
-  "auth_message_copy": "Vous pouvez maintenant copier cette commande dans votre terminal pour terminer la connexion, puis fermer cet onglet.",
-  "auth_message_copy_signin_token": "Vous pouvez maintenant utiliser ce token pour vous identifier avec le client.",
-  "auth_callback_missing_param": "Un ou plusieurs paramètres sont manquants pour poursuivre. Vous pouvez essayer de vous connecter à nouveau.",
-  "auth_callback_error": "Une erreur est seurvenue. Vous pouvez essayer de vous connecter à nouveau.",
-  "auth_ask_reset_error": "L'adresse renseignée est invalide ou inconnue.",
-  "auth_ask_reset_success": "La demande de récupération de mot de passe a été prise en compte, si un compte CDS existe avec l'adresse renseignée vous recevrez un lien pour le mettre à jour.",
-  "auth_reset_error": "Nouveau mot de passe invalide ou mauvaise clé de récupération transmise.",
-  "auth_reset_success": "Le mot de passe a été mis à jour avec succès.",
-  "auth_btn_go_to_home": "Continuer sur CDS",
-  "auth_signup_success": "Votre demande d'inscription a été prise en compte, vous allez recevoir un email pour vérifier votre compte.",
-  "auth_verify_error": "Impossible de vérifier le compte.",
-  "auth_verify_success": "Le compte utilisateur '{{username}}' a été vérifié avec succès.",
-  "auth_consumer_details_parent": "Client parent",
-  "auth_consumer_details_children": "Clients enfants",
-  "auth_consumer_regen": "Régénérer",
-  "auth_consumer_regen_revoke": "Régénérer en revoquant les sessions liées",
-  "account_btn_password": "Mot de passe perdu",
-  "account_btn_signup": "Créer un compte",
-  "account_btn_signin": "Se connecter",
-  "account_btn_submit": "Soumettre",
-  "account_btn_signin_ldap": "Se connecter via LDAP",
-  "account_login_btn_connect": "Connexion",
-  "account_login_btn_verify": "Vérifier",
-  "account_login_title": "Se connecter à CDS",
-  "account_password_btn_reset": "Réinitialiser le mot de passe",
-  "account_password_btn_reset_validate": "Modifier le mot de passe",
-  "account_password_title": "Mot de passe oublié",
-  "account_signup_btn_create": "Créer le compte",
-  "account_signup_init_token": "Clé d'initialisation",
-  "account_verify_title": "Information du compte",
-  "action_always_executed_details": "Cochée, cela signifie que cette étape sera toujours executée même si les étapes précédentes tombent en erreur",
-  "action_always_executed": "Toujours executée",
-  "action_btn_init_from_job": "Initialiser une nouvelle action à partir du job",
-  "action_builtin": "Prédéfinie",
-  "action_custom": "Personnalisée",
-  "action_deleted": "Action supprimée",
-  "action_delete": "Supprimer l'action",
-  "action_help_add_from": "L'action courante a été initialisée à partir des données du job \"{{jobName}}\" du pipeline \"{{pipelineName}}\" dans le projet \"{{projectKey}}\". Ces données ne seront pas enregistrées tant que l'action n'aura pas été sauvegardée.",
-  "action_help_line_1": "Qu'est-ce qu'une action ?",
-  "action_list_title": "Actions",
-  "action_name": "Nom de l'action",
-  "action_optional_details": "Cochée, cela signifie que même si cette étape tombe en erreur, le déroulement continue",
-  "action_optional": "Optionnelle",
-  "action_saved": "Action sauvegardée",
-  "action_step_title": "Étapes du job",
-  "action_usage_actions_using": "Cette action est utilisée dans les actions suivantes :",
-  "action_usage_not_used": "Cette action n'est pas utilisée par vos pipelines ou actions.",
-  "action_usage_pipelines_using": "Cette action est utilisée dans les pipelines suivants :",
-  "action_usage_warning_action": "L'action parente ne sera pas en mesure d'exécuter cette action car elles ne font pas parties du même groupe.",
-  "action_usage_warning_pipeline": "Ce pipeline ne sera pas en mesure d'exécuter cette action car la permission pour le groupe n'existe pas.",
-  "admin_queue_title": "File d'attente",
-  "cdsapi_loading": "CDS API est en cours de démarrage. Merci de patienter.",
-  "application_ascode_updated": "Brouillon mis à jour",
-  "application_create": "Créer une application",
-  "application_created": "Application créée",
-  "application_delete_description": "Une fois l'application supprimée, il n'y a pas de retour possible.",
-  "application_delete_label": "Suppression de l'application",
-  "application_deleted": "Application supprimée",
-  "application_deleted_by": "L'application {{appName}} a été supprimée par {{username}}",
-  "application_deployment_title": "Lier l'application à une intégration (déploiement): ",
-  "application_description": "Description de l'application",
-  "application_from_repository": "Application importée depuis {{repo}}",
-  "application_home_history": "Historique",
-  "application_home_integration": "Liste des intégrations liées",
-  "application_home_no_graph": "Aucun graphique à afficher (vulnerabilités, tests unitaires)",
-  "application_home_no_integration": "Lier une intégration",
-  "application_home_no_repo": "Lier un dépôt GIT",
-  "application_home_no_usage": "L'application n'est pas utilisée",
-  "application_home_repo": "Dépôt GIT",
-  "application_icon": "Icône de l'application",
-  "application_integration_added": "Intégration ajoutée",
-  "application_integration_deleted": "Intégration supprimée",
-  "application_integration_updated": "Intégration mise à jour",
-  "application_name_error": "Le nom de l'application est invalide. Pattern autorisé : a-zA-Z0-9._-",
-  "application_name": "Nom de l'application",
-  "application_new_title": "Nouvelle application",
-  "application_no_usage": "L'application n'est pas utilisée",
-  "application_rename_title": "Renommer l'application",
-  "application_repo_attach_ok": "Le dépôt est désormais lié à l'application",
-  "application_repo_choose": "Choisir un dépôt",
-  "application_repo_detach_ok": "Le dépôt n'est désormais plus lié à l'application",
-  "application_repo_missing_conf_details": "Presque terminé ! Vous devez configurer le protocole de connexion à votre dépôt ci-dessous et sauvegarder vos changements",
-  "application_repo_missing_conf": "Configuration manquante",
-  "application_repo_no": "L'application n'est pas liée à un dépôt GIT",
-  "application_repo_strat_title": "Configuration",
-  "application_repo_title": "Lier l'application à un dépôt : ",
-  "application_repoman_cant_link": "ici",
-  "application_repoman_cant": "Vous devez d'abord connecter un gestionnaire de dépôt à votre projet : ",
-  "application_repository_help_line_1": "Votre application n'a pas été importée depuis votre code.",
-  "application_update_name_error": "Le nom de l'application est obligatoire et doit être au format alphanumérique + .-_ ([a-zA-Z0-9._-]*)",
-  "application_update_ok": "L'application a été mise à jour",
-  "application_variable_form_title": "Ajouter une variable à l'application : ",
-  "application_variable_list_title": "Liste des variables de l'application : ",
-  "artifact_name": "Nom de l'artefact",
-  "artifact_sha512": "SHA512 Checksum",
-  "artifact_tag": "Tag",
-  "audit_action": "Action",
-  "ascode_error_unknown_type": "Impossible de détecter la ressource à mettre à jour",
-  "ascode_modal_title": "Sauvegarder as code",
-  "ascode_modal_label_branch": "Sélectionner ou créer une branche",
-  "ascode_modal_label_message": "Message de commit",
-  "ascode_save_modal_info_line_1": "Une \"pull request\" sera créée sur le gestionnaire de dépôt {{repo}}.",
-  "audit_change": "Modification",
-  "audit_loading": "Chargement des audits",
-  "audit_modification_type": "Type de modification",
-  "audit_no": "Aucun audit",
-  "audit_time_author": "Heure de modification",
-  "audit_title_variable": "Audit de variable",
-  "audit_title": "Historique des modifications",
-  "audit_username": "Utilisateur",
-  "broadcast_archived_help": "Cocher pour archiver cette information",
-  "broadcast_archived": "Archivée",
-  "broadcast_content": "Message",
-  "broadcast_created": "Créée le",
-  "broadcast_deleted": "Information supprimée",
-  "broadcast_help_line_1": "Une information est visible sur la page d'accueil de CDS.",
-  "broadcast_help_line_2": "Si un projet est sélectionné, l'information est visible que par les utilisateurs concernés.",
-  "broadcast_id": "N°",
-  "broadcast_level": "Type",
-  "broadcast_list_title": "Infos",
-  "broadcast_none": "Aucune information",
-  "broadcast_project_select": "Selectionnez un projet (optionnel)",
-  "broadcast_project": "Projet",
-  "broadcast_recent": "Récentes",
-  "broadcast_saved": "Information sauvegardée",
-  "broadcast_seen": "Vues",
-  "broadcast_title": "Titre",
-  "broadcast_updated": "Modifiée le",
-  "btn_add": "Ajouter",
-  "btn_apply": "Appliquer",
-  "btn_back": "Retour",
-  "btn_cancel": "Annuler",
-  "btn_close": "Fermer",
-  "btn_continue": "Continuer",
-  "btn_create_workflow": "Création du workflow",
-  "btn_create": "Créer",
-  "btn_delete": "Supprimer",
-  "btn_dry_run": "Tester",
-  "btn_filter": "Filtrer",
-  "btn_finish": "Terminer",
-  "btn_goto_workflow": "Voir le workflow",
-  "btn_inspect_repo": "Inspecter le dépôt",
-  "btn_link": "Connecter",
-  "btn_logs": "Afficher les logs",
-  "btn_next": "Suivant",
-  "btn_rename": "Renommer",
-  "btn_resync_repo": "Resynchroniser",
-  "btn_resync_ascode_event": "Synchroniser les modifications du dépôt",
-  "btn_run_workflow": "Lancer le workflow",
-  "btn_run": "Lancer le pipeline",
-  "btn_save": "Sauvegarder",
-  "btn_save_application": "Sauvegarder l'application",
-  "btn_save_pipeline": "Sauvegarder le pipeline",
-  "btn_save_workflow": "Sauvegarder le workflow",
-  "btn_save_environment": "Sauvegarder l'environment",
-  "btn_synchronize": "Synchronizer",
-  "btn_validate": "Valider",
-  "cdsctl_choice_arch": "Choisissez votre architecture",
-  "cdsctl_choice_options_with_keychain": "Avec support du trousseau de clé",
-  "cdsctl_choice_options": "Choisissez vos options",
-  "cdsctl_choice_os": "Choisissez votre système d'exploitation",
-  "cdsctl_info_list_1": "Gérer vos utilisateurs, vos groupes, vos actions, vos worker models, vos templates de workflow, ...",
-  "cdsctl_info_list_2": "Gérer vos projets, vos workflows, vos applications, vos environnements, ...",
-  "cdsctl_info_list_3": "Monitorer & administrer votre plateforme CDS",
-  "cdsctl_info_list_4": "Automatiser n'importe quelle tâche (administation, utilisation...)",
-  "cdsctl_info_title": "est l'outil de Ligne de Commande CDS. Cet outil contient toutes les fonctionnalités disponibles dans l'interface web, et même plus. Il vous permet par exemple de:",
-  "cdsctl_notice_1_1": "Binaires disponibles sur cette instance de CDS: ",
-  "cdsctl_notice_2_1": "Si vous avez plusieurs instances de CDS, vous pouvez utiliser le flag -f, exemple: `./cdsctl -f .cds/anotherInstance login --api-url http://another-instance:8081 -u ",
-  "cdsctl_notice_2_2": "Ne pas utiliser le secret en provenance du trousseau ou d'un fichier de configuration:",
-  "cdsctl_notice_2_3": "Vous souhaitez débugger quelque chose ? Vous pouvez utiliser la variable d'environnement CDS_VERBOSE:",
-  "cdsctl_notice_6_1": "Après avoir ajouté cet alias dans votre fichier gitconfig, vous pouvez maintenant utiliser: git push && git track",
-  "cdsctl_notice": "À noter:",
-  "cdsctl_part_1": "curl {{apiURL}}/download/cdsctl/{{osChoice}}/{{archChoice}}{{variant}} -o cdsctl\n\n# ajoutez les droits EXEC sur cdsctl\nchmod +x cdsctl\n\n# vérifiez que le binaire est ok, cette commande affiche la version du binaire\n./cdsctl version",
-  "cdsctl_part_2": "# La commande cdsctl login enregistre dans votre trousseau votre mot de passe\n./cdsctl login --api-url {{apiURL}} -u {{username}}\n\n# cette command affiche votre nom d'utilisateur, nom complet, adresse e-mail\n# si vous les voyez, c'est que cdsctl est bien configuré\n./cdsctl user me",
-  "cdsctl_part_3": "# La commande cdsctl shell permet de naviguer dans vos projets et vos workflows sans avoir besoin d'ouvrir un navigateur.\n# voir https://ovh.github.io/cds/docs/components/cdsctl/shell/\ncdsctl shell\n\n# l'aide est disponible sur chaque commande cdsctl\ncdsctl --help",
-  "cdsctl_part_4": "# Démarre un workflow, en considérant que cette commande est lancée à partir d'un dépôt git connu de CDS\ncdsctl workflow run\n\n# Vous pouvez explicitement renseigner le projet et le workflow\ncdsctl workflow run PRJ_KEY WORKFLOW_NAME\n\n# flags --interactive ou --open-web-browser peuvent vous intéresser, l'aide contient tous les détails\ncdsctl workflow run --help",
-  "cdsctl_part_5": "# Vérifiez le statut d'un run\ncdsctl workflow status\n\n# le flag --track permet d'attendre la fin de l'exécution du workflow\ncdsctl workflow status --track",
-  "cdsctl_part_6": "# Ajoutez un alias git dans votre fichier gitconfig\n$ cat ~/.gitconfig\n[alias]\n   track = !cdsctl workflow status --track",
-  "cdsctl_part_7": "# Mettez à jour votre binaire cdsctl depuis votre plateforme CDS\ncdsctl update\n\n# Mettez à jour depuis la dernière release disponible sur GitHub\ncdsctl update --from-github",
-  "cdsctl_part_8": "# Toutes les commandes ont un flag --format. Ceci facilite l'usage de cdsctl dans vos scripts\n# Formats disponibles: table, json, yaml\ncdsctl user list --format json",
-  "cdsctl_title_1": "1 - Télécharger CDSCTL",
-  "cdsctl_title_2": "2 - Login",
-  "cdsctl_title_3": "3 - Premiers pas avec cdsctl",
-  "cdsctl_title_4": "4 - Lancer un workflow",
-  "cdsctl_title_5": "5 - Vérifier le statut d'un workflow",
-  "cdsctl_title_6_info_1": "Imaginez maintenant que vous développiez quelque chose et que vous avez un workflow CDS lancé à chaque git push",
-  "cdsctl_title_6_info_2": "La commande `git track` permet de voir dans votre console le statut du workflow en cours, sans voir besoin d'ouvrir un navigateur web.",
-  "cdsctl_title_6": "6 - Git Track",
-  "cdsctl_title_7": "7 - Mettre à jour cdsctl",
-  "cdsctl_title_8": "8 - Automatisation",
-  "commit_author": "Auteur",
-  "commit_date": "Date",
-  "commit_id": "ID",
-  "commit_message": "Message",
-  "common_id": "ID",
-  "common_action": "Action",
-  "common_admin": "Administration",
-  "common_advanced": "Options avancées",
-  "common_all": "Tout",
-  "common_read": "Lecture",
-  "common_write": "Écriture",
-  "common_application_title": "Application : ",
-  "common_application": "Application",
-  "common_applications": "Applications",
-  "common_artifact": "Artefact",
-  "common_artifacts": "Artefacts",
-  "common_audit": "Audit",
-  "common_begin": "Début",
-  "common_cancel": "Annuler",
-  "common_capabilities": "Capacités",
-  "common_cds_documentation": "Documentation CDS",
-  "common_click_here": "Cliquez ici",
-  "common_click_more_informations": "Cliquez ici pour plus d'informations.",
-  "common_clone": "Cloner",
-  "common_close": "Fermer",
-  "common_color": "Couleur",
-  "common_commit": "Changement",
-  "common_commits": "Changements",
-  "common_copy_clipboard": "Copier dans le presse-papier",
-  "common_create_repo": "Depuis un dépôt",
-  "common_create_template": "Depuis un modèle",
-  "common_create": "Créer",
-  "common_created_by": "Créé par",
-  "common_created": "Date de création",
-  "common_date_end": "Date de fin",
-  "common_date_start": "Date de début",
-  "common_deleted": "Supprimé avec succès",
-  "common_deprecated": "Déprécié",
-  "common_description": "Description",
-  "common_details": "Détails",
-  "common_diff_to_much_item": "L'audit ne peut être affiché entièrement.",
-  "common_disabled_f": "Désactivée",
-  "common_documentation": "Documentation",
-  "common_done": "Terminé",
-  "common_duration_title": "Durée : ",
-  "common_edit_as_code": "Éditer en code",
-  "common_edit_ui": "Éditer avec l'interface",
-  "common_enable": "Actif",
-  "common_enabled_f": "Activée",
-  "common_end": "Fin",
-  "common_environment_title": "Environnement : ",
-  "common_environment": "Environnement",
-  "common_environments": "Environnements",
-  "common_error": "Erreur",
-  "common_favorites_all": "Tous les favoris",
-  "common_favorites_delete": "Supprimer des favoris",
-  "common_favorites_updated": "Favoris mis à jour avec succès",
-  "common_file_too_large": "Votre fichier est trop volumineux (max 100Ko)",
-  "common_filter": "Rechercher...",
-  "common_find_help": "Vous trouverez peut être de l'aide",
-  "common_format": "Formatter",
-  "common_group": "Groupe",
-  "common_help_no_group": "Aucun groupe n'a été trouvé, vous ne pourrez pas sauvegarder la ressource en cours d'édition.",
-  "common_here": "ici",
-  "common_hide_all": "Cacher tout",
-  "common_history": "Historique",
-  "common_home": "Vue d'ensemble",
-  "common_icon": "Icône",
-  "common_import": "Importer",
-  "common_integration_title": "Intégration : ",
-  "common_integration": "Intégration",
-  "common_integrations": "Intégrations",
-  "common_keys": "Clés",
-  "common_label": "Label",
-  "common_labels": "Labels",
-  "common_last_modified": "Dernière modification",
-  "common_link": "Lien",
-  "common_loading_environments": "Chargement des environnements",
-  "common_loading_project": "Chargement du projet...",
-  "common_loading_projects": "Chargement des projets...",
-  "common_loading_workflow": "Chargement du workflow...",
-  "common_loading": "Chargement...",
-  "common_migrate": "Migrer",
-  "common_name": "Nom",
-  "common_no_application": "Il n'y a aucune application liée",
-  "common_no_data": "Aucune donnée trouvée",
-  "common_no_description": "Sans description",
-  "common_no_environment": "Il n'y a aucun environnement lié",
-  "common_no_favorites": "Vous n'avez pas encore ajouté de projets ou de workflows dans vos favoris.",
-  "common_no_pipeline": "Il n'y a aucun pipeline lié",
-  "common_no_workflow": "Il n'y a aucun workflow lié",
-  "common_no": "Non",
-  "common_not_up_to_date": "Mise à jour disponible",
-  "common_notifications": "Notifications",
-  "common_optional": "Optionel",
-  "common_or": "OU",
-  "common_orientation": "Orientation",
-  "common_parameters": "Paramètres",
-  "common_pending": "En attente",
-  "common_permissions": "Permissions",
-  "common_pipeline_start_title": "Début : ",
-  "common_pipeline": "Pipeline",
-  "common_pipelines": "Pipelines",
-  "common_preview": "Prévisualisation",
-  "common_processing": "En cours",
-  "common_project_title": "Projet : ",
-  "common_project": "Projet",
-  "common_projects": "Projets",
-  "common_read_more": "En savoir plus",
-  "common_required": "Obligatoire",
-  "common_requirements": "Pré-requis",
-  "common_rollback": "Effectuer un rollback",
-  "common_run": "Lancer",
-  "common_search": "Rechercher",
-  "common_see_documentation": "Voir documentation",
-  "common_see": "voir",
-  "common_select": "Sélectionner",
-  "common_settings": "Configuration",
-  "common_show_all": "Voir tout",
-  "common_slug": "Slug",
-  "common_split": "Séparé",
-  "common_service": "Service",
-  "common_component": "Component",
-  "common_hostname": "Hostname",
-  "common_session": "Session",
-  "common_consumer": "Consumer",
-  "common_status": "Statut",
-  "common_stop": "Stop",
-  "common_test": "Test",
-  "common_tests": "Tests",
-  "common_trigger_by": "Déclenché par",
-  "common_type": "Type",
-  "common_unified": "Unifié",
-  "common_up_to_date": "À jour",
-  "common_update": "Modifier",
-  "common_usage": "Usage",
-  "common_value": "Value",
-  "common_variables": "Variables",
-  "common_version": "Version",
-  "common_vulnerabilities": "Vulnérabilités",
-  "common_warnings": "Warnings",
-  "common_workflow_title": "Workflow : ",
-  "common_workflow": "Workflow",
-  "common_workflows": "Workflows",
-  "common_yes": "Oui",
-  "common_informations": "Informations",
-  "coverage_branches_title": "Branches couvertes : ",
-  "coverage_functions_title": "Fonctions couvertes : ",
-  "coverage_lines_title": "Lignes couvertes : ",
-  "coverage_trend_current_branch": "Tendance par rapport au build précédent",
-  "coverage_trend_default_branch": "Tendance par rapport à la branche par défaut",
-  "danger_zone": "Zone dangereuse",
-  "deployment_integration_name": "Intégration / déploiement",
-  "downloads_title": "Téléchargements",
-  "environment_ascode_updated": "Brouillon mis a jour",
-  "environment_clone_placeholder": "Nom du nouvel environnement",
-  "environment_cloned": "Environnement cloné sans les variables mots de passe et les clés",
-  "environment_create": "Ajouter un environnement",
-  "environment_created": "Environnement créé",
-  "environment_delete_description": "Une fois l'environnement supprimé, il n'y a pas de retour possible.",
-  "environment_delete_label": "Suppression de l'environnement",
-  "environment_deleted": "Environnement supprimé",
-  "environment_from_repository": "Environnement importé depuis {{repo}}",
-  "environment_name_error": "Nom d'environnement invalide. Pattern autorisé: a-zA-Z0-9._-",
-  "environment_name": "Nom de l'environnement",
-  "environment_no_usage": "L'application n'est pas utilisée",
-  "environment_no": "Il n'y a aucun environnement défini",
-  "environment_rename_title": "Renommer l'environnement: ",
-  "environment_renamed": "Environnement renommé",
-  "environment_title_clone": "Cloner l'environnement",
-  "environment_variable_form_title": "Ajouter une variable",
-  "environment_variable_list_title": "Liste des variables",
-  "favorite_add_btn": "Ajouter à mes favoris",
-  "favorite_add": "Ajouter un favori",
-  "favorite_project_select": "Sélectionnez un projet",
-  "favorite_select_type": "Sélectionnez un type de favoris",
-  "favorite_title": "Gestion des favoris",
-  "favorite_workflow_select": "Sélectionnez un workflow",
-  "filter_list_click": "Cliquer pour filtrer la liste",
-  "filter": "Filtrer",
-  "graph_coverage_title": "Couverture",
-  "graph_coverage_y": "Taux de couverture",
-  "graph_unittest_title": "Tests unitaires",
-  "graph_unittest_x": "N° workflow",
-  "graph_unittest_y": "Total",
-  "graph_vulnerability_title": "Vulnérabilités",
-  "graph_vulnerability_x": "N° workflow",
-  "graph_vulnerability_y": "Total",
-  "group_add_admin_saved": "Administrateur ajouté",
-  "group_add_user_saved": "Membre ajouté",
-  "group_added": "Groupe ajouté",
-  "group_btn_set_admin": "administrateur",
-  "group_btn_unset_admin": "administrateur",
-  "group_create_title": "Création d'un groupe",
-  "group_deleted": "Groupe supprimé",
-  "group_list_title": "Groupes",
-  "group_load_group": "Chargement du groupe...",
-  "group_members": "Membres",
-  "group_name": "Nom du groupe",
-  "group_no_member": "Aucun membre dans le groupe",
-  "group_permission": "Permission du groupe",
-  "group_remove_admin_saved": "Administrateur supprimé",
-  "group_remove_user_saved": "Membre supprimé",
-  "group_saved": "Groupe sauvegardé",
-  "group_user_is_admin": "Cet utilisateur est administrateur du groupe",
-  "heatmap_empty": "Aucun évènement déclenché",
-  "heatmap_filter_mute": "Masquer les évènements de ce projet",
-  "heatmap_timestamp": "Date d'évènement",
-  "heatmap": "Heatmap",
-  "hook_task_execs_todo": "Exécutions planifiées",
-  "hook_task_execs_total": "Total des exécutions",
-  "hook_task_execs": "Exécutions",
-  "hook_tasks_summary": "Résumé des tâches du service Hooks",
-  "integration_add_title": "Lier une intégration : ",
-  "integration_configuration": "Configuration",
-  "integration_deployment": "déploiement",
-  "integration_event": "évènement",
-  "integration_hook": "hook",
-  "integration_list_title": "Liste des intégrations: ",
-  "integration_model": "Modèle",
-  "integration_name": "Nom",
-  "integration_no": "Aucune intégration liée",
-  "integration_official_tooltip": "Intégration publique",
-  "integration_storage": "stockage",
-  "job_add_step": "Nouvelle étape",
-  "job_delete": "Supprimer le job",
-  "job_spawn_no_information": "Pas d'information pour l'instant...",
-  "job_spawn_title": "Informations",
-  "key_copied": "La clé a été copiée",
-  "key_copy_public": "Copier la clé publique",
-  "key_or_create": "(Créer)",
-  "key_warning_add_repo_title": "N'oubliez pas d'ajouter votre clé SSH sur votre dépôt",
-  "keys_add_title": "Ajouter une clé",
-  "keys_added": "La clé a été ajoutée",
-  "keys_list_title": "Liste des clés",
-  "keys_loading": "Chargement des clés...",
-  "keys_name": "Nom de la clé",
-  "keys_no": "Il n'y a aucune clé",
-  "keys_pgp_key_help": "Une clé PGP est généralement utile lorsque vous souhaitez tagguer un dépôt Git. Si vous souhaitez utiliser cette clé manuellement, consultez la documentation:",
-  "keys_public": "Clé publique",
-  "keys_removed": "La clé a été supprimée",
-  "keys_ssh_key_help": "Une clé SSH est généralement utile lorsque vous souhaitez cloner un dépôt Git. CDS peut générer une paire de clé, il ne vous restera qu'à copier / coller la clé publique fournie par CDS dans les clés autorisées sur vos dépôts. Si vous souhaitez utiliser cette clé manuellement, consultez la documentation:",
-  "keys_type": "Type de clé",
-  "maintenance_title": "CDS est actuellement en maintenance. Merci de patienter.",
-  "monitoring": "Monitoring",
-  "navbar_actions": "Actions",
-  "navbar_admin_broadcast": "Informations",
-  "navbar_application_in": "Application dans ",
-  "navbar_cdsctl": "Ligne de commande cdsctl",
-  "navbar_dark_mode": "Mode sombre",
-  "navbar_disconnect": "Déconnexion",
-  "navbar_documentation": "Documentation",
-  "navbar_downloads": "Télécharger",
-  "navbar_favorites": "Favoris",
-  "navbar_general": "Global",
-  "navbar_groups": "Groupes",
-  "navbar_home": "Accueil",
-  "navbar_in_project": "projet ",
-  "navbar_mark_read": "Marquer comme lu",
-  "navbar_no_application": "Aucune application",
-  "navbar_no_project": "Aucun projet",
-  "navbar_no_result": "Aucun résultat",
-  "navbar_no_workflow": "Aucun workflow",
-  "navbar_profile": "Votre Compte",
-  "navbar_project_create": "Créer un projet",
-  "navbar_projects": "Projets",
-  "navbar_results": "Résultats de la recherche : ",
-  "navbar_search_help_details": "Tips: 'projectkey/' : affiche tous les workflows et les applications du projet. Vous aurez le même résultat avec 'titre du projet/'.",
-  "navbar_search_help": "Aide",
-  "navbar_search": "Rechercher...",
-  "navbar_see_all": "Voir tout",
-  "navbar_seen_recently": "Vu récemment: ",
-  "navbar_users": "Utilisateurs",
-  "navbar_worker_models": "Modèles de worker",
-  "navbar_workflow_in": "Workflow dans ",
-  "navbar_workflow_templates": "Modèles de workflow",
-  "notification_body": "Contenu",
-  "notification_on_failure": "Envoyer en cas d'échec",
-  "notification_on_start": "Envoyer au demarrage",
-  "notification_on_success": "Envoyer en cas de succès",
-  "notification_on_workflow_failing": "ÉCHEC sur le workflow '{{workflowName}}'",
-  "notification_on_workflow_success": "SUCCÈS sur le workflow '{{workflowName}}'",
-  "notification_title": "Titre",
-  "notification_type": "Type",
-  "parameter_added": "Paramètre ajouté",
-  "parameter_advanced": "Paramètre avancé",
-  "parameter_deleted": "Paramètre supprimé",
-  "parameter_description": "Description",
-  "parameter_git_url": "URL du dépôt git",
-  "parameter_name": "Nom",
-  "parameter_no": "Il n'y a aucun paramètre défini",
-  "parameter_type": "Type",
-  "parameter_updated": "Paramètre mis à jour",
-  "parameter_value": "Valeur",
-  "parameters_advanced": "Paramètres avancés",
-  "permission_added": "Permission ajoutée",
-  "permission_deleted": "Permission supprimée",
-  "permission_read_execute": "Lecture / Exécution",
-  "permission_read_write_execute": "Lecture / Ecriture / Exécution",
-  "permission_read": "Lecture",
-  "permission_updated": "Permission mise à jour",
-  "pipeline_added": "Pipeline créé",
-  "pipeline_administration": "Administration générale du pipeline",
-  "pipeline_commit_no": "Aucun nouveau commit",
-  "pipeline_commits_loading": "Récupération des commits...",
-  "pipeline_create": "Créer un pipeline",
-  "pipeline_delete_description": "Une fois le pipeline supprimé, il n'y a pas de retour possible.",
-  "pipeline_delete_label": "Suppression du pipeline",
-  "pipeline_deleted": "Pipeline supprimé",
-  "pipeline_description": "Description du pipeline",
-  "pipeline_from_repository": "Importé depuis {{repo}}",
-  "pipeline_job_variable_app": "Variables d'application",
-  "pipeline_job_variable_build": "Variables de build",
-  "pipeline_job_variable_cds": "Variables CDS",
-  "pipeline_job_variable_env": "Variables d'environnement",
-  "pipeline_job_variable_git": "Variables git",
-  "pipeline_job_variable_parent": "Variables du pipeline parent",
-  "pipeline_job_variable_pip": "Variables de pipeline",
-  "pipeline_job_variable_project": "Variables de projet",
-  "pipeline_job_variable_title": "Variables du job",
-  "pipeline_job_variable_workflow": "Variables du workflow",
-  "pipeline_job_variables_show_title": "Afficher les variables du job",
-  "pipeline_label_run_with_parameter": "Lancer le pipeline avec des paramètres",
-  "pipeline_label_stop": "Arrêter le pipeline",
-  "pipeline_loading": "Chargement du pipeline...",
-  "pipeline_name_error": "Le nom du pipeline est invalide. Pattern autorisé : a-zA-Z0-9._-",
-  "pipeline_name_new": "Nom du nouveau pipeline",
-  "pipeline_name": "Nom du pipeline",
-  "pipeline_parameters_form_title": "Ajouter un paramètre : ",
-  "pipeline_parameters_list_title": "Liste des paramètres du pipeline : ",
-  "pipeline_parent_commit_to_deploy": "Liste des commits",
-  "pipeline_repository_help_line_1": "Votre pipeline n'a pas été importé depuis votre code.",
-  "pipeline_run_test_failed_title_s": " tests en erreur",
-  "pipeline_run_test_failed_title": " test en erreur",
-  "pipeline_run_test_skipped_title_s": " tests ignorés",
-  "pipeline_run_test_skipped_title": " test ignoré",
-  "pipeline_run_test_total_title_s": " tests",
-  "pipeline_run_test_total_title": " test",
-  "pipeline_stage_edit": "Édition du stage",
-  "pipeline_stage_moved": "Stage déplacé",
-  "pipeline_stage_no": "Le pipeline ne contient aucun stage",
-  "pipeline_stop": "Le pipeline a été arrêté",
-  "pipeline_ascode_updated": "Brouillon mis à jour",
-  "pipeline_updated": "Pipeline mis à jour",
-  "pipeline_wizard_description": "Créer ou sélectionner votre premier pipeline associé à votre workflow",
-  "project_add_group_help": "Les permissions de Lecture / Écriture / Exécution doivent être positionnées sur ce nouveau projet. Vous pouvez choisir un groupe d'utilisateurs existant ou laisser vide pour que CDS créé automatiquement un nouveau groupe, vous en serez l'administrateur.",
-  "project_added": "Projet créé",
-  "project_advanced_title": "Administration du projet",
-  "project_applications_list": "Liste des applications du projet : ",
-  "project_create": "Création d'un projet",
-  "project_delete_description": "Une fois le projet supprimé, il n'y a pas de retour possible.",
-  "project_delete_label": "Suppression du projet",
-  "project_deleted": "Projet supprimé",
-  "project_description": "Description du projet",
-  "project_env_list_title": "Liste des environnements : ",
-  "project_icon": "Icône du projet",
-  "project_key_error": "L'identifiant est obligatoire et doit être en majuscule",
-  "project_key": "Identifiant du projet",
-  "project_label_no": "Il n'y a pas de label configuré dans ce projet.",
-  "project_list": "Tous les projets",
-  "project_name_error": "Le nom du projet est obligatoire",
-  "project_name": "Nom du projet",
-  "project_no_application": "Le projet ne contient aucune application",
-  "project_no_pipeline": "Le projet ne contient aucun pipeline",
-  "project_no_workflow_label": "Aucun workflow n'est lié à ce label",
-  "project_no_workflow": "Le projet ne contient aucun workflow",
-  "project_no": "Il n'y a aucun projet",
-  "project_permission_form_title": "Ajouter une permission : ",
-  "project_permission_form_wizard_new": "(ou créer un nouveau groupe en choisissant le nom)",
-  "project_permission_form_wizard_title": "Choisir un groupe existant - ou laisser vide pour créer automatiquement un nouveau groupe : ",
-  "project_permission_list_title": "Liste des permissions sur le projet : ",
-  "project_permission_propagate_question": "Voulez-vous propager l'ajout de votre nouvelle permission sur tous vos workflows existants ?",
-  "project_permission_propagate_title": "Propagation de la nouvelle permission de projet",
-  "project_pipelines_list": "Liste des pipelines du projet : ",
-  "project_rename_title": "Renommer le projet : ",
-  "project_repoman_created_by": "Lien créé par ",
-  "project_repoman_title": "Lier le projet à un gestionnaire dépôt : ",
-  "project_update_msg_ok": "Le projet a été mis à jour.",
-  "project_update_name_error": "Le nom du projet est obligatoire et doit être au format alphanumérique ([a-zA-Z0-9]*)",
-  "project_updated": "Projet mis à jour",
-  "project_variable_form_title": "Ajouter une nouvelle variable: ",
-  "project_variable_list_title": "Liste des variables du projet: ",
-  "project_vcs_no": "Le projet n'est lié à aucun gestionnaire de dépôt",
-  "project_workflows_list": "Liste des workflows du projet : ",
-  "repo_name": "Nom du dépôt",
-  "repoman_delete_confirm_message": "Êtes-vous sûr de vouloir supprimer ce gestionnaire de dépôt du projet ?",
-  "repoman_delete_confirm_title": "Suppression du gestionnaire de dépôt",
-  "repoman_delete_dependencies_message": "Êtes-vous sûr de vouloir supprimer ce gestionnaire de dépôt du projet ? Il est actuellement lié à ces applications: {{apps}}",
-  "repoman_delete_msg_ok": "Le gestionnaire de dépôt n'est plus lié au projet",
-  "repoman_modal_verif_code_placeholder": "Code de vérification",
-  "repoman_modal_verif_text": "Cliquez sur le lien suivant pour finaliser la connexion entre CDS et le gestionnaire de dépôt : ",
-  "repoman_modal_verif_title": "Autorisation auprès du gestionnaire de dépôt",
-  "repoman_name": "Gestionnaire de dépôt",
-  "repoman_verif_msg_ok": "Le gestionnaire de dépôt est désormais connecté à CDS.",
-  "requirement_add": "Ajouter un pré-requis",
-  "requirement_documentation": "À propos des pré-requis",
-  "requirement_error_model": "Vous ne pouvez pas ajouter plusieurs pré-requis de type modèle",
-  "requirement_error_hostname": "Vous ne pouvez pas ajouter plusieurs pré-requis de type hostname",
-  "requirement_error_region": "Vous ne pouvez pas ajouter plusieurs pré-requis de type region",
-  "requirement_help_binary_0": "Pré-requis type 'binary': CDS choisira un worker possédant ce binaire dans son PATH.",
-  "requirement_help_hostname_0": "Pré-requis type 'hostname': Ce job sera lancé par un worker possédant ce Hostname",
-  "requirement_help_os-architecture_0": "Pré-requis type 'os-architecture': CDS choisira un worker correspondant à l'OS et l'architecture spécifié.",
-  "requirement_help_memory_0": "Pré-requis type 'memory'",
-  "requirement_help_memory_1": "Si vous souhaitez 5Go, entrez la valeur suivante: 4096",
-  "requirement_help_memory_2": "Le prérequis memory est disponible uniquement avec les ",
-  "requirement_help_model_0": "Pré-requis type 'model'",
-  "requirement_help_model_1": "Si vous sélectionnez un Worker Model, CDS lancera votre Job dans une instance de celui-ci",
-  "requirement_help_model_2": "Créer un modèle de worker en utilisant une image depuis Docker Hub",
-  "requirement_help_model_3": "Créer un modèle de worker avec votre propre image docker",
-  "requirement_help_model_4": "Créer un modèle de worker Openstack",
-  "requirement_help_model_5": "En savoir plus",
-  "requirement_help_service_0": "Pré-requis type 'service':", 
-  "requirement_help_service_1": "Note sur le Pré-requis Service",
-  "requirement_help_service_2": "Tutorial - Service Link Requirement Nginx Tutorial",
-  "requirement_help_service_3": "Service Link Requirement PostgreSQL",
-  "requirement_help_service_4": "Vous pouvez forcer la memoire sur un service, exemple: 'CDS_SERVICE_MEMORY=4096'",
-  "requirement_hostname_error": "Le hostname est obligatoire et doit être au format alphanumérique ([a-zA-Z0-9]*)",
-  "requirement_hostname_service": "Hostname :",
-  "requirement_options": "Options :",
-  "requirement_os-architecture_error": "Le champ est obligatoire",
-  "requirement_placeholder_name_os-architecture": "os/arch",
-  "requirement_placeholder_name_service": "HostnameDuService",
-  "requirement_placeholder_opts_model": "Si c'est un modèle de worker type Docker, vous pouvez ajouter des options. Consultez la documentation.",
-  "requirement_placeholder_opts_service": "POSTGRES_USER=myuser\nPOSTGRES_PASSWORD=mypassword",
-  "requirement_placeholder_value_hostname": "votreHostname",
-  "requirement_placeholder_value_model": "Choisissez un modèle de worker",
-  "requirement_settings_worker_model": "Voir les paramètres du Worker Model",
-  "requirement_type": "Type",
-  "requirement_value_service": "Image Docker :",
-  "requirement_value": "Valeur",
-  "service_alert": "Status Alert - vérifier le statut ci-dessous",
-  "service_ok": "Status OK - tout est ok",
-  "service_view_details": "Voir les details",
-  "service_warning": "Status Warning - vérifier le statut ci-dessous",
-  "services_list": "CDS Services",
-  "services": "services",
-  "settings_tips": "Conseils",
-  "stage_added": "Stage ajouté",
-  "stage_deleted": "Stage supprimé",
-  "stage_job_added": "Job ajouté",
-  "stage_job_deleted": "Job supprimé",
-  "stage_job_updated": "Job mis à jour",
-  "stage_updated": "Stage mis à jour",
-  "static_file_name": "Fichiers statiques",
-  "step_add_job": "Ajouter un job",
-  "step_edit_title": "Éditer le titre",
-  "step_nonfinal_no": "Il n'y a aucune étape.",
-  "step_title_duration": "Début: {{start}}, Fin: {{end}}",
-  "timeline_filter_mute": "Masquer les évènements de ce workflow",
-  "timeline_filter_project_select": "Sélectionner un projet",
-  "timeline_filter_project": "Cacher les évènements sur :",
-  "timeline_filter_updated": "Filtre mis à jour",
-  "timeline_loading": "Chargement du fil d'actualité...",
-  "timeline_no": "Le fil d'actualité est vide",
-  "timeline_title": "Fil d'actualité",
-  "toast_http_error_request_id": "Identifiant de la requête:",
-  "ui_updated": "L'interface vient d'être mise à jour. Merci de cliquer ici pour rafraîchir votre page.",
-  "usage_application_list": "Applications utilisées",
-  "usage_environment_list": "Environnements utilisés",
-  "usage_pipeline_list": "Pipelines utilisés",
-  "usage_workflow_list": "Workflows utilisés",
-  "user_deleted": "L'utilisateur est supprimé",
-  "user_is_admin": "Cet utilisateur est un administrateur CDS",
-  "user_label_email": "Adresse e-mail",
-  "user_label_fullname": "Nom complet de l'utilisateur",
-  "user_label_password": "Mot de passe",
-  "user_label_username": "Nom d'utilisateur",
-  "user_list_title": "Utilisateurs",
-  "user_load_user": "Chargement de l'utilisateur...",
-  "user_saved": "Utilisateur sauvegardé",
-  "variable_added": "Variable ajoutée",
-  "variable_deleted": "Variable supprimée",
-  "variable_name": "Nom",
-  "variable_no": "Il n'y a aucune variable définie",
-  "variable_type": "Type de variable",
-  "variable_updated": "Variable mise à jour",
-  "variable_value": "Valeur",
-  "user_label_new_password": "Nouveau mot de passe",
-  "user_label_ring": "Rôle",
-  "user_profile_btn": "Profil",
-  "user_groups_btn": "Groupes",
-  "user_contacts_btn": "Contacts",
-  "user_authentication_btn": "Authentification",
-  "user_group_admin": "Administrateur",
-  "user_group_member": "Membre",
-  "user_group_role": "Rôle",
-  "user_contact_primary": "Par défaut",
-  "user_contact_not_verified": "Non vérifiée",
-  "user_auth_scopes": "Scopes",
-  "user_auth_groups": "Groupes",
-  "user_auth_consumer": "Client",
-  "user_auth_expire_at": "Expire le",
-  "user_auth_revoke_btn": "Révoquer",
-  "user_auth_consumers": "Clients",
-  "user_auth_my_consumers": "Mes clients",
-  "user_auth_sessions": "Sessions",
-  "user_auth_session_current": "Courante",
-  "user_auth_consumer_detach_btn": "Détacher",
-  "user_auth_consumer_delete_btn": "Supprimer",
-  "user_auth_consumer_disabled": "Désactivé",
-  "user_auth_consumer_warning_last_group_removed": "Le dernier groupe a été retiré.",
-  "user_auth_consumer_warning_group_invalid": "Le groupe '{{name}}' a été invalidé.",
-  "user_auth_consumer_warning_group_removed": "Le groupe '{{name}}' a été supprimé.",
-  "auth_consumer_details_modal_title": "Détails pour le client '{{name}}'",
-  "auth_consumer_create_modal_title": "Créer un nouveau client",
-  "auth_consumer_create_modal_info_groups": "Laissez la sélection de groupes vide pour générer un client avec un accès à tous les groupes.",
-  "vcs_connection": "Connexion : ",
-  "vcs_password": "Mot de passe : ",
-  "vcs_pgp_key": "Clé PGP",
-  "vcs_server": "Serveur VCS",
-  "vcs_ssh_key": "Clé SSH",
-  "vcs_user": "Utilisateur : ",
-  "vulnerability_fixin": "Fixé en",
-  "vulnerability_ignore": "Ignorer",
-  "vulnerability_no": "Aucune vulnérabilité détectée",
-  "vulnerability_origin": "Origine",
-  "vulnerability_showmore": "Voir plus",
-  "vulnerability_updated": "Vulnérabilité mise à jour",
-  "vunerability_hide": "Cacher",
-  "warning_application": "{{username}} vient de modifier l'application.",
-  "warning_build_title": "Le build est en succès mais il y a {{nb}} étape(s) optionnelle(s) en erreur",
-  "warning_modal_body": "Un autre utilisateur vient de modifier cette ressource. Vous risquez d'écraser ses modifications.",
-  "warning_modal_title": "Attention - accès concurrents",
-  "warning_pipeline": "{{username}} vient de modifier le pipeline.",
-  "warning_project": "{{username}} vient de modifier le projet.",
-  "warning_workflow": "{{username}} vient de modifier le workflow.",
-  "wizard_app_clone": "Cloner une application",
-  "wizard_app_empty": "Application vide",
-  "wizard_repo_files": "Fichiers trouvés : ",
-  "worker_model_arch": "Architecture",
-  "worker_model_binary_capability": "Binaire",
-  "worker_model_capabilities_need_refresh": "La liste des binaires a besoin d'être rafraîchie par CDS. Dernière mise à jour : ",
-  "worker_model_capabilities_up_to_date": "La liste des binaires est à jour",
-  "worker_model_capabilities": "Binaires présents dans les répertoires de la variable d'environnement PATH",
-  "worker_model_cmd_docker": "Commande",
-  "worker_model_cmd_tooltip": "La commande doit terminer par ./worker",
-  "worker_model_cmd": "Commande principale du worker",
-  "worker_model_deleted": "Modèle de worker supprimé",
-  "worker_model_deprecated_help": "Indique que ce modèle de worker est déprécié",
-  "worker_model_deprecated_tooltip": "Ce modèle de worker est déprécié",
-  "worker_model_disable": "Désactiver",
-  "worker_model_disabled": "Ce modèle de worker est désactivé",
-  "worker_model_enable": "Activer",
-  "worker_model_enabled": "Ce modèle de worker est activé",
-  "worker_model_env_tooltip": "Les variables d'environnement ajoutées ici seront valorisées dans votre worker. Si vous n'avez pas de besoin spécifiques, les valeurs par défauts s'ajouteront automatiquement à la création",
-  "worker_model_env": "Variables d'environnement",
-  "worker_model_error_log": "Sortie console",
-  "worker_model_error": "Ce modèle de worker est en erreur. En sauvegardant, les erreurs seront supprimées.",
-  "worker_model_group": "Groupe",
-  "worker_model_help_capabilities": "Les binaires sont automatiquement découverts par CDS lorsqu'un worker démarre. Si votre instance de CDS utilise une hatchery, tous les modèles de worker sont automatiquement enregistrés.",
-  "worker_model_help_error_docker_1": "Vérifiez que vous avez les droits d'écriture dans $TMPDIR dans votre image docker",
-  "worker_model_help_error_docker_2": "Vérifiez que vous avez curl dans le PATH de l'image docker",
-  "worker_model_help_error_host_1": "Vérifiez que le binaire 'worker' est présent dans votre $PATH, sur le host où vous avez lancez la hatchery locale",
-  "worker_model_help_error_host_2": "Vérifiez que vous avez les droits d'écriture dans $TMPDIR ou modifiez le paramètre 'basedir' de votre hatchery locale",
-  "worker_model_help_error_openstack_1": "Vérifiez que vous avez curl dans le PATH de l'image openstack",
-  "worker_model_help_error_openstack_2": "Vérifiez que vous avez les droits d'écriture dans $TMPDIR ou modifiez le paramètre 'basedir' de votre hatchery openstack",
-  "worker_model_help_error": "Ce que vous pouvez vérifier:",
-  "worker_model_help_howto_link_1": "Créer un modèle de worker en utilisant une image depuis Docker Hub",
-  "worker_model_help_howto_link_2": "Créer un modèle de worker avec votre propre image docker",
-  "worker_model_help_howto_link_3": "Créer un modèle de worker Openstack",
-  "worker_model_help_howtos": "Tutoriels :",
-  "worker_model_help_line_1": "L'objectif de CDS est de démarrer sur demande un worker utilisant un modèle possédant vos pre-requis. Si aucun modèle existant correspond à vos besoins, vous pouvez enregistrer un nouveau modèle de worker.",
-  "worker_model_help_line_2": "L'objectif d'un modèle de worker est de décrire les capacités d'une image (docker, openstack, etc...) en terme d'architecture, binaires / librairies pré-installés",
-  "worker_model_image": "Image",
-  "worker_model_is_deprecated": "Modèle de worker déprécié",
-  "worker_model_last_registration": "Dernier enregistrement",
-  "worker_model_list_title": "Modèles de worker",
-  "worker_model_name": "Nom",
-  "worker_model_no_usage": "Ce modèle de worker n'est pas utilisé explicitement dans un pipeline",
-  "worker_model_official_tooltip": "Modèle de worker officiel maintenu par les administrateurs de CDS",
-  "worker_model_official": "Modèle de worker vérifié (ajouté par un administrateur)",
-  "worker_model_os": "Système d'exploitation",
-  "worker_model_password": "Mot de passe",
-  "worker_model_pattern_help_line_1": "Worker Model Pattern documentation:",
-  "worker_model_pattern_deleted": "Pattern supprimé",
-  "worker_model_pattern_saved": "Pattern sauvegardé",
-  "worker_model_pattern_title": "Patterns des scripts de configuration",
-  "worker_model_post_cmd": "Commande après exécution du worker",
-  "worker_model_pre_cmd": "Commande avant exécution du worker",
-  "worker_model_private_tooltip": "Si votre image provient d'une registry privée qui requiert une authentification",
-  "worker_model_private": "Registry privée",
-  "worker_model_restricted_help": "Ne peut être utilisé que par une hatchery utilisateur - cocher si oui.",
-  "worker_model_restricted": "Restreint",
-  "worker_model_saved": "Modèle sauvegardé",
-  "worker_model_shell_docker": "Commande shell",
-  "worker_model_spawn_error_tooltip": "Modèle en erreur",
-  "worker_model_state": "État du modèle de worker",
-  "worker_model_status": "Statut",
-  "worker_model_type": "Type",
-  "worker_model_unofficial": "Modèle de worker non vérifié (ajouté par un utilisateur)",
-  "worker_model_username": "Nom d'utilisateur",
-  "worker_model_warning": "Merci de vérifier ce modèle, il y a des erreurs dessus",
-  "worker_model": "Modèle de worker",
-  "workflow_added": "Workflow créé",
-  "workflow_as_code_pr_success": "Une demande de revue a été créée :",
-  "workflow_context_invalid": "JSON invalide",
-  "workflow_create_btn": "Créer un workflow",
-  "workflow_create": "Création d'un workflow",
-  "workflow_delete_description": "Une fois le workflow supprimé, il n'y a pas de retour possible.",
-  "workflow_delete_label": "Suppression du workflow",
-  "workflow_deleted_warning": "Attention si vous supprimez ce pipeline avec tout ses enfants vous allez automatiquement supprimer tout votre workflow et sa configuration",
-  "workflow_deleted": "Workflow supprimé",
-  "workflow_description": "Description du workflow",
-  "workflow_edit_as_code": "Éditer en code",
-  "workflow_error_bad_application_name_title": "Application introuvable",
-  "workflow_error_bad_cds_dir_description": "Veuillez vérifier que votre dossier .cds contenant tout vos fichiers de workflow as code a bien été commité et envoyé sur votre dépôt git. Le cas échéant, vérifiez que votre dossier .cds ne se trouve pas dans votre .gitignore",
-  "workflow_error_bad_cds_dir_title": "Dossier .cds introuvable",
-  "workflow_error_bad_environment_name_title": "Environnement introuvable",
-  "workflow_error_bad_integration_name_title": "Intégration introuvable",
-  "workflow_error_bad_pipeline_name_title": "Pipeline introuvable",
-  "workflow_error_bad_vcs_strategy_description": "Veuillez vérifier que la clé SSH avec laquelle vous avez lié votre application à votre dépôt git est bien active et indiqué sur votre dépôt git pour avoir les accès. Veuillez aussi vérifier que vous ayez bien une clé ssh de mentionnée dans votre fichier yaml d'application pointant sur votre dépôt git si vous avez mis le mode de clone SSH.",
-  "workflow_error_bad_vcs_strategy_title": "Mauvaise configuration des paramètres VCS de votre application",
-  "workflow_error_unknown_key_description": "Veuillez vérifier que la clé SSH avec laquelle vous avez lié votre application à votre dépôt git est bien active et indiqué sur votre dépôt git pour avoir les accès",
-  "workflow_error_unknown_key_title": "Clé introuvable",
-  "workflow_event_explanation": "_Vous pouvez configurer ici une ou plusieurs intégrations de type `Event`. Cela vous permet d'envoyer toutes les données techniques dans un backend afin de les rendre accessibles par des applications tierces telles que Kafka ou ElasticSearch. Consultez la documentation [Event Notifications](https://ovh.github.io/cds/docs/concepts/workflow/notifications/) pour plus d'informations._",
-  "workflow_event_form": "Intégrations - Évènements",
-  "workflow_first_execution": "Date de première exécution",
-  "workflow_from_repository_btn": "Import du workflow depuis un gestionnaire de dépôt",
-  "workflow_from_repository_pending": "Acceptez la Pull Request suivante pour passer votre workflow as-code",
-  "workflow_from_repository": "Workflow importé depuis {{repo}}",
-  "workflow_from_template_btn": "Génération du workflow depuis un modèle",
-  "workflow_from_template": "Workflow importé depuis le modèle",
-  "workflow_hook_delete_msg": "Êtes-vous certain de vouloir supprimer ce hook ?",
-  "workflow_hook_delete_title": "Supprimer le hook",
-  "workflow_hook_log_title": "Logs du hook",
-  "workflow_hook_log_workflow_run": "Numéro de run",
-  "workflow_icon": "Icône du workflow",
-  "workflow_last_execution": "Date de dernière exécution",
-  "workflow_loading": "Chargement du workflow...",
-  "workflow_logs_all": "Afficher tous les logs",
-  "workflow_logs_pretty_ansi": "Afficher les logs avec l'interpréteur ANSI",
-  "workflow_logs_pretty_html": "Afficher les logs avec l'interpréteur HTML",
-  "workflow_logs_raw": "Afficher les logs bruts",
-  "workflow_modal_change_view_confirm": "Êtes-vous sûr de vouloir perdre vos changements ?",
-  "workflow_name_error_duplicate": "Le nom de ce workflow existe déjà dans votre projet",
-  "workflow_name_error": "Le nom du workflow doit suivre ce pattern ^[a-zA-Z0-9._-]{1,}$",
-  "workflow_name": "Nom du workflow",
-  "workflow_no_event_integration": "Vous n'avez pas d'intégration de type évènement sur votre projet.",
-  "workflow_no_run_found": "Aucune exécution trouvée pour ce workflow",
-  "workflow_node_condition_advanced": "Avancé",
-  "workflow_node_condition_duplicate": "Vous testez plusieurs fois une même variable",
-  "workflow_node_condition_empty": "Certaines conditions de lancement sont vides",
-  "workflow_node_condition_label": "Conditions de lancement",
-  "workflow_node_condition_lua_help": "(doit retourner un booléen)",
-  "workflow_node_condition_lua_title": "Script Lua",
-  "workflow_node_condition_warning": "Attention si vous avez des conditions basiques et avancées, seulement les conditions avancées seront éffectives.",
-  "workflow_node_context_label": "Contexte d'exécution",
-  "workflow_node_context_payload_read_only": "Payload utilisé (lecture seule)",
-  "workflow_node_context_payload": "Payload par défaut",
-  "workflow_node_context_pipeline_parameter": "Paramètres du pipeline",
-  "workflow_node_delete_alert_soft": "Enlever uniquement ce pipeline",
-  "workflow_node_delete_alert": "ATTENTION, cela va supprimer le pipeline et tout ce qu'il déclenche",
-  "workflow_node_delete_title": "Suppression de '{{node}}'",
-  "workflow_node_fork_add": "Ajouter un embranchement",
-  "workflow_node_form_title": "Pipeline à déclencher",
-  "workflow_node_hook_add": "Ajouter un hook",
-  "workflow_node_hook_details_modal_title": "Détails du hook",
-  "workflow_node_hook_form_config": "Configuration du hook",
-  "workflow_node_hook_form_title": "Type de hook",
-  "workflow_node_hook_modal_title": "Édition du hook",
-  "workflow_node_hook_no_configuration": "Aucune configuration n'est nécessaire",
-  "workflow_node_hook_select": "Sélectionner un type de hook",
-  "workflow_node_input": "Paramètres de lancement",
-  "workflow_node_join_add": "Ajouter une jointure",
-  "workflow_node_join_link": "Lier à une jointure",
-  "workflow_node_menu_edit_pipeline": "Éditer le pipeline",
-  "workflow_node_menu_edit_ro": "Voir la configuration du contexte",
-  "workflow_node_menu_edit": "Éditer le contexte",
-  "workflow_node_outgoing_hook": "Ajouter un hook sortant",
-  "workflow_node_parent": "Ajouter un pipeline parent",
-  "workflow_node_permissions_form_title": "Ajouter une permission sur le nœud du workflow",
-  "workflow_node_trigger_add": "Ajouter un pipeline",
-  "workflow_node_trigger_condition_name": "Nom de la variable",
-  "workflow_node_trigger_condition_no": "Il n'y a aucune condition de déclenchement",
-  "workflow_node_trigger_condition_operator": "Opération",
-  "workflow_node_trigger_condition_value": "Valeur attendue",
-  "workflow_node_trigger_title": "Trigger à partir de {{pip}}",
-  "workflow_node_type_outgoing_hook": "Hook Sortant",
-  "workflow_notification_body": "Message",
-  "workflow_notification_conditions": "Conditions",
-  "workflow_notification_copy": "Copié",
-  "workflow_notification_email_user": "E-mails",
-  "workflow_notification_explanation": "_Une notification utilisateur peut être utile pour signaler le status d'un workflow en fonction de son état. Chaque pipeline d'un workflow peut donner lieu à une notification en fonction de sont statut en `Succès`, `En Echec` ou sur changement de statut. Le message envoyé aux destinataires peut être paramétré à l'aide de [variables CDS](https://ovh.github.io/cds/docs/concepts/variables/). Les notifications de type mail peuvent également contenir du HTML, cf. documentation [User Notifications](https://ovh.github.io/cds/docs/concepts/workflow/notifications/)._",
-  "workflow_notification_form": "Ajouter une notification",
-  "workflow_notification_jabber_user": "Utilisateurs jabber",
-  "workflow_notification_on_failure": "Envoyer en cas d'erreur",
-  "workflow_notification_on_start": "Envoyer au lancement",
-  "workflow_notification_on_success": "Envoyer en cas de succès",
-  "workflow_notification_pipeline": "Pipelines",
-  "workflow_notification_title": "Titre",
-  "workflow_notification_to_group": "Envoyer au groupe",
-  "workflow_notification_to_initiator": "Envoyer à l'initiateur",
-  "workflow_notification_type": "Type de notification",
-  "workflow_notification_vcs_status_enabled": "Envoyer un status sur le commit",
-  "workflow_notification_vcs_comment_always": "Toujours envoyer",
-  "workflow_notification_vcs_comment_enabled": "Commentaire de pull-request activé",
-  "workflow_notification_vcs_pr_comment_body": "Contenu du commentaire de pull-request",
-  "workflow_permission_form_title": "Ajouter une permission sur le workflow",
-  "workflow_permission_list_title": "Liste des permissions sur le workflow",
-  "workflow_preview_mode": "Votre workflow est dans un état de prévisualisation",
-  "workflow_repository_help_line_1": "Votre workflow n'a pas été importé depuis votre code.",
-  "workflow_repository_help_line_2": "Gérez votre workflow \"as code\" depuis votre gestionnaire de dépôt pour modifier automatiquement celui-ci avec les changements sur vos branches.",
-  "workflow_resync_vcs_tooltip": "Cette action va renvoyer les statuts de vos pipelines à votre gestionnaire de dépots distants",
-  "workflow_resync_vcs": "Resynchroniser les statuts VCS",
-  "workflow_resync": "Resynchroniser le workflow",
-  "workflow_retention_maxruns": "Nombre maximum d'exécutions du workflow: ",
-  "workflow_retention_maxruns_admin": "Vous pouvez contacter un administrateur pour changer cette valeur",
-  "workflow_retention_policy": "Politique de rétention des éxécutions du workflow",
-  "workflow_retention_variables": "Variables disponibles: {{vars}}",
-  "workflow_retention_result_title": "Les exécutions suivantes seront gardées",
-  "workflow_run_delayed": "L'exécution du workflow a été reportée",
-  "workflow_run_only_failed": "Uniquement les jobs en erreur",
-  "workflow_root_context_application": "Application (facultatif)",
-  "workflow_root_context_environment": "Environnement (facultatif)",
-  "workflow_root_context_integration": "Intégration (facultatif)",
-  "workflow_root_context_mutex": "Limiter à une execution à la fois",
-  "workflow_root_context_pipeline": "Pipeline",
-  "workflow_run_conditions_hook": "Attention, vous ne pouvez pas utiliser des conditions de lancement utilisant {{.cds.build...}} car la vérification des conditions s'effectue avant la création d'un run.",
-  "workflow_run_loading": "Chargement des exécutions",
-  "workflow_run_node_job_queued": "Attente depuis {{time}}",
-  "workflow_run_readonly_title": "Exécution du workflow en mode lecture seul",
-  "workflow_run_readonly_content": "Cette éxécution du workflow est en mode lecture seule, vous ne pouvez plus la lancer.",
-  "workflow_run_resync_help": "Resynchronisez ce run par rapport aux derniers changements sur vos pipelines. Ne resynchronise pas les variables de projet.",
-  "workflow_run_scheduling": "Construction du workflow",
-  "workflow_run_with_parameters": "Lancer le workflow",
-  "workflow_runnumber_title": "Numéro de run courant",
-  "workflow_sidebar_tag_zone": "Tags à afficher dans la sidebar (ordonnés par priorité)",
-  "workflow_start_with_fork": "Commencer avec un embranchement",
-  "workflow_stopped": "Workflow arrêté",
-  "workflow_tag_dragdrop": "Drag & drop sur les tags pour modifier l'ordre",
-  "workflow_template_apply_detach": "Détacher le workflow généré du template.",
-  "workflow_template_btn_create_bulk": "Mettre à jour en masse",
-  "workflow_template_btn_run_bulk": "Lancer la mise à jour en masse",
-  "workflow_template_bulk_create_title": "Application d'un modèle de workflow en masse",
-  "workflow_template_bulk_fill_parameters": "Saisie des paramètres",
-  "workflow_template_bulk_fill_run": "Exécution",
-  "workflow_template_bulk_select_instances": "Sélection des instances",
-  "workflow_template_bulk_info_line_ascode": "Certaines des instances sélectionnées font référence à des workflows \"ascode\". Une \"pull request\" sera créée pour chaque workflow sur son gestionnaire de dépôt. Vous pouvez renseigner le nom de la branche et le message de commit à utiliser.",
-  "workflow_template_change_message": "Ajoutez une description optionelle de vos changements...",
-  "workflow_template_created": "Modèle de workflow créé",
-  "workflow_template_deleted": "Modèle de workflow supprimé",
-  "workflow_template_detach": "Détacher",
-  "workflow_template_diff_application": "Application {{number}}",
-  "workflow_template_diff_environment": "Environnement {{number}}",
-  "workflow_template_diff_pipeline": "Pipeline {{number}}",
-  "workflow_template_diff_workflow": "Workflow",
-  "workflow_template_help_add_from": "Le modèle de workflow courant a été initialisé à partir des données du workflow \"{{workflowName}}\" dans le projet \"{{projectKey}}\". Ces données ne seront pas enregistrées tant que le modèle n'aura pas été sauvegardé.",
-  "workflow_template_help_edit_from": "Le modèle de workflow est synchronisé à partir d'une URL, vous ne pouvez pas modifier ses informations.",
-  "workflow_template_help_line_1": "Votre workflow n'a pas été généré depuis un modèle de workflow.",
-  "workflow_template_help_line_2": "Un modèle de workflow vous permet d'initialiser et de mettre à jour un workflow et ses composants rapidement.",
-  "workflow_template_help_line_3": "Vous pouvez initialiser un nouveau modèle depuis le workflow courant.",
-  "workflow_template_import_from_url": "Importer depuis une URL",
-  "workflow_template_imported_from_url": "Importé depuis une URL",
-  "workflow_template_init_from_workflow": "Initialiser un modèle",
-  "workflow_template_instances": "Instances",
-  "workflow_template_instances_with_nb": "Instances ({{nb}})",
-  "workflow_template_no_usage": "Ce modèle de workflow n'est pas utilisé sur l'un de vos workflows",
-  "workflow_template_not_imported_yet": "Pas encore importé",
-  "workflow_template_saved": "Modèle de workflow sauvegardé",
-  "workflow_template_update_info_btn": "Afficher les différences de la nouvelle version",
-  "workflow_template_update_info_line_1": "Appliquer le template va écraser le workflow existant ainsi que ses composants avec la dernière version du modèle sélectionné.",
-  "workflow_template_update_info_line_2": "Appliquer le template va créer une 'pull request' sur le gestionnaire de dépôt qui contient votre workflow, avec la dernière version du modèle sélectionné.",
-  "workflow_template_update_title": "Mise à jour du workflow à partir du modèle",
-  "workflow_template": "Modèle de workflow",
-  "workflow_templates": "Modèles de workflow",
-  "workflow_template_param_select_key": "Sélectionnez une clé",
-  "workflow_update_name_error": "Nom de workflow invalide. Pattern autorisé : ^[a-zA-Z0-9._-]{1,}$",
-  "workflow_ascode_updated": "Brouillon mis à jour",
-  "workflow_updated": "Workflow mis à jour",
-  "workflow_vcs_resynced": "Statuts du dépot distant resynchronisés",
-  "workflow_warning_deprecated_yaml": "Configuration yaml du workflow dépréciée",
-  "workflow_wizard_description": "Choisissez vos options de workflow",
-  "workflow_wizard_repo_analyse": "Analyse du dépôt...",
-  "workflow_wizard_select_repo_loading": "Chargement des dépots...",
-  "workflow_wizard_select_repo_man_add": "Ou ajouter un gestionnaire de dépôt",
-  "workflow_wizard_select_repo_man": "Sélectionner un gestionnaire de dépôt",
-  "workflow_wizard_select_repo": "Sélectionner un dépot",
-  "workflow_wizard_select_template": "Sélectionner un modèle de workflow",
-  "workfow_tag_no": "Aucun tag"
-}


Test patch:
diff --git a/engine/api/application/application_parser_test.go b/engine/api/application/application_parser_test.go
new file mode 100644
index 000000000..d1d76b3ab
--- /dev/null
+++ b/engine/api/application/application_parser_test.go
@@ -0,0 +1,105 @@
+package application_test
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/ovh/cds/engine/api/action"
+	"github.com/ovh/cds/engine/api/application"
+	"github.com/ovh/cds/engine/api/ascode"
+	"github.com/ovh/cds/engine/api/test"
+	"github.com/ovh/cds/engine/api/test/assets"
+	"github.com/ovh/cds/sdk"
+	"github.com/ovh/cds/sdk/exportentities"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"gopkg.in/yaml.v2"
+)
+
+func TestParseAndImport(t *testing.T) {
+	db, cache := test.SetupPG(t)
+	u, _ := assets.InsertAdminUser(t, db)
+
+	key := sdk.RandomString(10)
+	appName := sdk.RandomString(10)
+	proj := assets.InsertTestProject(t, db, cache, key, key)
+	app1 := sdk.Application{
+		Name:           appName,
+		FromRepository: "foo",
+	}
+	require.NoError(t, application.Insert(db, *proj, &app1))
+
+	var eapp = new(exportentities.Application)
+
+	body := []byte(`
+version: v1.0
+name: ` + appName + `
+`)
+	errapp := yaml.Unmarshal(body, eapp)
+	require.NoError(t, errapp)
+
+	// try to import without force, it must give an error
+	_, _, _, globalError := application.ParseAndImport(context.TODO(), db, cache, *proj, eapp, application.ImportOptions{Force: false}, nil, u)
+	require.Error(t, globalError)
+
+	// try to import with force, but with another repository, it must give an error
+	_, _, _, globalError2 := application.ParseAndImport(context.TODO(), db, cache, *proj, eapp, application.ImportOptions{Force: true, FromRepository: "bar"}, nil, u)
+	require.Error(t, globalError2)
+
+	// try to import with force, without a repo, it's ok
+	_, _, _, globalError3 := application.ParseAndImport(context.TODO(), db, cache, *proj, eapp, application.ImportOptions{Force: true}, nil, u)
+	require.NoError(t, globalError3)
+
+}
+
+func TestParseAndImportAsCodeEvent(t *testing.T) {
+	db, cache := test.SetupPG(t)
+	u, _ := assets.InsertAdminUser(t, db)
+
+	key := sdk.RandomString(10)
+	appName := sdk.RandomString(10)
+	proj := assets.InsertTestProject(t, db, cache, key, key)
+	app1 := sdk.Application{
+		Name:           appName,
+		FromRepository: "myfoorepo",
+	}
+	require.NoError(t, application.Insert(db, *proj, &app1))
+
+	var eapp = new(exportentities.Application)
+
+	body := []byte(`
+version: v1.0
+name: ` + appName + `
+`)
+	errapp := yaml.Unmarshal(body, eapp)
+	require.NoError(t, errapp)
+
+	require.NoError(t, action.CreateBuiltinActions(db))
+	wf := assets.InsertTestWorkflow(t, db, cache, proj, "workflow1")
+
+	// Add some events to resync
+	asCodeEvent := sdk.AsCodeEvent{
+		WorkflowID: wf.ID,
+		Username:   u.GetUsername(),
+		CreateDate: time.Now(),
+		FromRepo:   "myfoorepo",
+		Data: sdk.AsCodeEventData{
+			Applications: map[int64]string{
+				app1.ID: app1.Name,
+			},
+		},
+	}
+
+	assert.NoError(t, ascode.UpsertEvent(db, &asCodeEvent))
+	events, err := ascode.LoadEventsByWorkflowID(context.TODO(), db, wf.ID)
+	assert.Equal(t, 1, len(events))
+
+	// try to import with force, without a repo, it's ok
+	_, _, _, globalError3 := application.ParseAndImport(context.TODO(), db, cache, *proj, eapp, application.ImportOptions{Force: true}, nil, u)
+	require.NoError(t, globalError3)
+
+	events, err = ascode.LoadEventsByWorkflowID(context.TODO(), db, wf.ID)
+	assert.NoError(t, err)
+	assert.Equal(t, 0, len(events))
+}
diff --git a/engine/api/ascode_test.go b/engine/api/ascode_test.go
index 32764412a..9bdad0cac 100644
--- a/engine/api/ascode_test.go
+++ b/engine/api/ascode_test.go
@@ -48,7 +48,7 @@ func writeError(w *http.Response, err error) (*http.Response, error) {
 	body := new(bytes.Buffer)
 	enc := json.NewEncoder(body)
 	w.Body = ioutil.NopCloser(body)
-	sdkErr := sdk.ExtractHTTPError(err, "")
+	sdkErr := sdk.ExtractHTTPError(err)
 	enc.Encode(sdkErr)
 	w.StatusCode = sdkErr.Status
 	return w, sdkErr
diff --git a/engine/api/environment/environment_parser_test.go b/engine/api/environment/environment_parser_test.go
new file mode 100644
index 000000000..3e7be62c5
--- /dev/null
+++ b/engine/api/environment/environment_parser_test.go
@@ -0,0 +1,104 @@
+package environment_test
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/ovh/cds/engine/api/action"
+	"github.com/ovh/cds/engine/api/ascode"
+	"github.com/ovh/cds/engine/api/environment"
+	"github.com/ovh/cds/engine/api/test"
+	"github.com/ovh/cds/engine/api/test/assets"
+	"github.com/ovh/cds/sdk"
+	"github.com/ovh/cds/sdk/exportentities"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"gopkg.in/yaml.v2"
+)
+
+func TestParseAndImport(t *testing.T) {
+	db, cache := test.SetupPG(t)
+	u, _ := assets.InsertAdminUser(t, db)
+
+	key := sdk.RandomString(10)
+	envName := sdk.RandomString(10)
+	proj := assets.InsertTestProject(t, db, cache, key, key)
+	env1 := sdk.Environment{
+		Name:           envName,
+		FromRepository: "foo",
+		ProjectID:      proj.ID,
+		ProjectKey:     proj.Key,
+	}
+	require.NoError(t, environment.InsertEnvironment(db, &env1))
+
+	var eenv = new(exportentities.Environment)
+
+	body := []byte(`
+version: v1.0
+name: ` + envName + `
+`)
+	errenv := yaml.Unmarshal(body, eenv)
+	require.NoError(t, errenv)
+
+	_, _, _, globalError := environment.ParseAndImport(context.TODO(), db, *proj, *eenv, environment.ImportOptions{Force: false}, nil, u)
+	require.Error(t, globalError)
+
+	_, _, _, globalError2 := environment.ParseAndImport(context.TODO(), db, *proj, *eenv, environment.ImportOptions{Force: true, FromRepository: "bar"}, nil, u)
+	require.Error(t, globalError2)
+
+	_, _, _, globalError3 := environment.ParseAndImport(context.TODO(), db, *proj, *eenv, environment.ImportOptions{Force: true}, nil, u)
+	require.NoError(t, globalError3)
+}
+func TestParseAndImportCleanAsCode(t *testing.T) {
+	db, cache := test.SetupPG(t)
+	u, _ := assets.InsertAdminUser(t, db)
+
+	key := sdk.RandomString(10)
+	envName := sdk.RandomString(10)
+	proj := assets.InsertTestProject(t, db, cache, key, key)
+	env1 := sdk.Environment{
+		Name:           envName,
+		FromRepository: "myfoorepoenv",
+		ProjectID:      proj.ID,
+		ProjectKey:     proj.Key,
+	}
+	require.NoError(t, environment.InsertEnvironment(db, &env1))
+
+	var eenv = new(exportentities.Environment)
+
+	body := []byte(`
+version: v1.0
+name: ` + envName + `
+`)
+	errenv := yaml.Unmarshal(body, eenv)
+	require.NoError(t, errenv)
+
+	require.NoError(t, action.CreateBuiltinActions(db))
+	wf := assets.InsertTestWorkflow(t, db, cache, proj, "workflow1")
+
+	// Add some events to resync
+	asCodeEvent := sdk.AsCodeEvent{
+		WorkflowID: wf.ID,
+		Username:   u.GetUsername(),
+		CreateDate: time.Now(),
+		FromRepo:   "myfoorepoenv",
+		Data: sdk.AsCodeEventData{
+			Environments: map[int64]string{
+				env1.ID: env1.Name,
+			},
+		},
+	}
+
+	assert.NoError(t, ascode.UpsertEvent(db, &asCodeEvent))
+	events, err := ascode.LoadEventsByWorkflowID(context.TODO(), db, wf.ID)
+	assert.Equal(t, 1, len(events))
+
+	// try to import with force, without a repo, it's ok
+	_, _, _, globalError3 := environment.ParseAndImport(context.TODO(), db, *proj, *eenv, environment.ImportOptions{Force: true}, nil, u)
+	require.NoError(t, globalError3)
+
+	events, err = ascode.LoadEventsByWorkflowID(context.TODO(), db, wf.ID)
+	assert.NoError(t, err)
+	assert.Equal(t, 0, len(events))
+}
diff --git a/engine/api/pipeline/pipeline_importer_test.go b/engine/api/pipeline/pipeline_importer_test.go
index 869e0b607..0e8777f7c 100644
--- a/engine/api/pipeline/pipeline_importer_test.go
+++ b/engine/api/pipeline/pipeline_importer_test.go
@@ -45,7 +45,7 @@ func testImportUpdate(t *testing.T, db gorp.SqlExecutor, store cache.Store, tt t
 				done <- true
 				return
 			}
-			log.Debug("[TEST] %s >>> %s", tt.name, msg.String("en"))
+			log.Debug("[TEST] %s >>> %s", tt.name, msg.String())
 		}
 	}()
 
diff --git a/engine/api/pipeline/pipeline_parser_test.go b/engine/api/pipeline/pipeline_parser_test.go
new file mode 100644
index 000000000..669103722
--- /dev/null
+++ b/engine/api/pipeline/pipeline_parser_test.go
@@ -0,0 +1,104 @@
+package pipeline_test
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/ovh/cds/engine/api/action"
+	"github.com/ovh/cds/engine/api/ascode"
+	"github.com/ovh/cds/engine/api/pipeline"
+	"github.com/ovh/cds/engine/api/test"
+	"github.com/ovh/cds/engine/api/test/assets"
+	"github.com/ovh/cds/sdk"
+	"github.com/ovh/cds/sdk/exportentities"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"gopkg.in/yaml.v2"
+)
+
+func TestParseAndImport(t *testing.T) {
+	db, cache := test.SetupPG(t)
+	u, _ := assets.InsertAdminUser(t, db)
+
+	key := sdk.RandomString(10)
+	pipName := sdk.RandomString(10)
+	proj := assets.InsertTestProject(t, db, cache, key, key)
+	pip1 := sdk.Pipeline{
+		Name:           pipName,
+		FromRepository: "foo",
+		ProjectID:      proj.ID,
+		ProjectKey:     proj.Key,
+	}
+	require.NoError(t, pipeline.InsertPipeline(db, &pip1))
+
+	var epip = new(exportentities.PipelineV1)
+
+	body := []byte(`
+version: v1.0
+name: ` + pipName + `
+`)
+	errenv := yaml.Unmarshal(body, epip)
+	require.NoError(t, errenv)
+
+	_, _, globalError := pipeline.ParseAndImport(context.TODO(), db, cache, *proj, *epip, u, pipeline.ImportOptions{Force: false})
+	require.Error(t, globalError)
+
+	_, _, globalError2 := pipeline.ParseAndImport(context.TODO(), db, cache, *proj, *epip, u, pipeline.ImportOptions{Force: true, FromRepository: "bar"})
+	require.Error(t, globalError2)
+
+	_, _, globalError3 := pipeline.ParseAndImport(context.TODO(), db, cache, *proj, *epip, u, pipeline.ImportOptions{Force: true})
+	require.NoError(t, globalError3)
+}
+func TestParseAndImportCleanAsCode(t *testing.T) {
+	db, cache := test.SetupPG(t)
+	u, _ := assets.InsertAdminUser(t, db)
+
+	key := sdk.RandomString(10)
+	pipName := sdk.RandomString(10)
+	proj := assets.InsertTestProject(t, db, cache, key, key)
+	pip1 := sdk.Pipeline{
+		Name:           pipName,
+		FromRepository: "myfoorepoenv",
+		ProjectID:      proj.ID,
+		ProjectKey:     proj.Key,
+	}
+	require.NoError(t, pipeline.InsertPipeline(db, &pip1))
+
+	var epip = new(exportentities.PipelineV1)
+
+	body := []byte(`
+version: v1.0
+name: ` + pipName + `
+`)
+	errenv := yaml.Unmarshal(body, epip)
+	require.NoError(t, errenv)
+
+	require.NoError(t, action.CreateBuiltinActions(db))
+	wf := assets.InsertTestWorkflow(t, db, cache, proj, "workflow1")
+
+	// Add some events to resync
+	asCodeEvent := sdk.AsCodeEvent{
+		WorkflowID: wf.ID,
+		Username:   u.GetUsername(),
+		CreateDate: time.Now(),
+		FromRepo:   "myfoorepoenv",
+		Data: sdk.AsCodeEventData{
+			Pipelines: map[int64]string{
+				pip1.ID: pip1.Name,
+			},
+		},
+	}
+
+	assert.NoError(t, ascode.UpsertEvent(db, &asCodeEvent))
+	events, err := ascode.LoadEventsByWorkflowID(context.TODO(), db, wf.ID)
+	assert.Equal(t, 1, len(events))
+
+	// try to import with force, without a repo, it's ok
+	_, _, globalError3 := pipeline.ParseAndImport(context.TODO(), db, cache, *proj, *epip, u, pipeline.ImportOptions{Force: true})
+	require.NoError(t, globalError3)
+
+	events, err = ascode.LoadEventsByWorkflowID(context.TODO(), db, wf.ID)
+	assert.NoError(t, err)
+	assert.Equal(t, 0, len(events))
+}
diff --git a/engine/api/test/assets/assets.go b/engine/api/test/assets/assets.go
index 71de57e9b..cc74bcec6 100644
--- a/engine/api/test/assets/assets.go
+++ b/engine/api/test/assets/assets.go
@@ -63,6 +63,8 @@ func InsertTestProject(t *testing.T, db gorpmapper.SqlExecutorWithTx, store cach
 
 	require.NoError(t, project.Insert(db, proj))
 
+	require.NoError(t, group.InitializeDefaultGroupName(db, ""))
+
 	require.NoError(t, group.InsertLinkGroupProject(context.TODO(), db, &group.LinkGroupProject{
 		GroupID:   g.ID,
 		ProjectID: proj.ID,
diff --git a/engine/api/worker/dao_test.go b/engine/api/worker/dao_test.go
new file mode 100644
index 000000000..cf3756297
--- /dev/null
+++ b/engine/api/worker/dao_test.go
@@ -0,0 +1,83 @@
+package worker_test
+
+import (
+	"context"
+	"database/sql"
+	"github.com/ovh/cds/engine/api/bootstrap"
+	"github.com/ovh/cds/engine/api/test"
+	"github.com/ovh/cds/engine/api/test/assets"
+	"github.com/ovh/cds/engine/api/worker"
+	"github.com/ovh/cds/engine/api/workflow"
+	"github.com/ovh/cds/sdk"
+	"github.com/stretchr/testify/require"
+	"testing"
+)
+
+func TestUpdate(t *testing.T) {
+	db, cache := test.SetupPG(t, bootstrap.InitiliazeDB)
+
+	key := sdk.RandomString(10)
+	proj := assets.InsertTestProject(t, db, cache, key, key)
+
+	wk := sdk.Workflow{
+		Name:       "coucou",
+		ProjectKey: key,
+		ProjectID:  proj.ID,
+		WorkflowData: sdk.WorkflowData{
+			Node: sdk.Node{
+				Name: "coucou",
+			},
+		},
+	}
+	require.NoError(t, workflow.Insert(context.TODO(), db, cache, *proj, &wk))
+
+	wr := workflow.Run{
+		WorkflowID: wk.ID,
+		Workflow:   wk,
+		ProjectID:  proj.ID,
+	}
+	require.NoError(t, db.Insert(&wr))
+
+	nodeRun := workflow.NodeRun{
+		WorkflowRunID:  wr.ID,
+		WorkflowID:     sql.NullInt64{Int64: wk.ID},
+		WorkflowNodeID: wk.WorkflowData.Node.ID,
+	}
+	require.NoError(t, db.Insert(&nodeRun))
+	jobRun := sdk.WorkflowNodeJobRun{
+		ID:                1,
+		ProjectID:         proj.ID,
+		WorkflowNodeRunID: nodeRun.ID,
+	}
+	dbj := new(workflow.JobRun)
+	require.NoError(t, dbj.ToJobRun(&jobRun))
+	require.NoError(t, db.Insert(dbj))
+	jobRun.ID = dbj.ID
+	t.Cleanup(func() {
+		workflow.DeleteNodeJobRun(db, jobRun.ID)
+	})
+
+	w := sdk.Worker{
+		ID:         sdk.UUID(),
+		ConsumerID: sdk.UUID(),
+		Status:     sdk.StatusBuilding,
+		Name:       sdk.RandomString(10),
+	}
+	require.NoError(t, worker.Insert(context.TODO(), db, &w))
+
+	w.PrivateKey = []byte("je suis la cle")
+	require.NoError(t, worker.SetToBuilding(context.TODO(), db, w.ID, jobRun.ID, w.PrivateKey))
+
+	wDB, err := worker.LoadWorkerByNameWithDecryptKey(context.TODO(), db, w.Name)
+	require.NoError(t, err)
+	require.Equal(t, sdk.StatusBuilding, wDB.Status)
+	require.Equal(t, string(w.PrivateKey), string(wDB.PrivateKey))
+
+	require.NoError(t, worker.SetStatus(context.TODO(), db, w.ID, sdk.StatusWaiting))
+	wDB, err = worker.LoadWorkerByNameWithDecryptKey(context.TODO(), db, w.Name)
+	require.NoError(t, err)
+	require.Nil(t, wDB.JobRunID)
+	require.Equal(t, sdk.StatusWaiting, wDB.Status)
+	require.Equal(t, string(w.PrivateKey), string(wDB.PrivateKey))
+
+}
diff --git a/engine/api/workflow/process_node_test.go b/engine/api/workflow/process_node_test.go
index 1b2516091..71e56f90f 100644
--- a/engine/api/workflow/process_node_test.go
+++ b/engine/api/workflow/process_node_test.go
@@ -2776,7 +2776,7 @@ func writeError(w *http.Response, err error) (*http.Response, error) {
 	body := new(bytes.Buffer)
 	enc := json.NewEncoder(body)
 	w.Body = ioutil.NopCloser(body)
-	sdkErr := sdk.ExtractHTTPError(err, "")
+	sdkErr := sdk.ExtractHTTPError(err)
 	_ = enc.Encode(sdkErr) // nolint
 	w.StatusCode = sdkErr.Status
 	return w, sdkErr
diff --git a/engine/api/workflowtemplate/execute_test.go b/engine/api/workflowtemplate/execute_test.go
index f317718f4..3cec9cf1d 100644
--- a/engine/api/workflowtemplate/execute_test.go
+++ b/engine/api/workflowtemplate/execute_test.go
@@ -177,7 +177,7 @@ name: Environment-[[if .id]]`)),
 
 	_, err := workflowtemplate.Parse(tmpl)
 	assert.NotNil(t, err)
-	e := sdk.ExtractHTTPError(err, "")
+	e := sdk.ExtractHTTPError(err)
 	assert.Equal(t, sdk.ErrCannotParseTemplate.ID, e.ID)
 	errs := []sdk.WorkflowTemplateError{{
 		Type:    "workflow",
diff --git a/engine/cdn/cdn_gc_test.go b/engine/cdn/cdn_gc_test.go
index eddd71827..16315fa10 100644
--- a/engine/cdn/cdn_gc_test.go
+++ b/engine/cdn/cdn_gc_test.go
@@ -209,7 +209,7 @@ func TestCleanWaitingItem(t *testing.T) {
 
 	ctx, cancel := context.WithCancel(context.TODO())
 	t.Cleanup(cancel)
-	s.Units = newRunningStorageUnits(t, m, s.DBConnectionFactory.GetDBMap(m)(), ctx, 1000)
+	s.Units = newRunningStorageUnits(t, m, s.DBConnectionFactory.GetDBMap(m)(), ctx)
 
 	it := sdk.CDNItem{
 		ID:     sdk.UUID(),
diff --git a/engine/cdn/cdn_item_test.go b/engine/cdn/cdn_item_test.go
index 8143a71c6..0bf2b78dd 100644
--- a/engine/cdn/cdn_item_test.go
+++ b/engine/cdn/cdn_item_test.go
@@ -46,7 +46,7 @@ func TestGetItemValue(t *testing.T) {
 	t.Cleanup(cancel)
 
 	cfg := test.LoadTestingConf(t, sdk.TypeCDN)
-	cdnUnits := newRunningStorageUnits(t, m, s.DBConnectionFactory.GetDBMap(m)(), ctx, 1000)
+	cdnUnits := newRunningStorageUnits(t, m, s.DBConnectionFactory.GetDBMap(m)(), ctx)
 	s.Units = cdnUnits
 	var err error
 	s.LogCache, err = lru.NewRedisLRU(db.DbMap, 1000, cfg["redisHost"], cfg["redisPassword"])
@@ -204,7 +204,7 @@ func TestGetItemValue_ThousandLines(t *testing.T) {
 
 	ctx, cancel := context.WithCancel(context.TODO())
 	t.Cleanup(cancel)
-	cdnUnits := newRunningStorageUnits(t, m, db.DbMap, ctx, 1000000)
+	cdnUnits := newRunningStorageUnits(t, m, db.DbMap, ctx)
 	s.Units = cdnUnits
 	var err error
 	s.LogCache, err = lru.NewRedisLRU(db.DbMap, 1000, cfg["redisHost"], cfg["redisPassword"])
@@ -311,7 +311,7 @@ func TestGetItemValue_Reverse(t *testing.T) {
 
 	ctx, cancel := context.WithCancel(context.TODO())
 	t.Cleanup(cancel)
-	cdnUnits := newRunningStorageUnits(t, m, db.DbMap, ctx, 1000)
+	cdnUnits := newRunningStorageUnits(t, m, db.DbMap, ctx)
 	s.Units = cdnUnits
 	var err error
 	s.LogCache, err = lru.NewRedisLRU(db.DbMap, 1000, cfg["redisHost"], cfg["redisPassword"])
@@ -421,7 +421,7 @@ func TestGetItemValue_ThousandLinesReverse(t *testing.T) {
 
 	ctx, cancel := context.WithCancel(context.TODO())
 	t.Cleanup(cancel)
-	cdnUnits := newRunningStorageUnits(t, m, db.DbMap, ctx, 200000)
+	cdnUnits := newRunningStorageUnits(t, m, db.DbMap, ctx)
 	s.Units = cdnUnits
 	var err error
 	s.LogCache, err = lru.NewRedisLRU(db.DbMap, 1000, cfg["redisHost"], cfg["redisPassword"])
diff --git a/engine/cdn/cdn_log_store_test.go b/engine/cdn/cdn_log_store_test.go
index 581e0e60a..216f7d884 100644
--- a/engine/cdn/cdn_log_store_test.go
+++ b/engine/cdn/cdn_log_store_test.go
@@ -43,7 +43,7 @@ func TestStoreNewStepLog(t *testing.T) {
 
 	ctx, cancel := context.WithCancel(context.TODO())
 	t.Cleanup(cancel)
-	cdnUnits := newRunningStorageUnits(t, m, db.DbMap, ctx, 1000)
+	cdnUnits := newRunningStorageUnits(t, m, db.DbMap, ctx)
 	s.Units = cdnUnits
 
 	hm := handledMessage{
@@ -126,7 +126,7 @@ func TestStoreLastStepLog(t *testing.T) {
 
 	ctx, cancel := context.WithCancel(context.TODO())
 	t.Cleanup(cancel)
-	cdnUnits := newRunningStorageUnits(t, m, db.DbMap, ctx, 1000)
+	cdnUnits := newRunningStorageUnits(t, m, db.DbMap, ctx)
 	s.Units = cdnUnits
 
 	hm := handledMessage{
@@ -215,7 +215,7 @@ func TestStoreNewServiceLog(t *testing.T) {
 
 	ctx, cancel := context.WithCancel(context.TODO())
 	t.Cleanup(cancel)
-	cdnUnits := newRunningStorageUnits(t, m, db.DbMap, ctx, 1000)
+	cdnUnits := newRunningStorageUnits(t, m, db.DbMap, ctx)
 	s.Units = cdnUnits
 
 	hm := handledMessage{
diff --git a/engine/cdn/cdn_log_tcp_test.go b/engine/cdn/cdn_log_tcp_test.go
index b1cf5a7c2..6314e98a1 100644
--- a/engine/cdn/cdn_log_tcp_test.go
+++ b/engine/cdn/cdn_log_tcp_test.go
@@ -285,7 +285,7 @@ func TestStoreTruncatedLogs(t *testing.T) {
 
 	ctx, ccl := context.WithCancel(context.TODO())
 	t.Cleanup(ccl)
-	cdnUnits := newRunningStorageUnits(t, m, db.DbMap, ctx, 10)
+	cdnUnits := newRunningStorageUnits(t, m, db.DbMap, ctx)
 	s.Units = cdnUnits
 
 	hm := handledMessage{
diff --git a/engine/cdn/cdn_test.go b/engine/cdn/cdn_test.go
index 30c05f3bc..24db17c48 100644
--- a/engine/cdn/cdn_test.go
+++ b/engine/cdn/cdn_test.go
@@ -103,7 +103,7 @@ var fakeAPIPrivateKey = struct {
 	key *rsa.PrivateKey
 }{}
 
-func newRunningStorageUnits(t *testing.T, m *gorpmapper.Mapper, dbMap *gorp.DbMap, ctx context.Context, maxStepSize int64) *storage.RunningStorageUnits {
+func newRunningStorageUnits(t *testing.T, m *gorpmapper.Mapper, dbMap *gorp.DbMap, ctx context.Context) *storage.RunningStorageUnits {
 	cfg := test.LoadTestingConf(t, sdk.TypeCDN)
 	tmpDir, err := ioutil.TempDir("", t.Name()+"-cdn-1-*")
 	require.NoError(t, err)
diff --git a/engine/cdn/item_handler_test.go b/engine/cdn/item_handler_test.go
index 1d0a6b3ca..709e71881 100644
--- a/engine/cdn/item_handler_test.go
+++ b/engine/cdn/item_handler_test.go
@@ -110,7 +110,7 @@ func TestGetItemLogsDownloadHandler(t *testing.T) {
 
 	ctx, cancel := context.WithCancel(context.TODO())
 	t.Cleanup(cancel)
-	s.Units = newRunningStorageUnits(t, s.Mapper, db.DbMap, ctx, 1000)
+	s.Units = newRunningStorageUnits(t, s.Mapper, db.DbMap, ctx)
 
 	hm := handledMessage{
 		Msg: hook.Message{
diff --git a/engine/cdn/item_logs_handler_test.go b/engine/cdn/item_logs_handler_test.go
index 5928730d7..a39025d81 100644
--- a/engine/cdn/item_logs_handler_test.go
+++ b/engine/cdn/item_logs_handler_test.go
@@ -4,6 +4,8 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"github.com/ovh/cds/engine/cdn/item"
+	"github.com/ovh/cds/engine/cdn/storage"
 	"net/http"
 	"net/http/httptest"
 	"strconv"
@@ -26,6 +28,162 @@ import (
 	"github.com/ovh/cds/sdk/log/hook"
 )
 
+func TestGetItemsAllLogsLinesHandler(t *testing.T) {
+	projectKey := sdk.RandomString(10)
+	s, db := newTestService(t)
+
+	ctx, cancel := context.WithCancel(context.TODO())
+	t.Cleanup(cancel)
+	s.Units = newRunningStorageUnits(t, s.Mapper, db.DbMap, ctx)
+
+	// Add step 1
+	hm1 := handledMessage{
+		Msg: hook.Message{
+			Full: "this is a message",
+		},
+		IsTerminated: sdk.StatusTerminated,
+		Signature: log.Signature{
+			ProjectKey:   projectKey,
+			WorkflowID:   1,
+			WorkflowName: "MyWorkflow",
+			RunID:        1,
+			NodeRunID:    1,
+			NodeRunName:  "MyPipeline",
+			JobName:      "MyJob",
+			JobID:        1,
+			Worker: &log.SignatureWorker{
+				StepName:  "script1",
+				StepOrder: 0,
+			},
+		},
+	}
+	content := buildMessage(hm1)
+	err := s.storeLogs(context.TODO(), sdk.CDNTypeItemStepLog, hm1.Signature, hm1.IsTerminated, content)
+	require.NoError(t, err)
+	hash1 := sdk.CDNLogAPIRef{
+		ProjectKey:     hm1.Signature.ProjectKey,
+		WorkflowName:   hm1.Signature.WorkflowName,
+		WorkflowID:     hm1.Signature.WorkflowID,
+		RunID:          hm1.Signature.RunID,
+		NodeRunName:    hm1.Signature.NodeRunName,
+		NodeRunID:      hm1.Signature.NodeRunID,
+		NodeRunJobName: hm1.Signature.JobName,
+		NodeRunJobID:   hm1.Signature.JobID,
+		StepOrder:      hm1.Signature.Worker.StepOrder,
+		StepName:       hm1.Signature.Worker.StepName,
+	}
+	hashRef1, err := hash1.ToHash()
+	require.NoError(t, err)
+
+	// Add step 2
+	hm2 := hm1
+	hm2.Signature.Worker.StepOrder = 1
+	content = buildMessage(hm2)
+	err = s.storeLogs(context.TODO(), sdk.CDNTypeItemStepLog, hm2.Signature, hm2.IsTerminated, content)
+	require.NoError(t, err)
+	hash2 := sdk.CDNLogAPIRef{
+		ProjectKey:     hm2.Signature.ProjectKey,
+		WorkflowName:   hm2.Signature.WorkflowName,
+		WorkflowID:     hm2.Signature.WorkflowID,
+		RunID:          hm2.Signature.RunID,
+		NodeRunName:    hm2.Signature.NodeRunName,
+		NodeRunID:      hm2.Signature.NodeRunID,
+		NodeRunJobName: hm2.Signature.JobName,
+		NodeRunJobID:   hm2.Signature.JobID,
+		StepOrder:      hm2.Signature.Worker.StepOrder,
+		StepName:       hm2.Signature.Worker.StepName,
+	}
+	hashRef2, err := hash2.ToHash()
+	require.NoError(t, err)
+
+	// Add step 3
+	hm3 := hm1
+	hm3.Signature.Worker.StepOrder = 2
+	hm3.Msg.Full = "First Line"
+	hm3.IsTerminated = false
+	content = buildMessage(hm3)
+	err = s.storeLogs(context.TODO(), sdk.CDNTypeItemStepLog, hm3.Signature, hm3.IsTerminated, content)
+	require.NoError(t, err)
+	hash3 := sdk.CDNLogAPIRef{
+		ProjectKey:     hm3.Signature.ProjectKey,
+		WorkflowName:   hm3.Signature.WorkflowName,
+		WorkflowID:     hm3.Signature.WorkflowID,
+		RunID:          hm3.Signature.RunID,
+		NodeRunName:    hm3.Signature.NodeRunName,
+		NodeRunID:      hm3.Signature.NodeRunID,
+		NodeRunJobName: hm3.Signature.JobName,
+		NodeRunJobID:   hm3.Signature.JobID,
+		StepOrder:      hm3.Signature.Worker.StepOrder,
+		StepName:       hm3.Signature.Worker.StepName,
+	}
+
+	hm4 := hm3
+	hm4.Msg.Full = "Second Line"
+	hm4.IsTerminated = true
+	content = buildMessage(hm4)
+	err = s.storeLogs(context.TODO(), sdk.CDNTypeItemStepLog, hm4.Signature, hm4.IsTerminated, content)
+	require.NoError(t, err)
+
+	hashRef3, err := hash3.ToHash()
+	require.NoError(t, err)
+
+	it, err := item.LoadByAPIRefHashAndType(ctx, s.Mapper, s.mustDBWithCtx(ctx), hashRef3, sdk.CDNTypeItemStepLog)
+	require.NoError(t, err)
+
+	unit, err := storage.LoadUnitByName(ctx, s.Mapper, s.mustDBWithCtx(ctx), s.Units.Storages[0].Name())
+	require.NoError(t, err)
+	cpt := 0
+	for {
+		cpt++
+		_, err = storage.LoadItemUnitByUnit(ctx, s.Mapper, s.mustDBWithCtx(ctx), unit.ID, it.ID)
+		if sdk.ErrorIs(err, sdk.ErrNotFound) {
+			if cpt == 10 {
+				t.Fail()
+			}
+			time.Sleep(250 * time.Millisecond)
+			continue
+		}
+		if err != nil {
+			t.Fail()
+		}
+		break
+	}
+
+	signer, err := authentication.NewSigner("cdn-test", test.SigningKey)
+	require.NoError(t, err)
+	s.Common.ParsedAPIPublicKey = signer.GetVerifyKey()
+	jwtToken := jwt.NewWithClaims(jwt.SigningMethodRS512, sdk.AuthSessionJWTClaims{
+		ID: sdk.UUID(),
+		StandardClaims: jwt.StandardClaims{
+			Issuer:    "test",
+			Subject:   sdk.UUID(),
+			Id:        sdk.UUID(),
+			IssuedAt:  time.Now().Unix(),
+			ExpiresAt: time.Now().Add(time.Minute).Unix(),
+		},
+	})
+	jwtTokenRaw, err := signer.SignJWT(jwtToken)
+	require.NoError(t, err)
+
+	uri := fmt.Sprintf("%s?apiRefHash=%s&apiRefHash=%s&apiRefHash=%s", s.Router.GetRoute("GET", s.getItemsAllLogsLinesHandler, map[string]string{
+		"type": string(sdk.CDNTypeItemStepLog),
+	}), hashRef1, hashRef2, hashRef3)
+	require.NotEmpty(t, uri)
+	req := assets.NewJWTAuthentifiedRequest(t, jwtTokenRaw, "GET", uri, nil)
+	rec := httptest.NewRecorder()
+	s.Router.Mux.ServeHTTP(rec, req)
+	require.Equal(t, 200, rec.Code)
+
+	var logslines []sdk.CDNLogsLines
+	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &logslines))
+	require.Len(t, logslines, 3)
+
+	require.Equal(t, int64(1), logslines[0].LinesCount)
+	require.Equal(t, int64(1), logslines[1].LinesCount)
+	require.Equal(t, int64(2), logslines[2].LinesCount)
+
+}
+
 func TestGetItemLogsLinesHandler(t *testing.T) {
 	projectKey := sdk.RandomString(10)
 
@@ -38,7 +196,7 @@ func TestGetItemLogsLinesHandler(t *testing.T) {
 
 	ctx, cancel := context.WithCancel(context.TODO())
 	t.Cleanup(cancel)
-	s.Units = newRunningStorageUnits(t, s.Mapper, db.DbMap, ctx, 1000)
+	s.Units = newRunningStorageUnits(t, s.Mapper, db.DbMap, ctx)
 
 	hm := handledMessage{
 		Msg: hook.Message{
@@ -133,7 +291,7 @@ func TestGetItemLogsStreamHandler(t *testing.T) {
 
 	ctx, cancel := context.WithCancel(context.TODO())
 	t.Cleanup(cancel)
-	s.Units = newRunningStorageUnits(t, s.Mapper, db.DbMap, ctx, 1000)
+	s.Units = newRunningStorageUnits(t, s.Mapper, db.DbMap, ctx)
 
 	signature := log.Signature{
 		ProjectKey:   projectKey,
diff --git a/engine/cdn/redis/reader_test.go b/engine/cdn/redis/reader_test.go
new file mode 100644
index 000000000..035b20ba0
--- /dev/null
+++ b/engine/cdn/redis/reader_test.go
@@ -0,0 +1,58 @@
+package redis_test
+
+import (
+	"io"
+	"strings"
+	"testing"
+
+	"github.com/ovh/cds/engine/cache"
+	"github.com/ovh/cds/engine/cdn/redis"
+	"github.com/ovh/cds/engine/test"
+	"github.com/ovh/cds/sdk"
+	"github.com/stretchr/testify/require"
+)
+
+func TestReader_EOF(t *testing.T) {
+	cfg := test.LoadTestingConf(t, sdk.TypeCDN)
+
+	c, err := cache.New(cfg["redisHost"], cfg["redisPassword"], -1)
+	require.NoError(t, err)
+
+	cacheKey := cache.Key("test:cdn:item")
+
+	itemID := sdk.RandomString(10)
+
+	w := &redis.Writer{
+		Store:     c,
+		ItemID:    itemID,
+		PrefixKey: cacheKey,
+	}
+
+	r := &redis.Reader{
+		Store:     c,
+		ItemID:    itemID,
+		PrefixKey: cacheKey,
+		Format:    sdk.CDNReaderFormatJSON,
+	}
+
+	t.Logf("writings logs for fake item with id: %s", itemID)
+
+	// Insert lines in redis
+	_, err = io.Copy(w, strings.NewReader("this is the first line\nthis is the second line\n"))
+	require.NoError(t, err)
+
+	// Read all lines without closing the reader
+	buf1 := new(strings.Builder)
+	_, err = io.Copy(buf1, r)
+	require.NoError(t, err)
+	require.Equal(t, "[{\"number\":0,\"value\":\"this is the first line\\n\"},{\"number\":1,\"value\":\"this is the second line\\n\"}]", buf1.String())
+
+	// Insert a new line in redis
+	_, err = io.Copy(w, strings.NewReader("this is the third line\n"))
+	require.NoError(t, err)
+
+	// Try to read again from same reader should return an error
+	buf := make([]byte, 1)
+	_, err = r.Read(buf)
+	require.Error(t, err, "read should return EOF")
+}
diff --git a/engine/cdn/redis/writer_test.go b/engine/cdn/redis/writer_test.go
new file mode 100644
index 000000000..946a990ab
--- /dev/null
+++ b/engine/cdn/redis/writer_test.go
@@ -0,0 +1,36 @@
+package redis_test
+
+import (
+	"io"
+	"strings"
+	"testing"
+
+	"github.com/ovh/cds/engine/cache"
+	"github.com/ovh/cds/engine/cdn/redis"
+	"github.com/ovh/cds/engine/test"
+	"github.com/ovh/cds/sdk"
+	"github.com/stretchr/testify/require"
+)
+
+func TestWriter_Closed(t *testing.T) {
+	cfg := test.LoadTestingConf(t, sdk.TypeCDN)
+	c, err := cache.New(cfg["redisHost"], cfg["redisPassword"], -1)
+	require.NoError(t, err)
+
+	cacheKey := cache.Key("test:cdn:item")
+	itemID := sdk.RandomString(10)
+
+	w := &redis.Writer{
+		Store:     c,
+		ItemID:    itemID,
+		PrefixKey: cacheKey,
+	}
+
+	_, err = io.Copy(w, strings.NewReader("this is the first line\nthis is the second line\n"))
+	require.NoError(t, err)
+	require.NoError(t, w.Close())
+
+	_, err = io.Copy(w, strings.NewReader("this is the third line\n"))
+	require.Error(t, err)
+	require.Equal(t, "writer is closed", err.Error())
+}
diff --git a/sdk/error_test.go b/sdk/error_test.go
index 6ce03563f..233a5abdb 100644
--- a/sdk/error_test.go
+++ b/sdk/error_test.go
@@ -69,12 +69,12 @@ func TestNewError(t *testing.T) {
 	// print the error stack trace
 	t.Logf("%+v\n", err)
 
-	httpErr := ExtractHTTPError(err, "fr")
+	httpErr := ExtractHTTPError(err)
 
 	// print the http error
 	t.Log(httpErr)
 
-	assert.Equal(t, "la requête est incorrecte (from: this is an error generated from vendor)", httpErr.Error())
+	assert.Equal(t, "wrong request (from: this is an error generated from vendor)", httpErr.Error())
 }
 
 func TestWrapError(t *testing.T) {
@@ -118,23 +118,23 @@ func TestCause(t *testing.T) {
 func TestNewAdvancedError(t *testing.T) {
 	err := NewErrorFrom(ErrWrongRequest, "this is an error generated from vendor")
 	assert.Equal(t, "TestNewAdvancedError: wrong request (from: this is an error generated from vendor)", err.Error())
-	httpErr := ExtractHTTPError(err, "fr")
-	assert.Equal(t, "la requête est incorrecte (from: this is an error generated from vendor)", httpErr.Error())
+	httpErr := ExtractHTTPError(err)
+	assert.Equal(t, "wrong request (from: this is an error generated from vendor)", httpErr.Error())
 
 	err = WrapError(err, "Something no visible for http error")
 	assert.Equal(t, "TestNewAdvancedError: wrong request (from: this is an error generated from vendor) (caused by: Something no visible for http error: this is an error generated from vendor)", err.Error())
-	httpErr = ExtractHTTPError(err, "fr")
-	assert.Equal(t, "la requête est incorrecte (from: this is an error generated from vendor)", httpErr.Error())
+	httpErr = ExtractHTTPError(err)
+	assert.Equal(t, "wrong request (from: this is an error generated from vendor)", httpErr.Error())
 
 	err = NewError(ErrAlreadyTaken, err)
 	assert.Equal(t, "TestNewAdvancedError: This job is already taken by another worker (from: this is an error generated from vendor) (caused by: Something no visible for http error: this is an error generated from vendor)", err.Error())
-	httpErr = ExtractHTTPError(err, "fr")
-	assert.Equal(t, "Ce job est déjà en cours de traitement par un autre worker (from: this is an error generated from vendor)", httpErr.Error())
+	httpErr = ExtractHTTPError(err)
+	assert.Equal(t, "This job is already taken by another worker (from: this is an error generated from vendor)", httpErr.Error())
 
 	err = NewErrorWithStack(err, NewErrorFrom(ErrNotFound, "can't found this"))
 	assert.Equal(t, "TestNewAdvancedError: resource not found (from: can't found this) (caused by: Something no visible for http error: this is an error generated from vendor)", err.Error())
-	httpErr = ExtractHTTPError(err, "fr")
-	assert.Equal(t, "la ressource n'existe pas (from: can't found this)", httpErr.Error())
+	httpErr = ExtractHTTPError(err)
+	assert.Equal(t, "resource not found (from: can't found this)", httpErr.Error())
 }
 
 func TestMultiError(t *testing.T) {
@@ -150,23 +150,23 @@ func TestMultiError(t *testing.T) {
 	mError.Append(errFourth)
 
 	assert.Equal(t, "TestMultiError: internal server error (caused by: my first error), TestMultiError: wrong request (from: my second error), TestMultiError: internal server error (caused by: hidden info: my third error), TestMultiError: already exists (from: my fourth error)", mError.Error())
-	httpErr := ExtractHTTPError(mError, "fr")
-	assert.Equal(t, "erreur interne, la requête est incorrecte: my second error, erreur interne, conflit: my fourth error", httpErr.Error())
+	httpErr := ExtractHTTPError(mError)
+	assert.Equal(t, "internal server error, wrong request: my second error, internal server error, already exists: my fourth error", httpErr.Error())
 
 	wrappedErr := NewError(ErrWrongRequest, mError)
 	assert.Equal(t, "TestMultiError: wrong request (from: internal server error, wrong request: my second error, internal server error, already exists: my fourth error) (caused by: TestMultiError: internal server error (caused by: my first error), TestMultiError: wrong request (from: my second error), TestMultiError: internal server error (caused by: hidden info: my third error), TestMultiError: already exists (from: my fourth error))", wrappedErr.Error())
-	httpErr = ExtractHTTPError(wrappedErr, "fr")
-	assert.Equal(t, "la requête est incorrecte (from: internal server error, wrong request: my second error, internal server error, already exists: my fourth error)", httpErr.Error())
+	httpErr = ExtractHTTPError(wrappedErr)
+	assert.Equal(t, "wrong request (from: internal server error, wrong request: my second error, internal server error, already exists: my fourth error)", httpErr.Error())
 
 	stackErr := WithStack(mError)
 	assert.Equal(t, "TestMultiError: internal server error (from: internal server error, wrong request: my second error, internal server error, already exists: my fourth error) (caused by: TestMultiError: internal server error (caused by: my first error), TestMultiError: wrong request (from: my second error), TestMultiError: internal server error (caused by: hidden info: my third error), TestMultiError: already exists (from: my fourth error))", stackErr.Error())
-	httpErr = ExtractHTTPError(stackErr, "fr")
-	assert.Equal(t, "erreur interne (from: internal server error, wrong request: my second error, internal server error, already exists: my fourth error)", httpErr.Error())
+	httpErr = ExtractHTTPError(stackErr)
+	assert.Equal(t, "internal server error (from: internal server error, wrong request: my second error, internal server error, already exists: my fourth error)", httpErr.Error())
 
 	stackErr = WrapError(mError, "more info")
 	assert.Equal(t, "TestMultiError: internal server error (from: internal server error, wrong request: my second error, internal server error, already exists: my fourth error) (caused by: more info: TestMultiError: internal server error (caused by: my first error), TestMultiError: wrong request (from: my second error), TestMultiError: internal server error (caused by: hidden info: my third error), TestMultiError: already exists (from: my fourth error))", stackErr.Error())
-	httpErr = ExtractHTTPError(stackErr, "fr")
-	assert.Equal(t, "erreur interne (from: internal server error, wrong request: my second error, internal server error, already exists: my fourth error)", httpErr.Error())
+	httpErr = ExtractHTTPError(stackErr)
+	assert.Equal(t, "internal server error (from: internal server error, wrong request: my second error, internal server error, already exists: my fourth error)", httpErr.Error())
 }
 
 func Test_cause(t *testing.T) {