Skip to content
This repository has been archived by the owner on Jan 21, 2025. It is now read-only.

Commit

Permalink
Add auto-merge (#156)
Browse files Browse the repository at this point in the history
* Add AutoMerge to config
* Update docs
  • Loading branch information
Ben10k authored Apr 3, 2024
1 parent 4165e4d commit 137655d
Show file tree
Hide file tree
Showing 15 changed files with 216 additions and 130 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ Allows separating promotions into a separate PRs per environment/failure domain

e.g. "Sync all dev clusters in one PR but open a dedicated PR for every production cluster"

Also allows automatic merging of PRs based on the promotion policy.

e.g. "Automatically merge PRs that promote to multiple `lab` environments"

### Optional per-component allow/block override list

Allows overriding the general(per-repo) promotion policy on a per component level.
Expand Down
15 changes: 11 additions & 4 deletions cmd/telefonistka/bump-version-overwrite.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"strings"

lru "github.com/hashicorp/golang-lru/v2"
"github.com/hexops/gotextdiff"
"github.com/hexops/gotextdiff/myers"
"github.com/hexops/gotextdiff/span"
Expand All @@ -22,13 +23,14 @@ func init() { //nolint:gochecknoinits
var triggeringRepo string
var triggeringRepoSHA string
var triggeringActor string
var autoMerge bool
eventCmd := &cobra.Command{
Use: "bump-overwrite",
Short: "Bump artifact version based on provided file content.",
Long: "Bump artifact version based on provided file content.\nThis open a pull request in the target repo.",
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
bumpVersionOverwrite(targetRepo, targetFile, file, githubHost, triggeringRepo, triggeringRepoSHA, triggeringActor)
bumpVersionOverwrite(targetRepo, targetFile, file, githubHost, triggeringRepo, triggeringRepoSHA, triggeringActor, autoMerge)
},
}
eventCmd.Flags().StringVarP(&targetRepo, "target-repo", "t", getEnv("TARGET_REPO", ""), "Target Git repository slug(e.g. org-name/repo-name), defaults to TARGET_REPO env var.")
Expand All @@ -38,10 +40,11 @@ func init() { //nolint:gochecknoinits
eventCmd.Flags().StringVarP(&triggeringRepo, "triggering-repo", "p", getEnv("GITHUB_REPOSITORY", ""), "Github repo triggering the version bump(e.g. `octocat/Hello-World`) defaults to GITHUB_REPOSITORY env var.")
eventCmd.Flags().StringVarP(&triggeringRepoSHA, "triggering-repo-sha", "s", getEnv("GITHUB_SHA", ""), "Git SHA of triggering repo, defaults to GITHUB_SHA env var.")
eventCmd.Flags().StringVarP(&triggeringActor, "triggering-actor", "a", getEnv("GITHUB_ACTOR", ""), "GitHub user of the person/bot who triggered the bump, defaults to GITHUB_ACTOR env var.")
eventCmd.Flags().BoolVar(&autoMerge, "auto-merge", false, "Automatically merges the created PR, defaults to false.")
rootCmd.AddCommand(eventCmd)
}

func bumpVersionOverwrite(targetRepo string, targetFile string, file string, githubHost string, triggeringRepo string, triggeringRepoSHA string, triggeringActor string) {
func bumpVersionOverwrite(targetRepo string, targetFile string, file string, githubHost string, triggeringRepo string, triggeringRepoSHA string, triggeringActor string, autoMerge bool) {
b, err := os.ReadFile(file)
if err != nil {
log.Errorf("Failed to read file %s, %v", file, err)
Expand All @@ -56,10 +59,14 @@ func bumpVersionOverwrite(targetRepo string, targetFile string, file string, git
githubRestAltURL = "https://" + githubHost + "/api/v3"
log.Infof("Github REST API endpoint is configured to %s", githubRestAltURL)
}
var mainGithubClientPair githubapi.GhClientPair
mainGhClientCache, _ := lru.New[string, githubapi.GhClientPair](128)

mainGithubClientPair.GetAndCache(mainGhClientCache, "GITHUB_APP_ID", "GITHUB_APP_PRIVATE_KEY_PATH", "GITHUB_OAUTH_TOKEN", strings.Split(targetRepo, "/")[0], ctx)

var ghPrClientDetails githubapi.GhPrClientDetails

ghPrClientDetails.Ghclient = githubapi.CreateGithubRestClient(getCrucialEnv("GITHUB_OAUTH_TOKEN"), githubRestAltURL, ctx)
ghPrClientDetails.GhClientPair = &mainGithubClientPair
ghPrClientDetails.Ctx = ctx
ghPrClientDetails.Owner = strings.Split(targetRepo, "/")[0]
ghPrClientDetails.Repo = strings.Split(targetRepo, "/")[1]
Expand All @@ -77,7 +84,7 @@ func bumpVersionOverwrite(targetRepo string, targetFile string, file string, git
edits := myers.ComputeEdits(span.URIFromPath(""), initialFileContent, newFileContent)
ghPrClientDetails.PrLogger.Infof("Diff:\n%s", gotextdiff.ToUnified("Before", "After", initialFileContent, edits))

err = githubapi.BumpVersion(ghPrClientDetails, "main", targetFile, newFileContent, triggeringRepo, triggeringRepoSHA, triggeringActor)
err = githubapi.BumpVersion(ghPrClientDetails, "main", targetFile, newFileContent, triggeringRepo, triggeringRepoSHA, triggeringActor, autoMerge)
if err != nil {
log.Errorf("Failed to bump version: %v", err)
os.Exit(1)
Expand Down
17 changes: 12 additions & 5 deletions cmd/telefonistka/bump-version-regex.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"regexp"
"strings"

lru "github.com/hashicorp/golang-lru/v2"
"github.com/hexops/gotextdiff"
"github.com/hexops/gotextdiff/myers"
"github.com/hexops/gotextdiff/span"
Expand All @@ -24,13 +25,14 @@ func init() { //nolint:gochecknoinits
var triggeringRepo string
var triggeringRepoSHA string
var triggeringActor string
var autoMerge bool
eventCmd := &cobra.Command{
Use: "bump-regex",
Short: "Bump artifact version in a file using regex",
Long: "Bump artifact version in a file using regex.\nThis open a pull request in the target repo.",
Long: "Bump artifact version in a file using regex.\nThis open a pull request in the target repo.\n",
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
bumpVersionRegex(targetRepo, targetFile, regex, replacement, githubHost, triggeringRepo, triggeringRepoSHA, triggeringActor)
bumpVersionRegex(targetRepo, targetFile, regex, replacement, githubHost, triggeringRepo, triggeringRepoSHA, triggeringActor, autoMerge)
},
}
eventCmd.Flags().StringVarP(&targetRepo, "target-repo", "t", getEnv("TARGET_REPO", ""), "Target Git repository slug(e.g. org-name/repo-name), defaults to TARGET_REPO env var.")
Expand All @@ -41,21 +43,26 @@ func init() { //nolint:gochecknoinits
eventCmd.Flags().StringVarP(&triggeringRepo, "triggering-repo", "p", getEnv("GITHUB_REPOSITORY", ""), "Github repo triggering the version bump(e.g. `octocat/Hello-World`) defaults to GITHUB_REPOSITORY env var.")
eventCmd.Flags().StringVarP(&triggeringRepoSHA, "triggering-repo-sha", "s", getEnv("GITHUB_SHA", ""), "Git SHA of triggering repo, defaults to GITHUB_SHA env var.")
eventCmd.Flags().StringVarP(&triggeringActor, "triggering-actor", "a", getEnv("GITHUB_ACTOR", ""), "GitHub user of the person/bot who triggered the bump, defaults to GITHUB_ACTOR env var.")
eventCmd.Flags().BoolVar(&autoMerge, "auto-merge", false, "Automatically merges the created PR, defaults to false.")
rootCmd.AddCommand(eventCmd)
}

func bumpVersionRegex(targetRepo string, targetFile string, regex string, replacement string, githubHost string, triggeringRepo string, triggeringRepoSHA string, triggeringActor string) {
func bumpVersionRegex(targetRepo string, targetFile string, regex string, replacement string, githubHost string, triggeringRepo string, triggeringRepoSHA string, triggeringActor string, autoMerge bool) {
ctx := context.Background()
var githubRestAltURL string

if githubHost != "" {
githubRestAltURL = "https://" + githubHost + "/api/v3"
log.Infof("Github REST API endpoint is configured to %s", githubRestAltURL)
}
var mainGithubClientPair githubapi.GhClientPair
mainGhClientCache, _ := lru.New[string, githubapi.GhClientPair](128)

mainGithubClientPair.GetAndCache(mainGhClientCache, "GITHUB_APP_ID", "GITHUB_APP_PRIVATE_KEY_PATH", "GITHUB_OAUTH_TOKEN", strings.Split(targetRepo, "/")[0], ctx)

var ghPrClientDetails githubapi.GhPrClientDetails

ghPrClientDetails.Ghclient = githubapi.CreateGithubRestClient(getCrucialEnv("GITHUB_OAUTH_TOKEN"), githubRestAltURL, ctx)
ghPrClientDetails.GhClientPair = &mainGithubClientPair
ghPrClientDetails.Ctx = ctx
ghPrClientDetails.Owner = strings.Split(targetRepo, "/")[0]
ghPrClientDetails.Repo = strings.Split(targetRepo, "/")[1]
Expand All @@ -74,7 +81,7 @@ func bumpVersionRegex(targetRepo string, targetFile string, regex string, replac
edits := myers.ComputeEdits(span.URIFromPath(""), initialFileContent, newFileContent)
ghPrClientDetails.PrLogger.Infof("Diff:\n%s", gotextdiff.ToUnified("Before", "After", initialFileContent, edits))

err = githubapi.BumpVersion(ghPrClientDetails, "main", targetFile, newFileContent, triggeringRepo, triggeringRepoSHA, triggeringActor)
err = githubapi.BumpVersion(ghPrClientDetails, "main", targetFile, newFileContent, triggeringRepo, triggeringRepoSHA, triggeringActor, autoMerge)
if err != nil {
log.Errorf("Failed to bump version: %v", err)
os.Exit(1)
Expand Down
5 changes: 4 additions & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ Configuration keys:
|`promotionPaths`| Array of maps, each map describes a promotion flow|
|`promotionPaths[0].sourcePath`| directory that holds components(subdirectories) to be synced, can include a regex.|
|`promotionPaths[0].conditions` | conditions for triggering a specific promotion flows. Flows are evaluated in order, first one to match is triggered.|
|`promotionPaths[0].conditions.prHasLabels` | Array of PR labels, if the triggering PR has any of these labels the condition is considered fulfilled. Currently it's the only supported condition type|
|`promotionPaths[0].conditions.prHasLabels` | Array of PR labels, if the triggering PR has any of these labels the condition is considered fulfilled.|
|`promotionPaths[0].conditions.autoMerge`| Boolean value. If set to true, PR will be automatically merged after it is created.|
|`promotionPaths[0].promotionPrs`| Array of structures, each element represent a PR that will be opened when files are changed under `sourcePath`. Multiple elements means multiple PR will be opened|
|`promotionPaths[0].promotionPrs[0].targetPaths`| Array of strings, each element represent a directory to by synced from the changed component under `sourcePath`. Multiple elements means multiple directories will be synced in a PR|
|`dryRunMode`| if true, the bot will just comment the planned promotion on the merged PR|
Expand All @@ -102,6 +103,8 @@ Example:
```yaml
promotionPaths:
- sourcePath: "workspace/"
conditions:
autoMerge: true
promotionPrs:
- targetPaths:
- "clusters/dev/us-east4/c2"
Expand Down
6 changes: 4 additions & 2 deletions docs/version_bumping.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
If your IaC repo deploys software you maintain internally you probably want to automate artifact version bumping.
Telefonistka can automate opening the IaC repo PR for the version change from the Code repo pipeline.

Currently two modes of operation are supported:
Currently, two modes of operation are supported:

## Whole file overwrite

Expand All @@ -15,6 +15,7 @@ Usage:
telefonistka bump-overwrite [flags]

Flags:
--auto-merge Automatically merges the created PR, defaults to false.
-c, --file string File that holds the content the target file will be overwritten with, like "version.yaml" or '<(echo -e "image:\n tag: ${VERSION}")'.
-g, --github-host string GitHub instance HOSTNAME, defaults to "github.com". This is used for GitHub Enterprise Server instances.
-h, --help help for bump-overwrite.
Expand All @@ -40,6 +41,7 @@ Usage:
telefonistka bump-regex [flags]

Flags:
--auto-merge Automatically merges the created PR, defaults to false.
-g, --github-host string GitHub instance HOSTNAME, defaults to "github.com". This is used for GitHub Enterprise Server instances.
-h, --help help for bump-regex.
-r, --regex-string string Regex used to replace artifact version, e.g. 'tag:\s*(\S*)',
Expand All @@ -49,7 +51,7 @@ Flags:
-a, --triggering-actor string GitHub user of the person/bot who triggered the bump, defaults to GITHUB_ACTOR env var.
-p, --triggering-repo octocat/Hello-World Github repo triggering the version bump(e.g. octocat/Hello-World) defaults to GITHUB_REPOSITORY env var.
-s, --triggering-repo-sha string Git SHA of triggering repo, defaults to GITHUB_SHA env var.
```
```

notes:

Expand Down
1 change: 1 addition & 0 deletions internal/pkg/configuration/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type ComponentConfig struct {

type Condition struct {
PrHasLabels []string `yaml:"prHasLabels"`
AutoMerge bool `yaml:"autoMerge"`
}

type PromotionPr struct {
Expand Down
7 changes: 7 additions & 0 deletions internal/pkg/configuration/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func TestConfigurationParse(t *testing.T) {
PrHasLabels: []string{
"some-label",
},
AutoMerge: true,
},
PromotionPrs: []PromotionPr{
{
Expand All @@ -45,6 +46,9 @@ func TestConfigurationParse(t *testing.T) {
},
{
SourcePath: "env/staging/us-east4/c1/",
Conditions: Condition{
AutoMerge: false,
},
PromotionPrs: []PromotionPr{
{
TargetPaths: []string{
Expand All @@ -55,6 +59,9 @@ func TestConfigurationParse(t *testing.T) {
},
{
SourcePath: "env/prod/us-central1/c2/",
Conditions: Condition{
AutoMerge: false,
},
PromotionPrs: []PromotionPr{
{
TargetPaths: []string{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@ promotionPaths:
conditions:
prHasLabels:
- "some-label"
autoMerge: true
promotionPrs:
- targetPaths:
- "env/staging/us-east4/c1/"
- targetPaths:
- "env/staging/europe-west4/c1/"
- sourcePath: "env/staging/us-east4/c1/"
conditions:
autoMerge: false
promotionPrs:
- targetPaths:
- "env/prod/us-central1/c2/"
- sourcePath: "env/prod/us-central1/c2/"
conditions:
promotionPrs:
- targetPaths:
- "env/prod/us-west1/c2/"
Expand Down
8 changes: 4 additions & 4 deletions internal/pkg/githubapi/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func createGithubAppRestClient(githubAppPrivateKeyPath string, githubAppId int64
return client
}

func CreateGithubRestClient(githubOauthToken string, githubRestAltURL string, ctx context.Context) *github.Client {
func createGithubRestClient(githubOauthToken string, githubRestAltURL string, ctx context.Context) *github.Client {
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: githubOauthToken},
)
Expand Down Expand Up @@ -182,15 +182,15 @@ func createGhTokenClientPair(ctx context.Context, ghOauthToken string) GhClientP
log.Debugf("Using public Github API endpoint")
}

// ghClientPair.v3Client := CreateGithubRestClient(ghOauthToken, githubRestAltURL, ctx)
// ghClientPair.v3Client := createGithubRestClient(ghOauthToken, githubRestAltURL, ctx)
// ghClientPair.v4Client := createGithubGraphQlClient(ghOauthToken, githubGraphqlAltURL)
return GhClientPair{
v3Client: CreateGithubRestClient(ghOauthToken, githubRestAltURL, ctx),
v3Client: createGithubRestClient(ghOauthToken, githubRestAltURL, ctx),
v4Client: createGithubGraphQlClient(ghOauthToken, githubGraphqlAltURL),
}
}

func (gcp *GhClientPair) getAndCache(ghClientCache *lru.Cache[string, GhClientPair], ghAppIdEnvVarName string, ghAppPKeyPathEnvVarName string, ghOauthTokenEnvVarName string, repoOwner string, ctx context.Context) {
func (gcp *GhClientPair) GetAndCache(ghClientCache *lru.Cache[string, GhClientPair], ghAppIdEnvVarName string, ghAppPKeyPathEnvVarName string, ghOauthTokenEnvVarName string, repoOwner string, ctx context.Context) {
githubAppId := getEnv(ghAppIdEnvVarName, "")
var keyExist bool
if githubAppId != "" {
Expand Down
4 changes: 2 additions & 2 deletions internal/pkg/githubapi/drift_detection.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func generateDiffOutput(ghPrClientDetails GhPrClientDetails, defaultBranch strin

if len(filesWithDiff) != 0 {
diffOutput.WriteString("\n### Blame Links:\n")
githubURL := ghPrClientDetails.Ghclient.BaseURL.String()
githubURL := ghPrClientDetails.GhClientPair.v3Client.BaseURL.String()
blameUrlPrefix := githubURL + ghPrClientDetails.Owner + "/" + ghPrClientDetails.Repo + "/blame"

for _, f := range filesWithDiff {
Expand Down Expand Up @@ -100,7 +100,7 @@ func generateFlatMapfromFileTree(ghPrClientDetails *GhPrClientDetails, workingPa
getContentOpts := &github.RepositoryContentGetOptions{
Ref: *branch,
}
_, directoryContent, resp, _ := ghPrClientDetails.Ghclient.Repositories.GetContents(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, *workingPath, getContentOpts)
_, directoryContent, resp, _ := ghPrClientDetails.GhClientPair.v3Client.Repositories.GetContents(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, *workingPath, getContentOpts)
prom.InstrumentGhCall(resp)
for _, elementInDir := range directoryContent {
if *elementInDir.Type == "file" {
Expand Down
Loading

0 comments on commit 137655d

Please sign in to comment.