From 1c6cb623a0c21df99430b7075a31f38d8640ffb0 Mon Sep 17 00:00:00 2001 From: Shray Kumar Date: Thu, 25 Sep 2025 12:11:03 -0400 Subject: [PATCH] setup deletion functionality for github app api --- gitops/git/git.go | 51 +++++++++ gitops/git/github_app/github_app.go | 171 ++++++++++++++++++++++++++-- gitops/prer/create_gitops_prs.go | 29 ++++- 3 files changed, 236 insertions(+), 15 deletions(-) diff --git a/gitops/git/git.go b/gitops/git/git.go index aceb9967..8ff2550e 100644 --- a/gitops/git/git.go +++ b/gitops/git/git.go @@ -163,6 +163,57 @@ func (r *Repo) Push(branches []string) { exec.Mustex(r.Dir, "git", args...) } +type GitFileChange struct { + Path string + Status string // "A"dded, "M"odified, "D"eleted, etc. +} + +func (r *Repo) GetDetailedChanges() ([]GitFileChange, error) { + cmd := oe.Command("git", "status", "--porcelain") + cmd.Dir = r.Dir + output, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("failed to get git status: %w", err) + } + + if len(output) == 0 { + return []GitFileChange{}, nil + } + + // Split output into lines and process each line + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + changes := make([]GitFileChange, 0, len(lines)) + + for _, line := range lines { + if len(line) > 3 { + // git status --porcelain output format is "XY filename" + // where X is staged status, Y is unstaged status + statusCode := line[:2] + filename := strings.TrimSpace(line[2:]) + + // Determine the primary status + var status string + if strings.Contains(statusCode, "D") { + status = "D" // Deleted + } else if strings.Contains(statusCode, "A") { + status = "A" // Added + } else if strings.Contains(statusCode, "M") { + status = "M" // Modified + } else { + status = "M" // Default to modified for other cases + } + + changes = append(changes, GitFileChange{ + Path: filename, + Status: status, + }) + log.Printf("Git change: %s %s", status, filename) + } + } + + return changes, nil +} + func (r *Repo) GetModifiedFiles() ([]string, error) { cmd := oe.Command("git", "status", "--porcelain") cmd.Dir = r.Dir diff --git a/gitops/git/github_app/github_app.go b/gitops/git/github_app/github_app.go index fdb59bef..4046ba96 100644 --- a/gitops/git/github_app/github_app.go +++ b/gitops/git/github_app/github_app.go @@ -97,7 +97,7 @@ func CreatePR(from, to, title, body string) error { return err } -func CreateCommit(baseBranch string, commitBranch string, gitopsPath string, files []string, prTitle string, prDescription string) { +func CreateCommit(baseBranch string, commitBranch string, gitopsPath string, files []string, deletedFiles []string, prTitle string, prDescription string, branchesNeedingRecreation []string) { ctx := context.Background() gh := createGithubClient() @@ -105,20 +105,54 @@ func CreateCommit(baseBranch string, commitBranch string, gitopsPath string, fil log.Printf("Starting Create Commit: Base branch: %s\n", baseBranch) log.Printf("GitOps Path: %s\n", gitopsPath) log.Printf("Modified Files: %v\n", files) - fileEntries, err := getFilesToCommit(gitopsPath, files) - - if err != nil { - log.Fatalf("failed to get files to commit: %v", err) + log.Printf("Deleted Files: %v\n", deletedFiles) + log.Printf("Branches needing recreation: %v\n", branchesNeedingRecreation) + + // Check if this branch needs recreation due to deletions + needsRecreation := false + branchName := fmt.Sprintf("deploy/%s", commitBranch) + for _, recreateBranch := range branchesNeedingRecreation { + if recreateBranch == branchName { + needsRecreation = true + break + } } - ref := getRef(ctx, gh, baseBranch, commitBranch) - tree, err := getTree(ctx, gh, ref, fileEntries) - if err != nil { - log.Fatalf("failed to create tree: %v", err) + var ref *github.Reference + var err error + + if needsRecreation { + log.Printf("Branch %s needs recreation due to target deletions, force-resetting from %s\n", branchName, baseBranch) + ref, err = forceResetBranch(ctx, gh, baseBranch, branchName) + if err != nil { + log.Fatalf("failed to force reset branch: %v", err) + } + + // When recreating, we need to include all current files in the GitOps directory + allFileEntries, err := getAllFilesToCommit(gitopsPath) + if err != nil { + log.Fatalf("failed to get all files to commit: %v", err) + } + + tree, err := getTree(ctx, gh, ref, allFileEntries) + if err != nil { + log.Fatalf("failed to create tree: %v", err) + } + + pushCommit(ctx, gh, ref, tree, prTitle) + } else { + // Handle incremental changes (adds/modifies/deletes) + ref = getRef(ctx, gh, baseBranch, branchName) + + tree, err := getTreeWithChanges(ctx, gh, ref, gitopsPath, files, deletedFiles) + if err != nil { + log.Fatalf("failed to create tree with changes: %v", err) + } + + pushCommit(ctx, gh, ref, tree, prTitle) } - pushCommit(ctx, gh, ref, tree, prTitle) - createPR(ctx, gh, baseBranch, commitBranch, prTitle, prDescription) + createPR(ctx, gh, baseBranch, branchName, prTitle, prDescription) } func getFilesToCommit(gitopsPath string, inputPaths []string) ([]FileEntry, error) { @@ -227,6 +261,50 @@ func getRef(ctx context.Context, gh *github.Client, baseBranch string, commitBra return ref } +func getTreeWithChanges(ctx context.Context, gh *github.Client, ref *github.Reference, gitopsPath string, addedModifiedFiles []string, deletedFiles []string) (tree *github.Tree, err error) { + entries := []*github.TreeEntry{} + + // Add/modify files + if len(addedModifiedFiles) > 0 { + fileEntries, err := getFilesToCommit(gitopsPath, addedModifiedFiles) + if err != nil { + return nil, fmt.Errorf("failed to get files to commit: %v", err) + } + + for _, file := range fileEntries { + content, err := os.ReadFile(file.FullPath) + if err != nil { + return nil, fmt.Errorf("failed to read file %s: %v", file.FullPath, err) + } + log.Printf("Adding/modifying file %s to tree\n", file.RelativePath) + entries = append(entries, &github.TreeEntry{ + Path: github.Ptr(file.RelativePath), + Type: github.Ptr("blob"), + Content: github.Ptr(string(content)), + Mode: github.Ptr("100644"), + }) + } + } + + // Delete files by setting SHA to nil + for _, deletedFile := range deletedFiles { + log.Printf("Deleting file %s from tree\n", deletedFile) + entries = append(entries, &github.TreeEntry{ + Path: github.Ptr(deletedFile), + Mode: github.Ptr("100644"), + Type: github.Ptr("blob"), + SHA: nil, // Setting SHA to nil deletes the file + }) + } + + if len(entries) == 0 { + return nil, fmt.Errorf("no changes to commit") + } + + tree, _, err = gh.Git.CreateTree(ctx, *repoOwner, *repo, *ref.Object.SHA, entries) + return tree, err +} + func getTree(ctx context.Context, gh *github.Client, ref *github.Reference, files []FileEntry) (tree *github.Tree, err error) { // Create a tree with what to commit. entries := []*github.TreeEntry{} @@ -250,6 +328,77 @@ func getTree(ctx context.Context, gh *github.Client, ref *github.Reference, file return tree, err } +func forceResetBranch(ctx context.Context, gh *github.Client, baseBranch string, targetBranch string) (*github.Reference, error) { + // Get the base branch reference + baseRef, _, err := gh.Git.GetRef(ctx, *repoOwner, *repo, "refs/heads/"+baseBranch) + if err != nil { + return nil, fmt.Errorf("failed to get base branch ref: %v", err) + } + + // Try to get the existing target branch + targetRef, _, err := gh.Git.GetRef(ctx, *repoOwner, *repo, "refs/heads/"+targetBranch) + if err != nil { + // Branch doesn't exist, create it + log.Printf("Target branch %s doesn't exist, creating it\n", targetBranch) + newRef := &github.Reference{ + Ref: github.String("refs/heads/" + targetBranch), + Object: &github.GitObject{SHA: baseRef.Object.SHA}, + } + createdRef, _, err := gh.Git.CreateRef(ctx, *repoOwner, *repo, newRef) + if err != nil { + return nil, fmt.Errorf("failed to create branch ref: %v", err) + } + return createdRef, nil + } + + // Branch exists, force update it to point to base branch + log.Printf("Force updating branch %s to match %s\n", targetBranch, baseBranch) + targetRef.Object.SHA = baseRef.Object.SHA + updatedRef, _, err := gh.Git.UpdateRef(ctx, *repoOwner, *repo, targetRef, true) // force=true + if err != nil { + return nil, fmt.Errorf("failed to force update branch ref: %v", err) + } + + return updatedRef, nil +} + +func getAllFilesToCommit(gitopsPath string) ([]FileEntry, error) { + var allFileEntries []FileEntry + + // Walk through the entire GitOps directory and get all files + err := filepath.Walk(gitopsPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + // Get path relative to gitopsPath + relPath, err := filepath.Rel(gitopsPath, path) + if err != nil { + return fmt.Errorf("failed to get relative path for %s: %v", path, err) + } + // Skip hidden files and git files + if !strings.HasPrefix(filepath.Base(relPath), ".") { + allFileEntries = append(allFileEntries, FileEntry{ + RelativePath: relPath, + FullPath: path, + }) + } + } + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to walk GitOps directory: %v", err) + } + + if len(allFileEntries) == 0 { + return nil, fmt.Errorf("no files found in GitOps directory %s", gitopsPath) + } + + log.Printf("Found %d files in GitOps directory\n", len(allFileEntries)) + return allFileEntries, nil +} + func pushCommit(ctx context.Context, gh *github.Client, ref *github.Reference, tree *github.Tree, commitMessage string) { // Get the parent commit to attach the commit to. parent, _, err := gh.Repositories.GetCommit(ctx, *repoOwner, *repo, *ref.Object.SHA, nil) diff --git a/gitops/prer/create_gitops_prs.go b/gitops/prer/create_gitops_prs.go index aba4f255..a336ed63 100644 --- a/gitops/prer/create_gitops_prs.go +++ b/gitops/prer/create_gitops_prs.go @@ -309,10 +309,13 @@ func main() { var updatedTargets []string var updatedBranches []string var modifiedFiles []string + var deletedFiles []string + var branchesNeedingRecreation []string // Process each release train for train, targets := range trains { branch := fmt.Sprintf("deploy/%s%s", train, cfg.DeploymentBranchSuffix) + needsRecreation := false if !workdir.SwitchToBranch(branch, cfg.PRTargetBranch) { // Check if branch needs recreation due to deleted targets @@ -325,11 +328,16 @@ func main() { for _, t := range commitmsg.ExtractTargets(msg) { if !currentTargets[t] { workdir.RecreateBranch(branch, cfg.PRTargetBranch) + needsRecreation = true break } } } + if needsRecreation { + branchesNeedingRecreation = append(branchesNeedingRecreation, branch) + } + // Process targets for _, target := range targets { bin := bazel.TargetToExecutable(target) @@ -339,14 +347,27 @@ func main() { commitMsg := fmt.Sprintf("GitOps for release branch %s from %s commit %s\n%s", cfg.ReleaseBranch, cfg.BranchName, cfg.GitCommit, commitmsg.Generate(targets)) - files, err := workdir.GetModifiedFiles() + changes, err := workdir.GetDetailedChanges() if err != nil { - log.Fatalf("failed to get modified files: %v", err) + log.Fatalf("failed to get detailed changes: %v", err) + } + + // Separate the changes by type + var addedModifiedFiles []string + var branchDeletedFiles []string + for _, change := range changes { + if change.Status == "D" { + branchDeletedFiles = append(branchDeletedFiles, change.Path) + } else { + addedModifiedFiles = append(addedModifiedFiles, change.Path) + } } - modifiedFiles = append(modifiedFiles, files...) + modifiedFiles = append(modifiedFiles, addedModifiedFiles...) + deletedFiles = append(deletedFiles, branchDeletedFiles...) log.Printf("Modified files: %v", modifiedFiles) + log.Printf("Deleted files: %v", deletedFiles) if workdir.Commit(commitMsg, cfg.GitOpsPath) { log.Printf("Branch %s has changes, push required", branch) updatedTargets = append(updatedTargets, targets...) @@ -381,7 +402,7 @@ func main() { switch cfg.GitHost { case "github_app": - github_app.CreateCommit(cfg.PRTargetBranch, cfg.BranchName, gitopsDir, modifiedFiles, prTitle, prDescription) + github_app.CreateCommit(cfg.PRTargetBranch, cfg.BranchName, gitopsDir, modifiedFiles, deletedFiles, prTitle, prDescription, branchesNeedingRecreation) return default: workdir.Push(updatedBranches)