diff --git a/cmd/tk/bootstrap.go b/cmd/tk/bootstrap.go
index 052186c62b33081236d26a44ba60214842b25591..33fb6dde14e6241653f2b4e280b8d57f19f1645b 100644
--- a/cmd/tk/bootstrap.go
+++ b/cmd/tk/bootstrap.go
@@ -1,7 +1,25 @@
 package main
 
 import (
+	"context"
+	"fmt"
+	"net/url"
+	"os"
+	"path"
+	"path/filepath"
+	"sigs.k8s.io/yaml"
+	"strings"
+	"time"
+
 	"github.com/spf13/cobra"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1alpha1"
+	sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
 )
 
 var bootstrapCmd = &cobra.Command{
@@ -13,8 +31,203 @@ var (
 	bootstrapVersion string
 )
 
+const (
+	bootstrapBranch                = "master"
+	bootstrapInstallManifest       = "toolkit-components.yaml"
+	bootstrapSourceManifest        = "toolkit-source.yaml"
+	bootstrapKustomizationManifest = "toolkit-kustomization.yaml"
+)
+
 func init() {
 	bootstrapCmd.PersistentFlags().StringVar(&bootstrapVersion, "version", "master", "toolkit tag or branch")
 
 	rootCmd.AddCommand(bootstrapCmd)
 }
+
+func generateInstallManifests(targetPath, namespace, tmpDir string) (string, error) {
+	tkDir := path.Join(tmpDir, ".tk")
+	defer os.RemoveAll(tkDir)
+
+	if err := os.MkdirAll(tkDir, os.ModePerm); err != nil {
+		return "", fmt.Errorf("generating manifests failed: %w", err)
+	}
+
+	if err := genInstallManifests(bootstrapVersion, namespace, components, tkDir); err != nil {
+		return "", fmt.Errorf("generating manifests failed: %w", err)
+	}
+
+	manifestsDir := path.Join(tmpDir, targetPath, namespace)
+	if err := os.MkdirAll(manifestsDir, os.ModePerm); err != nil {
+		return "", fmt.Errorf("generating manifests failed: %w", err)
+	}
+
+	manifest := path.Join(manifestsDir, bootstrapInstallManifest)
+	if err := buildKustomization(tkDir, manifest); err != nil {
+		return "", fmt.Errorf("build kustomization failed: %w", err)
+	}
+
+	return manifest, nil
+}
+
+func applyInstallManifests(ctx context.Context, manifestPath string, components []string) error {
+	command := fmt.Sprintf("kubectl apply -f %s", manifestPath)
+	if _, err := utils.execCommand(ctx, ModeOS, command); err != nil {
+		return fmt.Errorf("install failed")
+	}
+
+	for _, deployment := range components {
+		command = fmt.Sprintf("kubectl -n %s rollout status deployment %s --timeout=%s",
+			namespace, deployment, timeout.String())
+		if _, err := utils.execCommand(ctx, ModeOS, command); err != nil {
+			return fmt.Errorf("install failed")
+		}
+	}
+	return nil
+}
+
+func generateSyncManifests(url, name, namespace, targetPath, tmpDir string, interval time.Duration) error {
+	gvk := sourcev1.GroupVersion.WithKind("GitRepository")
+	gitRepository := sourcev1.GitRepository{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       gvk.Kind,
+			APIVersion: gvk.GroupVersion().String(),
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: namespace,
+		},
+		Spec: sourcev1.GitRepositorySpec{
+			URL: url,
+			Interval: metav1.Duration{
+				Duration: interval,
+			},
+			Reference: &sourcev1.GitRepositoryRef{
+				Branch: "master",
+			},
+			SecretRef: &corev1.LocalObjectReference{
+				Name: name,
+			},
+		},
+	}
+
+	gitData, err := yaml.Marshal(gitRepository)
+	if err != nil {
+		return err
+	}
+
+	if err := utils.writeFile(string(gitData), filepath.Join(tmpDir, targetPath, namespace, bootstrapSourceManifest)); err != nil {
+		return err
+	}
+
+	gvk = kustomizev1.GroupVersion.WithKind("Kustomization")
+	emptyAPIGroup := ""
+	kustomization := kustomizev1.Kustomization{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       gvk.Kind,
+			APIVersion: gvk.GroupVersion().String(),
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: namespace,
+		},
+		Spec: kustomizev1.KustomizationSpec{
+			Interval: metav1.Duration{
+				Duration: 10 * time.Minute,
+			},
+			Path:  fmt.Sprintf("./%s", strings.TrimPrefix(targetPath, "./")),
+			Prune: true,
+			SourceRef: corev1.TypedLocalObjectReference{
+				APIGroup: &emptyAPIGroup,
+				Kind:     "GitRepository",
+				Name:     name,
+			},
+		},
+	}
+
+	ksData, err := yaml.Marshal(kustomization)
+	if err != nil {
+		return err
+	}
+
+	if err := utils.writeFile(string(ksData), filepath.Join(tmpDir, targetPath, namespace, bootstrapKustomizationManifest)); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func applySyncManifests(ctx context.Context, kubeClient client.Client, name, namespace, targetPath, tmpDir string) error {
+	command := fmt.Sprintf("kubectl apply -f %s", filepath.Join(tmpDir, targetPath, namespace))
+	if _, err := utils.execCommand(ctx, ModeStderrOS, command); err != nil {
+		return err
+	}
+
+	logWaiting("waiting for cluster sync")
+
+	if err := wait.PollImmediate(pollInterval, timeout,
+		isGitRepositoryReady(ctx, kubeClient, name, namespace)); err != nil {
+		return err
+	}
+
+	if err := wait.PollImmediate(pollInterval, timeout,
+		isKustomizationReady(ctx, kubeClient, name, namespace)); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func shouldInstallManifests(ctx context.Context, kubeClient client.Client, namespace string) bool {
+	namespacedName := types.NamespacedName{
+		Namespace: namespace,
+		Name:      namespace,
+	}
+	var kustomization kustomizev1.Kustomization
+	if err := kubeClient.Get(ctx, namespacedName, &kustomization); err != nil {
+		return true
+	}
+
+	return kustomization.Status.LastAppliedRevision == ""
+}
+
+func shouldCreateDeployKey(ctx context.Context, kubeClient client.Client, namespace string) bool {
+	namespacedName := types.NamespacedName{
+		Namespace: namespace,
+		Name:      namespace,
+	}
+
+	var existing corev1.Secret
+	if err := kubeClient.Get(ctx, namespacedName, &existing); err != nil {
+		return true
+	}
+	return false
+}
+
+func generateDeployKey(ctx context.Context, kubeClient client.Client, url *url.URL, namespace string) (string, error) {
+	pair, err := generateKeyPair(ctx)
+	if err != nil {
+		return "", err
+	}
+
+	hostKey, err := scanHostKey(ctx, url)
+	if err != nil {
+		return "", err
+	}
+
+	secret := corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      namespace,
+			Namespace: namespace,
+		},
+		StringData: map[string]string{
+			"identity":     string(pair.PrivateKey),
+			"identity.pub": string(pair.PublicKey),
+			"known_hosts":  string(hostKey),
+		},
+	}
+	if err := upsertSecret(ctx, kubeClient, secret); err != nil {
+		return "", err
+	}
+
+	return string(pair.PublicKey), nil
+}
diff --git a/cmd/tk/bootstrap_github.go b/cmd/tk/bootstrap_github.go
index 0b1452031c7958a8bd481d42fb07009795aa0970..3133f8914efe30f6d3cc878b0e22e01cb66e09a4 100644
--- a/cmd/tk/bootstrap_github.go
+++ b/cmd/tk/bootstrap_github.go
@@ -7,25 +7,11 @@ import (
 	"net/url"
 	"os"
 	"path"
-	"path/filepath"
-	"sigs.k8s.io/yaml"
-	"strings"
 	"time"
 
-	"github.com/go-git/go-git/v5"
-	"github.com/go-git/go-git/v5/plumbing"
-	"github.com/go-git/go-git/v5/plumbing/object"
-	"github.com/go-git/go-git/v5/plumbing/transport/http"
-	"github.com/google/go-github/v32/github"
 	"github.com/spf13/cobra"
-	corev1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/apimachinery/pkg/util/wait"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-
-	kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1alpha1"
-	sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
+
+	"github.com/fluxcd/toolkit/pkg/git"
 )
 
 var bootstrapGitHubCmd = &cobra.Command{
@@ -70,13 +56,7 @@ var (
 )
 
 const (
-	ghTokenName             = "GITHUB_TOKEN"
-	ghBranch                = "master"
-	ghInstallManifest       = "toolkit-components.yaml"
-	ghSourceManifest        = "toolkit-source.yaml"
-	ghKustomizationManifest = "toolkit-kustomization.yaml"
-	ghDefaultHostname       = "github.com"
-	ghDefaultPermission     = "maintain"
+	ghDefaultPermission = "maintain"
 )
 
 func init() {
@@ -86,22 +66,26 @@ func init() {
 	bootstrapGitHubCmd.Flags().BoolVar(&ghPersonal, "personal", false, "is personal repository")
 	bootstrapGitHubCmd.Flags().BoolVar(&ghPrivate, "private", true, "is private repository")
 	bootstrapGitHubCmd.Flags().DurationVar(&ghInterval, "interval", time.Minute, "sync interval")
-	bootstrapGitHubCmd.Flags().StringVar(&ghHostname, "hostname", ghDefaultHostname, "GitHub hostname")
+	bootstrapGitHubCmd.Flags().StringVar(&ghHostname, "hostname", git.GitHubDefaultHostname, "GitHub hostname")
 	bootstrapGitHubCmd.Flags().StringVar(&ghPath, "path", "", "repository path, when specified the cluster sync will be scoped to this path")
 
 	bootstrapCmd.AddCommand(bootstrapGitHubCmd)
 }
 
 func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
-	ghToken := os.Getenv(ghTokenName)
+	ghToken := os.Getenv(git.GitHubTokenName)
 	if ghToken == "" {
-		return fmt.Errorf("%s environment variable not found", ghTokenName)
+		return fmt.Errorf("%s environment variable not found", git.GitHubTokenName)
+	}
+
+	repository, err := git.NewRepository(ghRepository, ghOwner, ghHostname, ghToken, "tk", "tk@users.noreply.github.com")
+	if err != nil {
+		return err
 	}
 
-	ghURL := fmt.Sprintf("https://%s/%s/%s", ghHostname, ghOwner, ghRepository)
-	sshURL := fmt.Sprintf("ssh://git@%s/%s/%s", ghHostname, ghOwner, ghRepository)
-	if ghOwner == "" || ghRepository == "" {
-		return fmt.Errorf("owner and repository are required")
+	provider := &git.GithubProvider{
+		IsPrivate:  ghPrivate,
+		IsPersonal: ghPersonal,
 	}
 
 	kubeClient, err := utils.kubeClient(kubeconfig)
@@ -120,46 +104,49 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
 
 	// create GitHub repository if doesn't exists
 	logAction("connecting to %s", ghHostname)
-	if err := createGitHubRepository(ctx, ghHostname, ghOwner, ghRepository, ghToken, ghPrivate, ghPersonal); err != nil {
+	changed, err := provider.CreateRepository(ctx, repository)
+	if err != nil {
 		return err
 	}
+	if changed {
+		logSuccess("repository created")
+	}
 
 	withErrors := false
 	// add teams to org repository
 	if !ghPersonal {
 		for _, team := range ghTeams {
-			if err := addGitHubTeam(ctx, ghHostname, ghOwner, ghRepository, ghToken, team, ghDefaultPermission); err != nil {
+			if changed, err := provider.AddTeam(ctx, repository, team, ghDefaultPermission); err != nil {
 				logFailure(err.Error())
 				withErrors = true
-			} else {
+			} else if changed {
 				logSuccess("%s team access granted", team)
 			}
 		}
 	}
 
 	// clone repository and checkout the master branch
-	repo, err := checkoutGitHubRepository(ctx, ghURL, ghBranch, ghToken, tmpDir)
-	if err != nil {
+	if err := repository.Checkout(ctx, bootstrapBranch, tmpDir); err != nil {
 		return err
 	}
 	logSuccess("repository cloned")
 
 	// generate install manifests
 	logGenerate("generating manifests")
-	manifest, err := generateGitHubInstall(ghPath, namespace, tmpDir)
+	manifest, err := generateInstallManifests(ghPath, namespace, tmpDir)
 	if err != nil {
 		return err
 	}
 
 	// stage install manifests
-	changed, err := commitGitHubManifests(repo, ghPath, namespace)
+	changed, err = repository.Commit(ctx, path.Join(ghPath, namespace), "Add manifests")
 	if err != nil {
 		return err
 	}
 
 	// push install manifests
 	if changed {
-		if err := pushGitHubRepository(ctx, repo, ghToken); err != nil {
+		if err := repository.Push(ctx); err != nil {
 			return err
 		}
 		logSuccess("components manifests pushed")
@@ -168,74 +155,63 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
 	}
 
 	// determine if repo synchronization is working
-	isInstall := shouldInstallGitHub(ctx, kubeClient, namespace)
+	isInstall := shouldInstallManifests(ctx, kubeClient, namespace)
 
 	if isInstall {
 		// apply install manifests
 		logAction("installing components in %s namespace", namespace)
-		command := fmt.Sprintf("kubectl apply -f %s", manifest)
-		if _, err := utils.execCommand(ctx, ModeOS, command); err != nil {
-			return fmt.Errorf("install failed")
+		if err := applyInstallManifests(ctx, manifest, components); err != nil {
+			return err
 		}
 		logSuccess("install completed")
-
-		// check installation
-		logWaiting("verifying installation")
-		for _, deployment := range components {
-			command = fmt.Sprintf("kubectl -n %s rollout status deployment %s --timeout=%s",
-				namespace, deployment, timeout.String())
-			if _, err := utils.execCommand(ctx, ModeOS, command); err != nil {
-				return fmt.Errorf("install failed")
-			} else {
-				logSuccess("%s ready", deployment)
-			}
-		}
 	}
 
 	// setup SSH deploy key
-	if shouldCreateGitHubDeployKey(ctx, kubeClient, namespace) {
+	if shouldCreateDeployKey(ctx, kubeClient, namespace) {
 		logAction("configuring deploy key")
-		u, err := url.Parse(sshURL)
+		u, err := url.Parse(repository.GetSSH())
 		if err != nil {
 			return fmt.Errorf("git URL parse failed: %w", err)
 		}
 
-		key, err := generateGitHubDeployKey(ctx, kubeClient, u, namespace)
+		key, err := generateDeployKey(ctx, kubeClient, u, namespace)
 		if err != nil {
 			return fmt.Errorf("generating deploy key failed: %w", err)
 		}
 
-		if err := createGitHubDeployKey(ctx, key, ghHostname, ghOwner, ghRepository, ghPath, ghToken); err != nil {
+		keyName := "tk"
+		if ghPath != "" {
+			keyName = fmt.Sprintf("tk-%s", ghPath)
+		}
+
+		if changed, err := provider.AddDeployKey(ctx, repository, key, keyName); err != nil {
 			return err
+		} else if changed {
+			logSuccess("deploy key configured")
 		}
-		logSuccess("deploy key configured")
 	}
 
 	// configure repo synchronization
 	if isInstall {
 		// generate source and kustomization manifests
 		logAction("generating sync manifests")
-		if err := generateGitHubKustomization(sshURL, namespace, namespace, ghPath, tmpDir, ghInterval); err != nil {
+		if err := generateSyncManifests(repository.GetSSH(), namespace, namespace, ghPath, tmpDir, ghInterval); err != nil {
 			return err
 		}
 
-		// stage manifests
-		changed, err = commitGitHubManifests(repo, ghPath, namespace)
-		if err != nil {
+		// commit and push manifests
+		if changed, err = repository.Commit(ctx, path.Join(ghPath, namespace), "Add manifests"); err != nil {
 			return err
-		}
-
-		// push manifests
-		if changed {
-			if err := pushGitHubRepository(ctx, repo, ghToken); err != nil {
+		} else if changed {
+			if err := repository.Push(ctx); err != nil {
 				return err
 			}
+			logSuccess("sync manifests pushed")
 		}
-		logSuccess("sync manifests pushed")
 
 		// apply manifests and waiting for sync
 		logAction("applying sync manifests")
-		if err := applyGitHubKustomization(ctx, kubeClient, namespace, namespace, ghPath, tmpDir); err != nil {
+		if err := applySyncManifests(ctx, kubeClient, namespace, namespace, ghPath, tmpDir); err != nil {
 			return err
 		}
 	}
@@ -247,390 +223,3 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
 	logSuccess("bootstrap finished")
 	return nil
 }
-
-func makeGitHubClient(hostname, token string) (*github.Client, error) {
-	auth := github.BasicAuthTransport{
-		Username: "git",
-		Password: token,
-	}
-
-	gh := github.NewClient(auth.Client())
-	if hostname != ghDefaultHostname {
-		baseURL := fmt.Sprintf("https://%s/api/v3/", hostname)
-		uploadURL := fmt.Sprintf("https://%s/api/uploads/", hostname)
-		if g, err := github.NewEnterpriseClient(baseURL, uploadURL, auth.Client()); err == nil {
-			gh = g
-		} else {
-			return nil, fmt.Errorf("github client error: %w", err)
-		}
-	}
-
-	return gh, nil
-}
-
-func createGitHubRepository(ctx context.Context, hostname, owner, name, token string, isPrivate, isPersonal bool) error {
-	gh, err := makeGitHubClient(hostname, token)
-	if err != nil {
-		return err
-	}
-	org := ""
-	if !isPersonal {
-		org = owner
-	}
-
-	if _, _, err := gh.Repositories.Get(ctx, org, name); err == nil {
-		return nil
-	}
-
-	autoInit := true
-	_, _, err = gh.Repositories.Create(ctx, org, &github.Repository{
-		AutoInit: &autoInit,
-		Name:     &name,
-		Private:  &isPrivate,
-	})
-	if err != nil {
-		if !strings.Contains(err.Error(), "name already exists on this account") {
-			return fmt.Errorf("github create repository error: %w", err)
-		}
-	} else {
-		logSuccess("repository created")
-	}
-	return nil
-}
-
-func addGitHubTeam(ctx context.Context, hostname, owner, repository, token string, teamSlug, permission string) error {
-	gh, err := makeGitHubClient(hostname, token)
-	if err != nil {
-		return err
-	}
-
-	// check team exists
-	_, _, err = gh.Teams.GetTeamBySlug(ctx, owner, teamSlug)
-	if err != nil {
-		return fmt.Errorf("github get team %s error: %w", teamSlug, err)
-	}
-
-	// check if team is assigned to the repo
-	_, resp, err := gh.Teams.IsTeamRepoBySlug(ctx, owner, teamSlug, owner, repository)
-	if resp == nil && err != nil {
-		return fmt.Errorf("github is team %s error: %w", teamSlug, err)
-	}
-
-	// add team to the repo
-	if resp.StatusCode == 404 {
-		_, err = gh.Teams.AddTeamRepoBySlug(ctx, owner, teamSlug, owner, repository, &github.TeamAddTeamRepoOptions{
-			Permission: permission,
-		})
-		if err != nil {
-			return fmt.Errorf("github add team %s error: %w", teamSlug, err)
-		}
-	}
-
-	return nil
-}
-
-func checkoutGitHubRepository(ctx context.Context, url, branch, token, path string) (*git.Repository, error) {
-	auth := &http.BasicAuth{
-		Username: "git",
-		Password: token,
-	}
-	repo, err := git.PlainCloneContext(ctx, path, false, &git.CloneOptions{
-		URL:           url,
-		Auth:          auth,
-		RemoteName:    git.DefaultRemoteName,
-		ReferenceName: plumbing.NewBranchReferenceName(branch),
-		SingleBranch:  true,
-		NoCheckout:    false,
-		Progress:      nil,
-		Tags:          git.NoTags,
-	})
-	if err != nil {
-		return nil, fmt.Errorf("git clone error: %w", err)
-	}
-
-	_, err = repo.Head()
-	if err != nil {
-		return nil, fmt.Errorf("git resolve HEAD error: %w", err)
-	}
-
-	return repo, nil
-}
-
-func generateGitHubInstall(targetPath, namespace, tmpDir string) (string, error) {
-	tkDir := path.Join(tmpDir, ".tk")
-	defer os.RemoveAll(tkDir)
-
-	if err := os.MkdirAll(tkDir, os.ModePerm); err != nil {
-		return "", fmt.Errorf("generating manifests failed: %w", err)
-	}
-
-	if err := genInstallManifests(bootstrapVersion, namespace, components, tkDir); err != nil {
-		return "", fmt.Errorf("generating manifests failed: %w", err)
-	}
-
-	manifestsDir := path.Join(tmpDir, targetPath, namespace)
-	if err := os.MkdirAll(manifestsDir, os.ModePerm); err != nil {
-		return "", fmt.Errorf("generating manifests failed: %w", err)
-	}
-
-	manifest := path.Join(manifestsDir, ghInstallManifest)
-	if err := buildKustomization(tkDir, manifest); err != nil {
-		return "", fmt.Errorf("build kustomization failed: %w", err)
-	}
-
-	return manifest, nil
-}
-
-func commitGitHubManifests(repo *git.Repository, targetPath, namespace string) (bool, error) {
-	w, err := repo.Worktree()
-	if err != nil {
-		return false, err
-	}
-
-	_, err = w.Add(path.Join(targetPath, namespace))
-	if err != nil {
-		return false, err
-	}
-
-	status, err := w.Status()
-	if err != nil {
-		return false, err
-	}
-
-	if !status.IsClean() {
-		if _, err := w.Commit("Add manifests", &git.CommitOptions{
-			Author: &object.Signature{
-				Name:  "tk",
-				Email: "tk@users.noreply.github.com",
-				When:  time.Now(),
-			},
-		}); err != nil {
-			return false, err
-		}
-		return true, nil
-	}
-
-	return false, nil
-}
-
-func pushGitHubRepository(ctx context.Context, repo *git.Repository, token string) error {
-	auth := &http.BasicAuth{
-		Username: "git",
-		Password: token,
-	}
-	err := repo.PushContext(ctx, &git.PushOptions{
-		Auth:     auth,
-		Progress: nil,
-	})
-	if err != nil {
-		return fmt.Errorf("git push error: %w", err)
-	}
-	return nil
-}
-
-func generateGitHubKustomization(url, name, namespace, targetPath, tmpDir string, interval time.Duration) error {
-	gvk := sourcev1.GroupVersion.WithKind("GitRepository")
-	gitRepository := sourcev1.GitRepository{
-		TypeMeta: metav1.TypeMeta{
-			Kind:       gvk.Kind,
-			APIVersion: gvk.GroupVersion().String(),
-		},
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      name,
-			Namespace: namespace,
-		},
-		Spec: sourcev1.GitRepositorySpec{
-			URL: url,
-			Interval: metav1.Duration{
-				Duration: interval,
-			},
-			Reference: &sourcev1.GitRepositoryRef{
-				Branch: "master",
-			},
-			SecretRef: &corev1.LocalObjectReference{
-				Name: name,
-			},
-		},
-	}
-
-	gitData, err := yaml.Marshal(gitRepository)
-	if err != nil {
-		return err
-	}
-
-	if err := utils.writeFile(string(gitData), filepath.Join(tmpDir, targetPath, namespace, ghSourceManifest)); err != nil {
-		return err
-	}
-
-	gvk = kustomizev1.GroupVersion.WithKind("Kustomization")
-	emptyAPIGroup := ""
-	kustomization := kustomizev1.Kustomization{
-		TypeMeta: metav1.TypeMeta{
-			Kind:       gvk.Kind,
-			APIVersion: gvk.GroupVersion().String(),
-		},
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      name,
-			Namespace: namespace,
-		},
-		Spec: kustomizev1.KustomizationSpec{
-			Interval: metav1.Duration{
-				Duration: 10 * time.Minute,
-			},
-			Path:  fmt.Sprintf("./%s", strings.TrimPrefix(targetPath, "./")),
-			Prune: true,
-			SourceRef: corev1.TypedLocalObjectReference{
-				APIGroup: &emptyAPIGroup,
-				Kind:     "GitRepository",
-				Name:     name,
-			},
-		},
-	}
-
-	ksData, err := yaml.Marshal(kustomization)
-	if err != nil {
-		return err
-	}
-
-	if err := utils.writeFile(string(ksData), filepath.Join(tmpDir, targetPath, namespace, ghKustomizationManifest)); err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func applyGitHubKustomization(ctx context.Context, kubeClient client.Client, name, namespace, targetPath, tmpDir string) error {
-	command := fmt.Sprintf("kubectl apply -f %s", filepath.Join(tmpDir, targetPath, namespace))
-	if _, err := utils.execCommand(ctx, ModeStderrOS, command); err != nil {
-		return err
-	}
-
-	logWaiting("waiting for cluster sync")
-
-	if err := wait.PollImmediate(pollInterval, timeout,
-		isGitRepositoryReady(ctx, kubeClient, name, namespace)); err != nil {
-		return err
-	}
-
-	if err := wait.PollImmediate(pollInterval, timeout,
-		isKustomizationReady(ctx, kubeClient, name, namespace)); err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func shouldInstallGitHub(ctx context.Context, kubeClient client.Client, namespace string) bool {
-	namespacedName := types.NamespacedName{
-		Namespace: namespace,
-		Name:      namespace,
-	}
-	var kustomization kustomizev1.Kustomization
-	if err := kubeClient.Get(ctx, namespacedName, &kustomization); err != nil {
-		return true
-	}
-
-	return kustomization.Status.LastAppliedRevision == ""
-}
-
-func shouldCreateGitHubDeployKey(ctx context.Context, kubeClient client.Client, namespace string) bool {
-	namespacedName := types.NamespacedName{
-		Namespace: namespace,
-		Name:      namespace,
-	}
-
-	var existing corev1.Secret
-	if err := kubeClient.Get(ctx, namespacedName, &existing); err != nil {
-		return true
-	}
-	return false
-}
-
-func generateGitHubDeployKey(ctx context.Context, kubeClient client.Client, url *url.URL, namespace string) (string, error) {
-	pair, err := generateKeyPair(ctx)
-	if err != nil {
-		return "", err
-	}
-
-	hostKey, err := scanHostKey(ctx, url)
-	if err != nil {
-		return "", err
-	}
-
-	secret := corev1.Secret{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      namespace,
-			Namespace: namespace,
-		},
-		StringData: map[string]string{
-			"identity":     string(pair.PrivateKey),
-			"identity.pub": string(pair.PublicKey),
-			"known_hosts":  string(hostKey),
-		},
-	}
-	if err := upsertSecret(ctx, kubeClient, secret); err != nil {
-		return "", err
-	}
-
-	return string(pair.PublicKey), nil
-}
-
-func createGitHubDeployKey(ctx context.Context, key, hostname, owner, repository, targetPath, token string) error {
-	gh, err := makeGitHubClient(hostname, token)
-	if err != nil {
-		return err
-	}
-	keyName := "tk"
-	if targetPath != "" {
-		keyName = fmt.Sprintf("tk-%s", targetPath)
-	}
-
-	// list deploy keys
-	keys, resp, err := gh.Repositories.ListKeys(ctx, owner, repository, nil)
-	if err != nil {
-		return fmt.Errorf("github list deploy keys error: %w", err)
-	}
-	if resp.StatusCode >= 300 {
-		return fmt.Errorf("github list deploy keys failed with status code: %s", resp.Status)
-	}
-
-	// check if the key exists
-	shouldCreateKey := true
-	var existingKey *github.Key
-	for _, k := range keys {
-		if k.Title != nil && k.Key != nil && *k.Title == keyName {
-			if *k.Key != key {
-				existingKey = k
-			} else {
-				shouldCreateKey = false
-			}
-			break
-		}
-	}
-
-	// delete existing key if the value differs
-	if existingKey != nil {
-		resp, err := gh.Repositories.DeleteKey(ctx, owner, repository, *existingKey.ID)
-		if err != nil {
-			return fmt.Errorf("github delete deploy key error: %w", err)
-		}
-		if resp.StatusCode >= 300 {
-			return fmt.Errorf("github delete deploy key failed with status code: %s", resp.Status)
-		}
-	}
-
-	// create key
-	if shouldCreateKey {
-		isReadOnly := true
-		_, _, err = gh.Repositories.CreateKey(ctx, owner, repository, &github.Key{
-			Title:    &keyName,
-			Key:      &key,
-			ReadOnly: &isReadOnly,
-		})
-		if err != nil {
-			return fmt.Errorf("github create deploy key error: %w", err)
-		}
-	}
-
-	return nil
-}
diff --git a/cmd/tk/bootstrap_gitlab.go b/cmd/tk/bootstrap_gitlab.go
new file mode 100644
index 0000000000000000000000000000000000000000..0ec0e5ac0acedce54a2fb180b351005d7e38a0de
--- /dev/null
+++ b/cmd/tk/bootstrap_gitlab.go
@@ -0,0 +1,199 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"io/ioutil"
+	"net/url"
+	"os"
+	"path"
+	"time"
+
+	"github.com/spf13/cobra"
+
+	"github.com/fluxcd/toolkit/pkg/git"
+)
+
+var bootstrapGitLabCmd = &cobra.Command{
+	Use:   "gitlab",
+	Short: "Bootstrap GitLab repository",
+	Long: `
+The bootstrap command creates the GitHub repository if it doesn't exists and
+commits the toolkit components manifests to the master branch.
+Then it configure the target cluster to synchronize with the repository.
+If the toolkit components are present on the cluster,
+the bootstrap command will perform an upgrade if needed.`,
+	Example: `  # Create a GitLab API token and export it as an env var
+  export GITLAB_TOKEN=<my-token>
+
+  # Run bootstrap for a private repo owned by a GitLab group
+  bootstrap gitlab --owner=<group> --repository=<repo name>
+
+  # Run bootstrap for a repository path
+  bootstrap gitlab --owner=<group> --repository=<repo name> --path=dev-cluster
+
+  # Run bootstrap for a public repository on a personal account
+  bootstrap gitlab --owner=<user> --repository=<repo name> --private=false --personal=true 
+
+  # Run bootstrap for a private repo hosted on GitLab server 
+  bootstrap gitlab --owner=<group> --repository=<repo name> --hostname=<domain>
+`,
+	RunE: bootstrapGitLabCmdRun,
+}
+
+var (
+	glOwner      string
+	glRepository string
+	glInterval   time.Duration
+	glPersonal   bool
+	glPrivate    bool
+	glHostname   string
+	glPath       string
+)
+
+func init() {
+	bootstrapGitLabCmd.Flags().StringVar(&glOwner, "owner", "", "GitLab user or organization name")
+	bootstrapGitLabCmd.Flags().StringVar(&glRepository, "repository", "", "GitLab repository name")
+	bootstrapGitLabCmd.Flags().BoolVar(&glPersonal, "personal", false, "is personal repository")
+	bootstrapGitLabCmd.Flags().BoolVar(&glPrivate, "private", true, "is private repository")
+	bootstrapGitLabCmd.Flags().DurationVar(&glInterval, "interval", time.Minute, "sync interval")
+	bootstrapGitLabCmd.Flags().StringVar(&glHostname, "hostname", git.GitLabDefaultHostname, "GitLab hostname")
+	bootstrapGitLabCmd.Flags().StringVar(&glPath, "path", "", "repository path, when specified the cluster sync will be scoped to this path")
+
+	bootstrapCmd.AddCommand(bootstrapGitLabCmd)
+}
+
+func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
+	glToken := os.Getenv(git.GitLabTokenName)
+	if glToken == "" {
+		return fmt.Errorf("%s environment variable not found", git.GitLabTokenName)
+	}
+
+	repository, err := git.NewRepository(glRepository, glOwner, glHostname, glToken, "tk", "tk@users.noreply.gitlab.com")
+	if err != nil {
+		return err
+	}
+
+	provider := &git.GitLabProvider{
+		IsPrivate:  glPrivate,
+		IsPersonal: glPersonal,
+	}
+
+	kubeClient, err := utils.kubeClient(kubeconfig)
+	if err != nil {
+		return err
+	}
+
+	tmpDir, err := ioutil.TempDir("", namespace)
+	if err != nil {
+		return err
+	}
+	defer os.RemoveAll(tmpDir)
+
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+
+	// create GitLab project if doesn't exists
+	logAction("connecting to %s", glHostname)
+	changed, err := provider.CreateRepository(ctx, repository)
+	if err != nil {
+		return err
+	}
+	if changed {
+		logSuccess("repository created")
+	}
+
+	// clone repository and checkout the master branch
+	if err := repository.Checkout(ctx, bootstrapBranch, tmpDir); err != nil {
+		return err
+	}
+	logSuccess("repository cloned")
+
+	// generate install manifests
+	logGenerate("generating manifests")
+	manifest, err := generateInstallManifests(glPath, namespace, tmpDir)
+	if err != nil {
+		return err
+	}
+
+	// stage install manifests
+	changed, err = repository.Commit(ctx, path.Join(glPath, namespace), "Add manifests")
+	if err != nil {
+		return err
+	}
+
+	// push install manifests
+	if changed {
+		if err := repository.Push(ctx); err != nil {
+			return err
+		}
+		logSuccess("components manifests pushed")
+	} else {
+		logSuccess("components are up to date")
+	}
+
+	// determine if repo synchronization is working
+	isInstall := shouldInstallManifests(ctx, kubeClient, namespace)
+
+	if isInstall {
+		// apply install manifests
+		logAction("installing components in %s namespace", namespace)
+		if err := applyInstallManifests(ctx, manifest, components); err != nil {
+			return err
+		}
+		logSuccess("install completed")
+	}
+
+	// setup SSH deploy key
+	if shouldCreateDeployKey(ctx, kubeClient, namespace) {
+		logAction("configuring deploy key")
+		u, err := url.Parse(repository.GetSSH())
+		if err != nil {
+			return fmt.Errorf("git URL parse failed: %w", err)
+		}
+
+		key, err := generateDeployKey(ctx, kubeClient, u, namespace)
+		if err != nil {
+			return fmt.Errorf("generating deploy key failed: %w", err)
+		}
+
+		keyName := "tk"
+		if glPath != "" {
+			keyName = fmt.Sprintf("tk-%s", glPath)
+		}
+
+		if changed, err := provider.AddDeployKey(ctx, repository, key, keyName); err != nil {
+			return err
+		} else if changed {
+			logSuccess("deploy key configured")
+		}
+	}
+
+	// configure repo synchronization
+	if isInstall {
+		// generate source and kustomization manifests
+		logAction("generating sync manifests")
+		if err := generateSyncManifests(repository.GetSSH(), namespace, namespace, glPath, tmpDir, glInterval); err != nil {
+			return err
+		}
+
+		// commit and push manifests
+		if changed, err = repository.Commit(ctx, path.Join(glPath, namespace), "Add manifests"); err != nil {
+			return err
+		} else if changed {
+			if err := repository.Push(ctx); err != nil {
+				return err
+			}
+			logSuccess("sync manifests pushed")
+		}
+
+		// apply manifests and waiting for sync
+		logAction("applying sync manifests")
+		if err := applySyncManifests(ctx, kubeClient, namespace, namespace, glPath, tmpDir); err != nil {
+			return err
+		}
+	}
+
+	logSuccess("bootstrap finished")
+	return nil
+}
diff --git a/docs/cmd/tk.md b/docs/cmd/tk.md
index a957dea02f4c8d36ff28e2c3d6904af7a5f1aa0f..4381649debcdc7be74f051b4cabccd2433f04934 100644
--- a/docs/cmd/tk.md
+++ b/docs/cmd/tk.md
@@ -90,4 +90,4 @@ Command line utility for assembling Kubernetes CD pipelines the GitOps way.
 * [tk sync](tk_sync.md)	 - Synchronize commands
 * [tk uninstall](tk_uninstall.md)	 - Uninstall the toolkit components
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_bootstrap.md b/docs/cmd/tk_bootstrap.md
index 7d0990d786ccb2672b29c529f58f1bf24f16f6f8..09227700e2bb375144a22b2874a1577b45f68fd7 100644
--- a/docs/cmd/tk_bootstrap.md
+++ b/docs/cmd/tk_bootstrap.md
@@ -27,5 +27,6 @@ Bootstrap commands
 
 * [tk](tk.md)	 - Command line utility for assembling Kubernetes CD pipelines
 * [tk bootstrap github](tk_bootstrap_github.md)	 - Bootstrap GitHub repository
+* [tk bootstrap gitlab](tk_bootstrap_gitlab.md)	 - Bootstrap GitLab repository
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_bootstrap_github.md b/docs/cmd/tk_bootstrap_github.md
index 2210f60ca59e285063095afe95cb5c5288e24ee6..80c4bf654f77208ea48bc098b9fb8792c5d61017 100644
--- a/docs/cmd/tk_bootstrap_github.md
+++ b/docs/cmd/tk_bootstrap_github.md
@@ -24,6 +24,12 @@ tk bootstrap github [flags]
   # Run bootstrap for a private repo owned by a GitHub organization
   bootstrap github --owner=<organization> --repository=<repo name>
 
+  # Run bootstrap for a private repo and assign organization teams to it
+  bootstrap github --owner=<organization> --repository=<repo name> --team=<team1 slug> --team=<team2 slug>
+
+  # Run bootstrap for a repository path
+  bootstrap github --owner=<organization> --repository=<repo name> --path=dev-cluster
+
   # Run bootstrap for a public repository on a personal account
   bootstrap github --owner=<user> --repository=<repo name> --private=false --personal=true 
 
@@ -39,9 +45,11 @@ tk bootstrap github [flags]
       --hostname string     GitHub hostname (default "github.com")
       --interval duration   sync interval (default 1m0s)
       --owner string        GitHub user or organization name
+      --path string         repository path, when specified the cluster sync will be scoped to this path
       --personal            is personal repository
       --private             is private repository (default true)
       --repository string   GitHub repository name
+      --team stringArray    GitHub team to be given maintainer access
 ```
 
 ### Options inherited from parent commands
@@ -59,4 +67,4 @@ tk bootstrap github [flags]
 
 * [tk bootstrap](tk_bootstrap.md)	 - Bootstrap commands
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_bootstrap_gitlab.md b/docs/cmd/tk_bootstrap_gitlab.md
new file mode 100644
index 0000000000000000000000000000000000000000..983ede4cf0404783d6fb755bdbf25f1876d76997
--- /dev/null
+++ b/docs/cmd/tk_bootstrap_gitlab.md
@@ -0,0 +1,66 @@
+## tk bootstrap gitlab
+
+Bootstrap GitLab repository
+
+### Synopsis
+
+
+The bootstrap command creates the GitHub repository if it doesn't exists and
+commits the toolkit components manifests to the master branch.
+Then it configure the target cluster to synchronize with the repository.
+If the toolkit components are present on the cluster,
+the bootstrap command will perform an upgrade if needed.
+
+```
+tk bootstrap gitlab [flags]
+```
+
+### Examples
+
+```
+  # Create a GitLab API token and export it as an env var
+  export GITLAB_TOKEN=<my-token>
+
+  # Run bootstrap for a private repo owned by a GitLab group
+  bootstrap gitlab --owner=<group> --repository=<repo name>
+
+  # Run bootstrap for a repository path
+  bootstrap gitlab --owner=<group> --repository=<repo name> --path=dev-cluster
+
+  # Run bootstrap for a public repository on a personal account
+  bootstrap gitlab --owner=<user> --repository=<repo name> --private=false --personal=true 
+
+  # Run bootstrap for a private repo hosted on GitLab server 
+  bootstrap gitlab --owner=<group> --repository=<repo name> --hostname=<domain>
+
+```
+
+### Options
+
+```
+  -h, --help                help for gitlab
+      --hostname string     GitLab hostname (default "gitlab.com")
+      --interval duration   sync interval (default 1m0s)
+      --owner string        GitLab user or organization name
+      --path string         repository path, when specified the cluster sync will be scoped to this path
+      --personal            is personal repository
+      --private             is private repository (default true)
+      --repository string   GitLab repository name
+```
+
+### Options inherited from parent commands
+
+```
+      --components strings   list of components, accepts comma-separated values (default [source-controller,kustomize-controller])
+      --kubeconfig string    path to the kubeconfig file (default "~/.kube/config")
+      --namespace string     the namespace scope for this operation (default "gitops-system")
+      --timeout duration     timeout for this operation (default 5m0s)
+      --verbose              print generated objects
+      --version string       toolkit tag or branch (default "master")
+```
+
+### SEE ALSO
+
+* [tk bootstrap](tk_bootstrap.md)	 - Bootstrap commands
+
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_check.md b/docs/cmd/tk_check.md
index 3f06559264524f518a0f36652c94a2c1eac6c950..de378dc8c02f42f720baec47bf906bcf20e95733 100644
--- a/docs/cmd/tk_check.md
+++ b/docs/cmd/tk_check.md
@@ -44,4 +44,4 @@ tk check [flags]
 
 * [tk](tk.md)	 - Command line utility for assembling Kubernetes CD pipelines
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_completion.md b/docs/cmd/tk_completion.md
index 14f7c14638b033def7b3564ea2fcc686b74cd06a..904dbedf94e133c2b7011348f098efef492cf9d0 100644
--- a/docs/cmd/tk_completion.md
+++ b/docs/cmd/tk_completion.md
@@ -44,4 +44,4 @@ To configure your bash shell to load completions for each session add to your ba
 
 * [tk](tk.md)	 - Command line utility for assembling Kubernetes CD pipelines
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_create.md b/docs/cmd/tk_create.md
index 286a204cc970232e32c5ea394bcd7f8a025185d6..4441d63d05d8bdc7c41f4b2b1b7e0673f4007024 100644
--- a/docs/cmd/tk_create.md
+++ b/docs/cmd/tk_create.md
@@ -30,4 +30,4 @@ Create commands
 * [tk create kustomization](tk_create_kustomization.md)	 - Create or update a kustomization resource
 * [tk create source](tk_create_source.md)	 - Create source commands
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_create_kustomization.md b/docs/cmd/tk_create_kustomization.md
index 6cfb8f8437719c61688becbf50a656b128b5c2b8..1242decba7c75cb818e32c35f5134983827ff9ef 100644
--- a/docs/cmd/tk_create_kustomization.md
+++ b/docs/cmd/tk_create_kustomization.md
@@ -78,4 +78,4 @@ tk create kustomization [name] [flags]
 
 * [tk create](tk_create.md)	 - Create commands
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_create_source.md b/docs/cmd/tk_create_source.md
index 4d2559a6cc4d9140b97b347ada01043c86dffcd5..e4566e02f59b06ae091e37da4650216c6e382a05 100644
--- a/docs/cmd/tk_create_source.md
+++ b/docs/cmd/tk_create_source.md
@@ -29,4 +29,4 @@ Create source commands
 * [tk create](tk_create.md)	 - Create commands
 * [tk create source git](tk_create_source_git.md)	 - Create or update a git source
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_create_source_git.md b/docs/cmd/tk_create_source_git.md
index b6d2687ba0d2a0e56f753df9e842af705b093fdb..2ab7804519f1e9ee0ba41c4909a4049783b9f171 100644
--- a/docs/cmd/tk_create_source_git.md
+++ b/docs/cmd/tk_create_source_git.md
@@ -58,7 +58,7 @@ tk create source git [name] [flags]
       --branch string                          git branch (default "master")
   -h, --help                                   help for git
   -p, --password string                        basic authentication password
-      --ssh-ecdsa-curve ecdsaCurve             SSH ECDSA public key curve (p521, p256, p384) (default p384)
+      --ssh-ecdsa-curve ecdsaCurve             SSH ECDSA public key curve (p256, p384, p521) (default p384)
       --ssh-key-algorithm publicKeyAlgorithm   SSH public key algorithm (rsa, ecdsa, ed25519) (default rsa)
       --ssh-rsa-bits rsaKeyBits                SSH RSA public key bit size (multiplies of 8) (default 2048)
       --tag string                             git tag
@@ -83,4 +83,4 @@ tk create source git [name] [flags]
 
 * [tk create source](tk_create_source.md)	 - Create source commands
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_delete.md b/docs/cmd/tk_delete.md
index 09d149fcc53642793c424806c718f6384ddd87c0..438aaf651bafddf1ccfa86ab3a6bccbc5f8a9923 100644
--- a/docs/cmd/tk_delete.md
+++ b/docs/cmd/tk_delete.md
@@ -29,4 +29,4 @@ Delete commands
 * [tk delete kustomization](tk_delete_kustomization.md)	 - Delete kustomization
 * [tk delete source](tk_delete_source.md)	 - Delete sources commands
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_delete_kustomization.md b/docs/cmd/tk_delete_kustomization.md
index be27ba4a605a7a078b3febc6730f01911ef05715..4cc8db77b6f21c9060b905b8759e1d229c84316a 100644
--- a/docs/cmd/tk_delete_kustomization.md
+++ b/docs/cmd/tk_delete_kustomization.md
@@ -31,4 +31,4 @@ tk delete kustomization [name] [flags]
 
 * [tk delete](tk_delete.md)	 - Delete commands
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_delete_source.md b/docs/cmd/tk_delete_source.md
index 0aaac494e6fd66cc887dd9a495755940e9037bb3..78184fac36d470d9dc4e6f8b5944330fb08e1ac3 100644
--- a/docs/cmd/tk_delete_source.md
+++ b/docs/cmd/tk_delete_source.md
@@ -28,4 +28,4 @@ Delete sources commands
 * [tk delete](tk_delete.md)	 - Delete commands
 * [tk delete source git](tk_delete_source_git.md)	 - Delete git source
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_delete_source_git.md b/docs/cmd/tk_delete_source_git.md
index 45b5c40e511cbdd63bfa41ec3370cc9a09d64d66..57b315fdf2705a967ea53d3c51f137e8952ab145 100644
--- a/docs/cmd/tk_delete_source_git.md
+++ b/docs/cmd/tk_delete_source_git.md
@@ -31,4 +31,4 @@ tk delete source git [name] [flags]
 
 * [tk delete source](tk_delete_source.md)	 - Delete sources commands
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_export.md b/docs/cmd/tk_export.md
index b96750624133ec3f9a3a827d02cb2209170d3fab..fe894afaea177dccc745d5becc74efdf3ddbcb0c 100644
--- a/docs/cmd/tk_export.md
+++ b/docs/cmd/tk_export.md
@@ -29,4 +29,4 @@ Export commands
 * [tk export kustomization](tk_export_kustomization.md)	 - Export kustomization in YAML format
 * [tk export source](tk_export_source.md)	 - Export source commands
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_export_kustomization.md b/docs/cmd/tk_export_kustomization.md
index b13835782ae289fefb7354d6112cd27ee900bcb5..f855247a2f1937c3447fd2c0b2462cf1fc81d8e8 100644
--- a/docs/cmd/tk_export_kustomization.md
+++ b/docs/cmd/tk_export_kustomization.md
@@ -42,4 +42,4 @@ tk export kustomization [name] [flags]
 
 * [tk export](tk_export.md)	 - Export commands
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_export_source.md b/docs/cmd/tk_export_source.md
index c9fed0ab4a5ac1aedeffbaa8822d389deded4cb6..638dbea3a5e13509c24a6fe722968baf3a42c56e 100644
--- a/docs/cmd/tk_export_source.md
+++ b/docs/cmd/tk_export_source.md
@@ -29,4 +29,4 @@ Export source commands
 * [tk export](tk_export.md)	 - Export commands
 * [tk export source git](tk_export_source_git.md)	 - Export git sources in YAML format
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_export_source_git.md b/docs/cmd/tk_export_source_git.md
index 222aca9e8cc1a0ba9420f004169f76f74b32ff1d..5d41f1bbf330787ac95b6b2c735f3765e2d32592 100644
--- a/docs/cmd/tk_export_source_git.md
+++ b/docs/cmd/tk_export_source_git.md
@@ -43,4 +43,4 @@ tk export source git [name] [flags]
 
 * [tk export source](tk_export_source.md)	 - Export source commands
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_get.md b/docs/cmd/tk_get.md
index 4ed527cffdb6725be4e5337c8725fb03a71adf93..afcf9f312df512d9e4b5e6abe61fae0a50d9b6ae 100644
--- a/docs/cmd/tk_get.md
+++ b/docs/cmd/tk_get.md
@@ -28,4 +28,4 @@ Get commands
 * [tk get kustomizations](tk_get_kustomizations.md)	 - Get kustomizations status
 * [tk get sources](tk_get_sources.md)	 - Get sources commands
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_get_kustomizations.md b/docs/cmd/tk_get_kustomizations.md
index e41c66e565816d88e2265055a7a6a7344f909af1..c7386fa0dcb9e68da0df95d1121800f1e9b40ff9 100644
--- a/docs/cmd/tk_get_kustomizations.md
+++ b/docs/cmd/tk_get_kustomizations.md
@@ -31,4 +31,4 @@ tk get kustomizations [flags]
 
 * [tk get](tk_get.md)	 - Get commands
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_get_sources.md b/docs/cmd/tk_get_sources.md
index c5a31c8cf935fddb592380cadffe92291bd94f5a..b70d4224005f5743672d9618fe6353ab9d516d96 100644
--- a/docs/cmd/tk_get_sources.md
+++ b/docs/cmd/tk_get_sources.md
@@ -27,4 +27,4 @@ Get sources commands
 * [tk get](tk_get.md)	 - Get commands
 * [tk get sources git](tk_get_sources_git.md)	 - Get git sources status
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_get_sources_git.md b/docs/cmd/tk_get_sources_git.md
index 630ab8c62c14dd3ccc957851512cf14e38aaa7b9..6a26f86b9b0f0b71729b52941605f28e8eed9f17 100644
--- a/docs/cmd/tk_get_sources_git.md
+++ b/docs/cmd/tk_get_sources_git.md
@@ -31,4 +31,4 @@ tk get sources git [flags]
 
 * [tk get sources](tk_get_sources.md)	 - Get sources commands
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_install.md b/docs/cmd/tk_install.md
index 0da217201c12a92e2ffa138979fedade4c91b99f..7b077b31002d679c875bd40d1b50e7bf18504b06 100644
--- a/docs/cmd/tk_install.md
+++ b/docs/cmd/tk_install.md
@@ -49,4 +49,4 @@ tk install [flags]
 
 * [tk](tk.md)	 - Command line utility for assembling Kubernetes CD pipelines
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_resume.md b/docs/cmd/tk_resume.md
index 5127bb758b622a854e2f4c092949a47702b0f7e4..81c2345851e09c977fd827b8ba49c31a39dd747c 100644
--- a/docs/cmd/tk_resume.md
+++ b/docs/cmd/tk_resume.md
@@ -27,4 +27,4 @@ Resume commands
 * [tk](tk.md)	 - Command line utility for assembling Kubernetes CD pipelines
 * [tk resume kustomization](tk_resume_kustomization.md)	 - Resume kustomization
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_resume_kustomization.md b/docs/cmd/tk_resume_kustomization.md
index a0de59bac22fcf24c4423d00162af42f783e819d..180bc8d1a3e53d34248a01f7fe4df2114cc8560a 100644
--- a/docs/cmd/tk_resume_kustomization.md
+++ b/docs/cmd/tk_resume_kustomization.md
@@ -30,4 +30,4 @@ tk resume kustomization [name] [flags]
 
 * [tk resume](tk_resume.md)	 - Resume commands
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_suspend.md b/docs/cmd/tk_suspend.md
index b15f0e892215927e66206bdee0361faec51e957f..d99a4ddfd6dfe5f54bcb398ae6bfac2c09f60832 100644
--- a/docs/cmd/tk_suspend.md
+++ b/docs/cmd/tk_suspend.md
@@ -27,4 +27,4 @@ Suspend commands
 * [tk](tk.md)	 - Command line utility for assembling Kubernetes CD pipelines
 * [tk suspend kustomization](tk_suspend_kustomization.md)	 - Suspend kustomization
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_suspend_kustomization.md b/docs/cmd/tk_suspend_kustomization.md
index 50844a5ffe0134ce0cd64cd2e96b44718f9abafc..eaaf8c60effc91fe491ab6caef4cb1ff9fc81fb0 100644
--- a/docs/cmd/tk_suspend_kustomization.md
+++ b/docs/cmd/tk_suspend_kustomization.md
@@ -30,4 +30,4 @@ tk suspend kustomization [name] [flags]
 
 * [tk suspend](tk_suspend.md)	 - Suspend commands
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_sync.md b/docs/cmd/tk_sync.md
index 25351c408632d008ab3f897b8881c18340e2b102..df50a41168642500cd0d6594f27105086eb19b1b 100644
--- a/docs/cmd/tk_sync.md
+++ b/docs/cmd/tk_sync.md
@@ -28,4 +28,4 @@ Synchronize commands
 * [tk sync kustomization](tk_sync_kustomization.md)	 - Synchronize kustomization
 * [tk sync source](tk_sync_source.md)	 - Synchronize source commands
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_sync_kustomization.md b/docs/cmd/tk_sync_kustomization.md
index 24b06fc88e2381f12740b29a5820abd1d39cc2d9..47c2a65de3646696a9fb5b26c281baa66ad7ef1c 100644
--- a/docs/cmd/tk_sync_kustomization.md
+++ b/docs/cmd/tk_sync_kustomization.md
@@ -43,4 +43,4 @@ tk sync kustomization [name] [flags]
 
 * [tk sync](tk_sync.md)	 - Synchronize commands
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_sync_source.md b/docs/cmd/tk_sync_source.md
index 35ba621f23fc975565371bc043e2d292b41320a1..7343d584d377e7a1a21ae09ad1fe4670d60d1af2 100644
--- a/docs/cmd/tk_sync_source.md
+++ b/docs/cmd/tk_sync_source.md
@@ -27,4 +27,4 @@ Synchronize source commands
 * [tk sync](tk_sync.md)	 - Synchronize commands
 * [tk sync source git](tk_sync_source_git.md)	 - Synchronize git source
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_sync_source_git.md b/docs/cmd/tk_sync_source_git.md
index a08420c8dabb73234cceb14e9e13b593df13ea03..cf4cf226efb1cdd573c31e9f46e52541da3f4624 100644
--- a/docs/cmd/tk_sync_source_git.md
+++ b/docs/cmd/tk_sync_source_git.md
@@ -39,4 +39,4 @@ tk sync source git [name] [flags]
 
 * [tk sync source](tk_sync_source.md)	 - Synchronize source commands
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/cmd/tk_uninstall.md b/docs/cmd/tk_uninstall.md
index 467ddeb321f77d70d2dd4e60d03121c1b7ed5dc1..1e7e5f254658e87acc9067cff66612ed2ef538e4 100644
--- a/docs/cmd/tk_uninstall.md
+++ b/docs/cmd/tk_uninstall.md
@@ -26,10 +26,11 @@ tk uninstall [flags]
 ### Options
 
 ```
-      --crds      removes all CRDs previously installed
-      --dry-run   only print the object that would be deleted
-  -h, --help      help for uninstall
-  -s, --silent    delete components without asking for confirmation
+      --crds             removes all CRDs previously installed
+      --dry-run          only print the object that would be deleted
+  -h, --help             help for uninstall
+      --kustomizations   removes all kustomizations previously installed
+  -s, --silent           delete components without asking for confirmation
 ```
 
 ### Options inherited from parent commands
@@ -46,4 +47,4 @@ tk uninstall [flags]
 
 * [tk](tk.md)	 - Command line utility for assembling Kubernetes CD pipelines
 
-###### Auto generated by spf13/cobra on 9-Jun-2020
+###### Auto generated by spf13/cobra on 18-Jun-2020
diff --git a/docs/internal/release.md b/docs/internal/release.md
index 987c2e3cbf867067deacac0455cc1f7fea10c23c..fde81caea87489d999660c22cd48f0a69fe93ce9 100644
--- a/docs/internal/release.md
+++ b/docs/internal/release.md
@@ -2,11 +2,6 @@
 
 To release a new version the following steps should be followed:
 
-1. Create a new branch from `master` i.e. `release-<next semver>`. This
-   will function as your release preparation branch.
-1. Change the `VERSION` value in `cmd/tk/main.go` to that of the
-   semver release you are going to make. Commit and push your changes.
-1. Create a PR for your release branch and get it merged into `master`.
-1. Create a `<next semver>` tag for the merge commit in `master` and
+1. Create a `<next semver>` tag form `master` and
    push it to remote.
 1. Confirm CI builds and releases the newly tagged version.
diff --git a/go.mod b/go.mod
index d8b917b0c9f25ef73b808b5ab2d0709f73f1788e..93697b8d51144def7b783c584aeffbed82633849 100644
--- a/go.mod
+++ b/go.mod
@@ -7,10 +7,18 @@ require (
 	github.com/fluxcd/kustomize-controller v0.0.1-beta.2
 	github.com/fluxcd/source-controller v0.0.1-beta.2
 	github.com/go-git/go-git/v5 v5.0.0
+	github.com/golang/protobuf v1.4.2 // indirect
 	github.com/google/go-github/v32 v32.0.0
+	github.com/hashicorp/go-retryablehttp v0.6.6 // indirect
 	github.com/manifoldco/promptui v0.7.0
 	github.com/spf13/cobra v1.0.0
+	github.com/xanzy/go-gitlab v0.32.1
 	golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073
+	golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect
+	golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect
+	golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect
+	google.golang.org/appengine v1.6.6 // indirect
+	google.golang.org/protobuf v1.24.0 // indirect
 	k8s.io/api v0.18.2
 	k8s.io/apimachinery v0.18.2
 	k8s.io/client-go v0.18.2
diff --git a/go.sum b/go.sum
index b0d1eb80f022cb852341930340b66c2ba4baf584..4f610e47afdbe072a6fed9b699e4f9061272db4e 100644
--- a/go.sum
+++ b/go.sum
@@ -293,6 +293,14 @@ github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
 github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=
 github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
@@ -317,6 +325,7 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-github/v32 v32.0.0 h1:q74KVb22spUq0U5HqZ9VCYqQz8YRuOtL/39ZnfwO+NM=
@@ -360,7 +369,15 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t
 github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-cleanhttp v0.5.0 h1:wvCrVc9TjDls6+YGAF2hAifE1E5U1+b4tH6KdvN3Gig=
 github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
+github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
+github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
 github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
+github.com/hashicorp/go-retryablehttp v0.6.4 h1:BbgctKO892xEyOXnGiaAwIoSq1QZ/SS4AhjoAh9DnfY=
+github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
+github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM=
+github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
 github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo=
 github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=
 github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0=
@@ -617,6 +634,8 @@ github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk
 github.com/valyala/quicktemplate v1.2.0/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
 github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
 github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
+github.com/xanzy/go-gitlab v0.32.1 h1:eKGfAP2FWbqStD7DtGoRBb18IYwjuCxdtEVea2rNge4=
+github.com/xanzy/go-gitlab v0.32.1/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug=
 github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
 github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@@ -686,6 +705,7 @@ golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73r
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -696,6 +716,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
 golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -707,11 +728,16 @@ golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
 golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
+golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -748,6 +774,8 @@ golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7 h1:HmbHVPwrPEKPGLAcHSrMe6+hq
 golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
 golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -758,6 +786,10 @@ golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxb
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
+golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -797,9 +829,12 @@ google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -808,6 +843,7 @@ google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRn
 google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
 google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
@@ -817,6 +853,17 @@ google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
 google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
 gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/pkg/git/provider.go b/pkg/git/provider.go
new file mode 100644
index 0000000000000000000000000000000000000000..909f39674bfad0966d4dc1dfd92e734b16f1e180
--- /dev/null
+++ b/pkg/git/provider.go
@@ -0,0 +1,10 @@
+package git
+
+import "context"
+
+// Provider is the interface that a git provider should implement
+type Provider interface {
+	CreateRepository(ctx context.Context, r *Repository) (bool, error)
+	AddTeam(ctx context.Context, r *Repository, name, permission string) (bool, error)
+	AddDeployKey(ctx context.Context, r *Repository, key, keyName string) (bool, error)
+}
diff --git a/pkg/git/provider_github.go b/pkg/git/provider_github.go
new file mode 100644
index 0000000000000000000000000000000000000000..efb2edc1fc958356ec96d9a37d2b9219a4784238
--- /dev/null
+++ b/pkg/git/provider_github.go
@@ -0,0 +1,161 @@
+package git
+
+import (
+	"context"
+	"fmt"
+	"github.com/google/go-github/v32/github"
+	"strings"
+)
+
+// GithubProvider represents a GitHub API wrapper
+type GithubProvider struct {
+	IsPrivate  bool
+	IsPersonal bool
+}
+
+const (
+	GitHubTokenName       = "GITHUB_TOKEN"
+	GitHubDefaultHostname = "github.com"
+)
+
+func (p *GithubProvider) newClient(r *Repository) (*github.Client, error) {
+	auth := github.BasicAuthTransport{
+		Username: "git",
+		Password: r.Token,
+	}
+
+	gh := github.NewClient(auth.Client())
+	if r.Host != GitHubDefaultHostname {
+		baseURL := fmt.Sprintf("https://%s/api/v3/", r.Host)
+		uploadURL := fmt.Sprintf("https://%s/api/uploads/", r.Host)
+		if g, err := github.NewEnterpriseClient(baseURL, uploadURL, auth.Client()); err == nil {
+			gh = g
+		} else {
+			return nil, err
+		}
+	}
+
+	return gh, nil
+}
+
+// CreateRepository returns false if the repository exists
+func (p *GithubProvider) CreateRepository(ctx context.Context, r *Repository) (bool, error) {
+	gh, err := p.newClient(r)
+	if err != nil {
+		return false, fmt.Errorf("client error: %w", err)
+	}
+	org := ""
+	if !p.IsPersonal {
+		org = r.Owner
+	}
+
+	if _, _, err := gh.Repositories.Get(ctx, org, r.Name); err == nil {
+		return false, nil
+	}
+
+	autoInit := true
+	_, _, err = gh.Repositories.Create(ctx, org, &github.Repository{
+		AutoInit: &autoInit,
+		Name:     &r.Name,
+		Private:  &p.IsPrivate,
+	})
+	if err != nil {
+		if !strings.Contains(err.Error(), "name already exists on this account") {
+			return false, fmt.Errorf("create repository error: %w", err)
+		}
+	} else {
+		return true, nil
+	}
+	return false, nil
+}
+
+// AddTeam returns false if the team is already assigned to the repository
+func (p *GithubProvider) AddTeam(ctx context.Context, r *Repository, name, permission string) (bool, error) {
+	gh, err := p.newClient(r)
+	if err != nil {
+		return false, fmt.Errorf("client error: %w", err)
+	}
+
+	// check team exists
+	_, _, err = gh.Teams.GetTeamBySlug(ctx, r.Owner, name)
+	if err != nil {
+		return false, fmt.Errorf("get team %s error: %w", name, err)
+	}
+
+	// check if team is assigned to the repo
+	_, resp, err := gh.Teams.IsTeamRepoBySlug(ctx, r.Owner, name, r.Owner, r.Name)
+	if resp == nil && err != nil {
+		return false, fmt.Errorf("is team %s error: %w", name, err)
+	}
+
+	// add team to the repo
+	if resp.StatusCode == 404 {
+		_, err = gh.Teams.AddTeamRepoBySlug(ctx, r.Owner, name, r.Owner, r.Name, &github.TeamAddTeamRepoOptions{
+			Permission: permission,
+		})
+		if err != nil {
+			return false, fmt.Errorf("add team %s error: %w", name, err)
+		}
+		return true, nil
+	}
+
+	return false, nil
+}
+
+// AddDeployKey returns false if the key exists and the content is the same
+func (p *GithubProvider) AddDeployKey(ctx context.Context, r *Repository, key, keyName string) (bool, error) {
+	gh, err := p.newClient(r)
+	if err != nil {
+		return false, fmt.Errorf("client error: %w", err)
+	}
+
+	// list deploy keys
+	keys, resp, err := gh.Repositories.ListKeys(ctx, r.Owner, r.Name, nil)
+	if err != nil {
+		return false, fmt.Errorf("list deploy keys error: %w", err)
+	}
+	if resp.StatusCode >= 300 {
+		return false, fmt.Errorf("list deploy keys failed with status code: %s", resp.Status)
+	}
+
+	// check if the key exists
+	shouldCreateKey := true
+	var existingKey *github.Key
+	for _, k := range keys {
+		if k.Title != nil && k.Key != nil && *k.Title == keyName {
+			if *k.Key != key {
+				existingKey = k
+			} else {
+				shouldCreateKey = false
+			}
+			break
+		}
+	}
+
+	// delete existing key if the value differs
+	if existingKey != nil {
+		resp, err := gh.Repositories.DeleteKey(ctx, r.Owner, r.Name, *existingKey.ID)
+		if err != nil {
+			return false, fmt.Errorf("delete deploy key error: %w", err)
+		}
+		if resp.StatusCode >= 300 {
+			return false, fmt.Errorf("delete deploy key failed with status code: %s", resp.Status)
+		}
+	}
+
+	// create key
+	if shouldCreateKey {
+		isReadOnly := true
+		_, _, err = gh.Repositories.CreateKey(ctx, r.Owner, r.Name, &github.Key{
+			Title:    &keyName,
+			Key:      &key,
+			ReadOnly: &isReadOnly,
+		})
+		if err != nil {
+			return false, fmt.Errorf("create deploy key error: %w", err)
+		}
+		return true, nil
+	}
+
+	return false, nil
+}
diff --git a/pkg/git/provider_gitlab.go b/pkg/git/provider_gitlab.go
new file mode 100644
index 0000000000000000000000000000000000000000..07d97649f2fa1c81f922a723cafc6789c6771932
--- /dev/null
+++ b/pkg/git/provider_gitlab.go
@@ -0,0 +1,147 @@
+package git
+
+import (
+	"context"
+	"fmt"
+	"github.com/xanzy/go-gitlab"
+)
+
+// GitLabProvider represents a GitLab API wrapper
+type GitLabProvider struct {
+	IsPrivate  bool
+	IsPersonal bool
+}
+
+const (
+	GitLabTokenName       = "GITLAB_TOKEN"
+	GitLabDefaultHostname = "gitlab.com"
+)
+
+func (p *GitLabProvider) newClient(r *Repository) (*gitlab.Client, error) {
+	gl, err := gitlab.NewClient(r.Token)
+	if err != nil {
+		return nil, err
+	}
+
+	if r.Host != GitLabDefaultHostname {
+		gl, err = gitlab.NewClient(r.Token, gitlab.WithBaseURL(fmt.Sprintf("https://%s/api/v4", r.Host)))
+		if err != nil {
+			return nil, err
+		}
+	}
+	return gl, nil
+}
+
+// CreateRepository returns false if the repository already exists
+func (p *GitLabProvider) CreateRepository(ctx context.Context, r *Repository) (bool, error) {
+	gl, err := p.newClient(r)
+	if err != nil {
+		return false, fmt.Errorf("client error: %w", err)
+	}
+
+	var id *int
+	if !p.IsPersonal {
+		groups, _, err := gl.Groups.ListGroups(&gitlab.ListGroupsOptions{Search: gitlab.String(r.Owner)}, gitlab.WithContext(ctx))
+		if err != nil {
+			return false, fmt.Errorf("list groups error: %w", err)
+		}
+
+		if len(groups) > 0 {
+			id = &groups[0].ID
+		}
+	}
+
+	visibility := gitlab.PublicVisibility
+	if p.IsPrivate {
+		visibility = gitlab.PrivateVisibility
+	}
+
+	projects, _, err := gl.Projects.ListProjects(&gitlab.ListProjectsOptions{Search: gitlab.String(r.Name)}, gitlab.WithContext(ctx))
+	if err != nil {
+		return false, fmt.Errorf("list projects error: %w", err)
+	}
+
+	if len(projects) == 0 {
+		p := &gitlab.CreateProjectOptions{
+			Name:                 gitlab.String(r.Name),
+			NamespaceID:          id,
+			Visibility:           &visibility,
+			InitializeWithReadme: gitlab.Bool(true),
+		}
+
+		_, _, err := gl.Projects.CreateProject(p)
+		if err != nil {
+			return false, fmt.Errorf("create project error: %w", err)
+		}
+		return true, nil
+	}
+
+	return false, nil
+}
+
+// AddTeam returns false if the team is already assigned to the repository
+func (p *GitLabProvider) AddTeam(ctx context.Context, r *Repository, name, permission string) (bool, error) {
+	return false, nil
+}
+
+// AddDeployKey returns false if the key exists and the content is the same
+func (p *GitLabProvider) AddDeployKey(ctx context.Context, r *Repository, key, keyName string) (bool, error) {
+	gl, err := p.newClient(r)
+	if err != nil {
+		return false, fmt.Errorf("client error: %w", err)
+	}
+
+	// list deploy keys
+	var projId int
+	projects, _, err := gl.Projects.ListProjects(&gitlab.ListProjectsOptions{Search: gitlab.String(r.Name)}, gitlab.WithContext(ctx))
+	if err != nil {
+		return false, fmt.Errorf("list projects error: %w", err)
+	}
+	if len(projects) > 0 {
+		projId = projects[0].ID
+	} else {
+		return false, fmt.Errorf("no project found")
+	}
+
+	// check if the key exists
+	keys, _, err := gl.DeployKeys.ListProjectDeployKeys(projId, &gitlab.ListProjectDeployKeysOptions{})
+	if err != nil {
+		return false, fmt.Errorf("list keys error: %w", err)
+	}
+
+	shouldCreateKey := true
+	var existingKey *gitlab.DeployKey
+	for _, k := range keys {
+		if k.Title == keyName {
+			if k.Key != key {
+				existingKey = k
+			} else {
+				shouldCreateKey = false
+			}
+			break
+		}
+	}
+
+	// delete existing key if the value differs
+	if existingKey != nil {
+		_, err := gl.DeployKeys.DeleteDeployKey(projId, existingKey.ID, gitlab.WithContext(ctx))
+		if err != nil {
+			return false, fmt.Errorf("delete key error: %w", err)
+		}
+	}
+
+	// create key
+	if shouldCreateKey {
+		_, _, err := gl.DeployKeys.AddDeployKey(projId, &gitlab.AddDeployKeyOptions{
+			Title:   gitlab.String(keyName),
+			Key:     gitlab.String(key),
+			CanPush: gitlab.Bool(false),
+		}, gitlab.WithContext(ctx))
+		if err != nil {
+			return false, fmt.Errorf("add key error: %w", err)
+		}
+		return true, nil
+	}
+
+	return false, nil
+}
diff --git a/pkg/git/repository.go b/pkg/git/repository.go
new file mode 100644
index 0000000000000000000000000000000000000000..ca555859cfa60642494c8aacba8bc2881d8fd3d1
--- /dev/null
+++ b/pkg/git/repository.go
@@ -0,0 +1,151 @@
+package git
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/go-git/go-git/v5"
+	"github.com/go-git/go-git/v5/plumbing"
+	"github.com/go-git/go-git/v5/plumbing/object"
+	"github.com/go-git/go-git/v5/plumbing/transport"
+	"github.com/go-git/go-git/v5/plumbing/transport/http"
+)
+
+// Repository represents a git repository wrapper
+type Repository struct {
+	Name        string
+	Owner       string
+	Host        string
+	Token       string
+	AuthorName  string
+	AuthorEmail string
+
+	repo *git.Repository
+}
+
+// NewRepository returns a git repository wrapper
+func NewRepository(name, owner, host, token, authorName, authorEmail string) (*Repository, error) {
+	if name == "" {
+		return nil, fmt.Errorf("name required")
+	}
+	if owner == "" {
+		return nil, fmt.Errorf("owner required")
+	}
+	if host == "" {
+		return nil, fmt.Errorf("host required")
+	}
+	if token == "" {
+		return nil, fmt.Errorf("token required")
+	}
+	if authorName == "" {
+		return nil, fmt.Errorf("author name required")
+	}
+	if authorEmail == "" {
+		return nil, fmt.Errorf("author email required")
+	}
+
+	return &Repository{
+		Name:        name,
+		Owner:       owner,
+		Host:        host,
+		Token:       token,
+		AuthorName:  authorName,
+		AuthorEmail: authorEmail,
+	}, nil
+}
+
+// GetURL returns the repository HTTPS address
+func (r *Repository) GetURL() string {
+	return fmt.Sprintf("https://%s/%s/%s", r.Host, r.Owner, r.Name)
+}
+
+// GetSSH returns the repository SSH address
+func (r *Repository) GetSSH() string {
+	return fmt.Sprintf("ssh://git@%s/%s/%s", r.Host, r.Owner, r.Name)
+}
+
+func (r *Repository) auth() transport.AuthMethod {
+	return &http.BasicAuth{
+		Username: "git",
+		Password: r.Token,
+	}
+}
+
+// Checkout repository branch at specified path
+func (r *Repository) Checkout(ctx context.Context, branch, path string) error {
+	repo, err := git.PlainCloneContext(ctx, path, false, &git.CloneOptions{
+		URL:           r.GetURL(),
+		Auth:          r.auth(),
+		RemoteName:    git.DefaultRemoteName,
+		ReferenceName: plumbing.NewBranchReferenceName(branch),
+		SingleBranch:  true,
+		NoCheckout:    false,
+		Progress:      nil,
+		Tags:          git.NoTags,
+	})
+	if err != nil {
+		return fmt.Errorf("git clone error: %w", err)
+	}
+
+	_, err = repo.Head()
+	if err != nil {
+		return fmt.Errorf("git resolve HEAD error: %w", err)
+	}
+
+	r.repo = repo
+	return nil
+}
+
+// Commit changes for the specified path, returns false if no changes are detected
+func (r *Repository) Commit(ctx context.Context, path, message string) (bool, error) {
+	if r.repo == nil {
+		return false, fmt.Errorf("repository hasn't been cloned")
+	}
+
+	w, err := r.repo.Worktree()
+	if err != nil {
+		return false, err
+	}
+
+	_, err = w.Add(path)
+	if err != nil {
+		return false, err
+	}
+
+	status, err := w.Status()
+	if err != nil {
+		return false, err
+	}
+
+	if !status.IsClean() {
+		if _, err := w.Commit(message, &git.CommitOptions{
+			Author: &object.Signature{
+				Name:  r.AuthorName,
+				Email: r.AuthorEmail,
+				When:  time.Now(),
+			},
+		}); err != nil {
+			return false, err
+		}
+		return true, nil
+	}
+
+	return false, nil
+}
+
+// Push commits to origin
+func (r *Repository) Push(ctx context.Context) error {
+	if r.repo == nil {
+		return fmt.Errorf("repository hasn't been cloned")
+	}
+
+	err := r.repo.PushContext(ctx, &git.PushOptions{
+		Auth:     r.auth(),
+		Progress: nil,
+	})
+	if err != nil {
+		return fmt.Errorf("git push error: %w", err)
+	}
+	return nil
+}