GitOps Commit Bot

Clones a private GitHub repo, creates a feature branch, writes a Kubernetes deployment manifest, commits it, and pushes. This is the foundational pattern for GitOps config automation where infrastructure changes are driven by Git commits.

Input: GITHUB_TOKEN env var, repo URL and service name as CLI arguments. Output: A new branch pushed to the remote with an automated config commit.

Setup

mkdir gitops-commit-bot
cd gitops-commit-bot
go mod init github.com/yourorg/gitops-commit-bot
go get github.com/go-git/go-git/v5@latest

main.go

package main

import (
	"fmt"
	"log"
	"os"
	"time"

	git "github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/object"
	githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
)

func main() {
	if len(os.Args) != 3 {
		fmt.Fprintln(os.Stderr, "usage: gitops-commit-bot <repo-url> <service-name>")
		fmt.Fprintln(os.Stderr, "example: gitops-commit-bot https://github.com/org/infra-config.git api")
		fmt.Fprintln(os.Stderr, "\nrequires: GITHUB_TOKEN env var")
		os.Exit(1)
	}

	repoURL := os.Args[1]
	service := os.Args[2]

	token := os.Getenv("GITHUB_TOKEN")
	if token == "" {
		log.Fatal("GITHUB_TOKEN env var is required")
	}

	auth := &githttp.BasicAuth{
		Username: "x-access-token", // GitHub convention — username is ignored, token is the password
		Password: token,
	}

	// Clone into a temp directory — cleaned up on exit
	dir, err := os.MkdirTemp("", "gitops-*")
	if err != nil {
		log.Fatalf("create temp dir: %v", err)
	}
	defer os.RemoveAll(dir)

	fmt.Printf("cloning %s...\n", repoURL)
	repo, err := git.PlainClone(dir, false, &git.CloneOptions{
		URL:      repoURL,
		Auth:     auth,
		Depth:    1, // shallow clone — we don't need history
		Progress: os.Stdout,
	})
	if err != nil {
		log.Fatalf("clone: %v", err)
	}

	// Create and checkout a new branch
	wt, err := repo.Worktree()
	if err != nil {
		log.Fatalf("worktree: %v", err)
	}

	branchName := fmt.Sprintf("auto/update-%s-%d", service, time.Now().Unix())
	if err := wt.Checkout(&git.CheckoutOptions{
		Branch: plumbing.NewBranchReferenceName(branchName),
		Create: true,
	}); err != nil {
		log.Fatalf("checkout branch: %v", err)
	}
	fmt.Printf("created branch: %s\n", branchName)

	// Write the generated Kubernetes manifest
	configDir := dir + "/k8s"
	if err := os.MkdirAll(configDir, 0o755); err != nil {
		log.Fatalf("mkdir k8s: %v", err)
	}

	configContent := fmt.Sprintf(`apiVersion: apps/v1
kind: Deployment
metadata:
  name: %s
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: %s
          image: myregistry/%s:v2.1.0
`, service, service, service)

	configFile := "k8s/" + service + ".yaml"
	if err := os.WriteFile(dir+"/"+configFile, []byte(configContent), 0o644); err != nil {
		log.Fatalf("write config: %v", err)
	}

	// Stage only the specific file — never wt.Add(".")
	if _, err := wt.Add(configFile); err != nil {
		log.Fatalf("stage file: %v", err)
	}

	// Verify there are actually changes to commit
	status, err := wt.Status()
	if err != nil {
		log.Fatalf("status: %v", err)
	}
	if status.IsClean() {
		fmt.Println("nothing to commit — file is already up to date")
		return
	}

	// Commit
	hash, err := wt.Commit(
		fmt.Sprintf("chore: update %s deployment config", service),
		&git.CommitOptions{
			Author: &object.Signature{
				Name:  "GitOps Bot",
				Email: "[email protected]",
				When:  time.Now(),
			},
		},
	)
	if err != nil {
		log.Fatalf("commit: %v", err)
	}
	fmt.Printf("committed: %s\n", hash.String()[:8])

	// Push the branch to remote
	if err := repo.Push(&git.PushOptions{Auth: auth}); err != nil {
		log.Fatalf("push: %v", err)
	}

	fmt.Printf("\ndone — branch %q pushed to %s\n", branchName, repoURL)
	fmt.Println("open a pull request to merge the config change.")
}

Running It

export GITHUB_TOKEN=ghp_your_token_here

go run main.go https://github.com/yourorg/infra-config.git api

Expected output:

cloning https://github.com/yourorg/infra-config.git...
Enumerating objects: 5, done.
created branch: auto/update-api-1754900000
committed: a3f2c1d9
done — branch "auto/update-api-1754900000" pushed to https://github.com/yourorg/infra-config.git
open a pull request to merge the config change.

After running, go to your repo on GitHub and you'll see the new branch with the commit. Open a pull request from there to merge the config change.

Notes

  • The token needs repo scope on GitHub to clone private repos and push branches
  • Depth: 1 clones only the latest commit — faster for automation that doesn't need history
  • wt.Add(configFile) stages only the specific file. Never use wt.Add(".") — it stages everything in the working tree including files you didn't intend to commit
  • The branch name includes a Unix timestamp so each run creates a unique branch

💻 Run locally

Copy the code above and run it on your machine

© 2026 ByteLearn.dev. Free courses for developers. · Privacy