diff --git a/.github/workflows/docker-publish-on-comment.yml b/.github/workflows/docker-publish-on-comment.yml index cace4267..4763d08c 100644 --- a/.github/workflows/docker-publish-on-comment.yml +++ b/.github/workflows/docker-publish-on-comment.yml @@ -9,11 +9,6 @@ on: issue_comment: types: [created] -env: - # Use docker.io for Docker Hub if empty - REGISTRY: ghcr.io - # github.repository as / - IMAGE_NAME: ${{ github.repository }} jobs: @@ -51,14 +46,15 @@ jobs: # Login against a Docker registry except on PR # https://github.com/docker/login-action - - name: Log into GH registry ${{ env.REGISTRY }} + - name: Log into GH registry (ghcr.io) uses: docker/login-action@3d58c274f17dffee475a5520cbe67f0a882c4dbb with: - registry: ${{ env.REGISTRY }} + registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Log into Docker Hub registry ${{ env.REGISTRY }} + - name: Log into Docker Hub registry + if: secrets.DOCKERHUB_TOKEN != '' uses: docker/login-action@3d58c274f17dffee475a5520cbe67f0a882c4dbb with: username: ${{ secrets.DOCKERHUB_USERNAME }} @@ -75,7 +71,6 @@ jobs: context: git images: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - wayfaiross/telefonistka tags: | type=ref,event=branch type=ref,event=pr diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index c4d6fc09..cb3e3c72 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -15,12 +15,6 @@ on: pull_request: branches: [ "main" ] -env: - # Use docker.io for Docker Hub if empty - REGISTRY: ghcr.io - # github.repository as / - IMAGE_NAME: ${{ github.repository }} - jobs: build: @@ -53,16 +47,16 @@ jobs: # Login against a Docker registry except on PR # https://github.com/docker/login-action - - name: Log into GH registry ${{ env.REGISTRY }} - if: github.event_name != 'pull_request' + - name: Log into GH registry (ghcr.io) + if: github.event_name != 'pull_request' uses: docker/login-action@3d58c274f17dffee475a5520cbe67f0a882c4dbb with: - registry: ${{ env.REGISTRY }} + registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Log into Docker Hub registry ${{ env.REGISTRY }} - if: github.event_name != 'pull_request' + - name: Log into Docker Hub registry + if: github.event_name != 'pull_request' && secrets.DOCKERHUB_TOKEN != '' uses: docker/login-action@3d58c274f17dffee475a5520cbe67f0a882c4dbb with: username: ${{ secrets.DOCKERHUB_USERNAME }} @@ -78,7 +72,6 @@ jobs: with: images: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - wayfaiross/telefonistka # Build and push Docker image with Buildx (don't push on PR) diff --git a/README.md b/README.md index 2b82bf2d..efc540d6 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/cmd/telefonistka/bump-version-overwrite.go b/cmd/telefonistka/bump-version-overwrite.go index 6dd20b36..fa6b4f59 100644 --- a/cmd/telefonistka/bump-version-overwrite.go +++ b/cmd/telefonistka/bump-version-overwrite.go @@ -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" @@ -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.") @@ -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) @@ -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] @@ -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) diff --git a/cmd/telefonistka/bump-version-regex.go b/cmd/telefonistka/bump-version-regex.go index 017ee46d..0e9fa25e 100644 --- a/cmd/telefonistka/bump-version-regex.go +++ b/cmd/telefonistka/bump-version-regex.go @@ -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" @@ -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.") @@ -41,10 +43,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 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 @@ -52,10 +55,14 @@ func bumpVersionRegex(targetRepo string, targetFile string, regex string, replac 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] @@ -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) diff --git a/docs/installation.md b/docs/installation.md index a9d389cf..e5611baa 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -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| @@ -102,6 +103,8 @@ Example: ```yaml promotionPaths: - sourcePath: "workspace/" + conditions: + autoMerge: true promotionPrs: - targetPaths: - "clusters/dev/us-east4/c2" diff --git a/docs/version_bumping.md b/docs/version_bumping.md index 0f3eeb68..485f8574 100644 --- a/docs/version_bumping.md +++ b/docs/version_bumping.md @@ -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 @@ -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. @@ -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*)', @@ -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: diff --git a/internal/pkg/configuration/config.go b/internal/pkg/configuration/config.go index 6668feed..c9fa2924 100644 --- a/internal/pkg/configuration/config.go +++ b/internal/pkg/configuration/config.go @@ -16,6 +16,7 @@ type ComponentConfig struct { type Condition struct { PrHasLabels []string `yaml:"prHasLabels"` + AutoMerge bool `yaml:"autoMerge"` } type PromotionPr struct { diff --git a/internal/pkg/configuration/config_test.go b/internal/pkg/configuration/config_test.go index 966d771c..5a62a259 100644 --- a/internal/pkg/configuration/config_test.go +++ b/internal/pkg/configuration/config_test.go @@ -29,6 +29,7 @@ func TestConfigurationParse(t *testing.T) { PrHasLabels: []string{ "some-label", }, + AutoMerge: true, }, PromotionPrs: []PromotionPr{ { @@ -45,6 +46,9 @@ func TestConfigurationParse(t *testing.T) { }, { SourcePath: "env/staging/us-east4/c1/", + Conditions: Condition{ + AutoMerge: false, + }, PromotionPrs: []PromotionPr{ { TargetPaths: []string{ @@ -55,6 +59,9 @@ func TestConfigurationParse(t *testing.T) { }, { SourcePath: "env/prod/us-central1/c2/", + Conditions: Condition{ + AutoMerge: false, + }, PromotionPrs: []PromotionPr{ { TargetPaths: []string{ diff --git a/internal/pkg/configuration/tests/testConfigurationParsing.yaml b/internal/pkg/configuration/tests/testConfigurationParsing.yaml index e04b345d..dd68c0b0 100644 --- a/internal/pkg/configuration/tests/testConfigurationParsing.yaml +++ b/internal/pkg/configuration/tests/testConfigurationParsing.yaml @@ -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/" diff --git a/internal/pkg/githubapi/clients.go b/internal/pkg/githubapi/clients.go index e969b8c8..2f46d4fb 100644 --- a/internal/pkg/githubapi/clients.go +++ b/internal/pkg/githubapi/clients.go @@ -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}, ) @@ -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 != "" { diff --git a/internal/pkg/githubapi/drift_detection.go b/internal/pkg/githubapi/drift_detection.go index f856136c..ee3a3dbd 100644 --- a/internal/pkg/githubapi/drift_detection.go +++ b/internal/pkg/githubapi/drift_detection.go @@ -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 { @@ -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" { diff --git a/internal/pkg/githubapi/drift_detection_test.go b/internal/pkg/githubapi/drift_detection_test.go index b2d21d1b..54b84648 100644 --- a/internal/pkg/githubapi/drift_detection_test.go +++ b/internal/pkg/githubapi/drift_detection_test.go @@ -59,15 +59,15 @@ func TestGenerateFlatMapfromFileTree(t *testing.T) { }, ), ) - ghclient := github.NewClient(mockedHTTPClient) + ghClientPair := GhClientPair{v3Client: github.NewClient(mockedHTTPClient)} ghPrClientDetails := GhPrClientDetails{ - Ctx: ctx, - Ghclient: ghclient, - Owner: "AnOwner", - Repo: "Arepo", - PrNumber: 120, - Ref: "Abranch", + Ctx: ctx, + GhClientPair: &ghClientPair, + Owner: "AnOwner", + Repo: "Arepo", + PrNumber: 120, + Ref: "Abranch", PrLogger: log.WithFields(log.Fields{ "repo": "AnOwner/Arepo", "prNumber": 120, @@ -106,15 +106,15 @@ func TestGenerateDiffOutputDiffFileContent(t *testing.T) { ), ) - ghclient := github.NewClient(mockedHTTPClient) + ghClientPair := GhClientPair{v3Client: github.NewClient(mockedHTTPClient)} ghPrClientDetails := GhPrClientDetails{ - Ctx: ctx, - Ghclient: ghclient, - Owner: "AnOwner", - Repo: "Arepo", - PrNumber: 120, - Ref: "Abranch", + Ctx: ctx, + GhClientPair: &ghClientPair, + Owner: "AnOwner", + Repo: "Arepo", + PrNumber: 120, + Ref: "Abranch", PrLogger: log.WithFields(log.Fields{ "repo": "AnOwner/Arepo", "prNumber": 120, @@ -160,15 +160,15 @@ func TestGenerateDiffOutputIdenticalFiles(t *testing.T) { mockedHTTPClient := mock.NewMockedHTTPClient() - ghclient := github.NewClient(mockedHTTPClient) + ghClientPair := GhClientPair{v3Client: github.NewClient(mockedHTTPClient)} ghPrClientDetails := GhPrClientDetails{ - Ctx: ctx, - Ghclient: ghclient, - Owner: "AnOwner", - Repo: "Arepo", - PrNumber: 120, - Ref: "Abranch", + Ctx: ctx, + GhClientPair: &ghClientPair, + Owner: "AnOwner", + Repo: "Arepo", + PrNumber: 120, + Ref: "Abranch", PrLogger: log.WithFields(log.Fields{ "repo": "AnOwner/Arepo", "prNumber": 120, @@ -207,15 +207,15 @@ func TestGenerateDiffOutputMissingSourceFile(t *testing.T) { ), ) - ghclient := github.NewClient(mockedHTTPClient) + ghClientPair := GhClientPair{v3Client: github.NewClient(mockedHTTPClient)} ghPrClientDetails := GhPrClientDetails{ - Ctx: ctx, - Ghclient: ghclient, - Owner: "AnOwner", - Repo: "Arepo", - PrNumber: 120, - Ref: "Abranch", + Ctx: ctx, + GhClientPair: &ghClientPair, + Owner: "AnOwner", + Repo: "Arepo", + PrNumber: 120, + Ref: "Abranch", PrLogger: log.WithFields(log.Fields{ "repo": "AnOwner/Arepo", "prNumber": 120, @@ -259,15 +259,15 @@ func TestGenerateDiffOutputMissingTargetFile(t *testing.T) { ), ) - ghclient := github.NewClient(mockedHTTPClient) + ghClientPair := GhClientPair{v3Client: github.NewClient(mockedHTTPClient)} ghPrClientDetails := GhPrClientDetails{ - Ctx: ctx, - Ghclient: ghclient, - Owner: "AnOwner", - Repo: "Arepo", - PrNumber: 120, - Ref: "Abranch", + Ctx: ctx, + GhClientPair: &ghClientPair, + Owner: "AnOwner", + Repo: "Arepo", + PrNumber: 120, + Ref: "Abranch", PrLogger: log.WithFields(log.Fields{ "repo": "AnOwner/Arepo", "prNumber": 120, diff --git a/internal/pkg/githubapi/github.go b/internal/pkg/githubapi/github.go index 463f9249..4582cec8 100644 --- a/internal/pkg/githubapi/github.go +++ b/internal/pkg/githubapi/github.go @@ -26,7 +26,7 @@ type promotionInstanceMetaData struct { } type GhPrClientDetails struct { - Ghclient *github.Client + GhClientPair *GhClientPair // This whole struct describe the metadata of the PR, so it makes sense to share the context with everything to generate HTTP calls related to that PR, right? Ctx context.Context //nolint:containedctx DefaultBranch string @@ -148,18 +148,18 @@ func HandleEvent(r *http.Request, ctx context.Context, mainGhClientCache *lru.Ca // this is a commit push, do something with it? log.Infoln("is PushEvent") repoOwner := *eventPayload.Repo.Owner.Login - mainGithubClientPair.getAndCache(mainGhClientCache, "GITHUB_APP_ID", "GITHUB_APP_PRIVATE_KEY_PATH", "GITHUB_OAUTH_TOKEN", repoOwner, ctx) + mainGithubClientPair.GetAndCache(mainGhClientCache, "GITHUB_APP_ID", "GITHUB_APP_PRIVATE_KEY_PATH", "GITHUB_OAUTH_TOKEN", repoOwner, ctx) prLogger := log.WithFields(log.Fields{ "event_type": "push", }) ghPrClientDetails := GhPrClientDetails{ - Ctx: ctx, - Ghclient: mainGithubClientPair.v3Client, - Owner: repoOwner, - Repo: *eventPayload.Repo.Name, - PrLogger: prLogger, + Ctx: ctx, + GhClientPair: &mainGithubClientPair, + Owner: repoOwner, + Repo: *eventPayload.Repo.Name, + PrLogger: prLogger, } handlePushEvent(ctx, eventPayload, r, payload, ghPrClientDetails) @@ -173,27 +173,27 @@ func HandleEvent(r *http.Request, ctx context.Context, mainGhClientCache *lru.Ca repoOwner := *eventPayload.Repo.Owner.Login - mainGithubClientPair.getAndCache(mainGhClientCache, "GITHUB_APP_ID", "GITHUB_APP_PRIVATE_KEY_PATH", "GITHUB_OAUTH_TOKEN", repoOwner, ctx) - approverGithubClientPair.getAndCache(prApproverGhClientCache, "APPROVER_GITHUB_APP_ID", "APPROVER_GITHUB_APP_PRIVATE_KEY_PATH", "APPROVER_GITHUB_OAUTH_TOKEN", repoOwner, ctx) + mainGithubClientPair.GetAndCache(mainGhClientCache, "GITHUB_APP_ID", "GITHUB_APP_PRIVATE_KEY_PATH", "GITHUB_OAUTH_TOKEN", repoOwner, ctx) + approverGithubClientPair.GetAndCache(prApproverGhClientCache, "APPROVER_GITHUB_APP_ID", "APPROVER_GITHUB_APP_PRIVATE_KEY_PATH", "APPROVER_GITHUB_OAUTH_TOKEN", repoOwner, ctx) ghPrClientDetails := GhPrClientDetails{ - Ctx: ctx, - Ghclient: mainGithubClientPair.v3Client, - Labels: eventPayload.PullRequest.Labels, - Owner: repoOwner, - Repo: *eventPayload.Repo.Name, - PrNumber: *eventPayload.PullRequest.Number, - Ref: *eventPayload.PullRequest.Head.Ref, - PrAuthor: *eventPayload.PullRequest.User.Login, - PrLogger: prLogger, - PrSHA: *eventPayload.PullRequest.Head.SHA, + Ctx: ctx, + GhClientPair: &mainGithubClientPair, + Labels: eventPayload.PullRequest.Labels, + Owner: repoOwner, + Repo: *eventPayload.Repo.Name, + PrNumber: *eventPayload.PullRequest.Number, + Ref: *eventPayload.PullRequest.Head.Ref, + PrAuthor: *eventPayload.PullRequest.User.Login, + PrLogger: prLogger, + PrSHA: *eventPayload.PullRequest.Head.SHA, } HandlePREvent(eventPayload, ghPrClientDetails, mainGithubClientPair, approverGithubClientPair, ctx) case *github.IssueCommentEvent: repoOwner := *eventPayload.Repo.Owner.Login - mainGithubClientPair.getAndCache(mainGhClientCache, "GITHUB_APP_ID", "GITHUB_APP_PRIVATE_KEY_PATH", "GITHUB_OAUTH_TOKEN", repoOwner, ctx) + mainGithubClientPair.GetAndCache(mainGhClientCache, "GITHUB_APP_ID", "GITHUB_APP_PRIVATE_KEY_PATH", "GITHUB_OAUTH_TOKEN", repoOwner, ctx) botIdentity, _ := GetBotGhIdentity(mainGithubClientPair.v4Client, ctx) log.Infof("Actionable event type %s\n", eventType) @@ -203,13 +203,13 @@ func HandleEvent(r *http.Request, ctx context.Context, mainGhClientCache *lru.Ca }) if *eventPayload.Comment.User.Login != botIdentity { ghPrClientDetails := GhPrClientDetails{ - Ctx: ctx, - Ghclient: mainGithubClientPair.v3Client, - Owner: repoOwner, - Repo: *eventPayload.Repo.Name, - PrNumber: *eventPayload.Issue.Number, - PrAuthor: *eventPayload.Issue.User.Login, - PrLogger: prLogger, + Ctx: ctx, + GhClientPair: &mainGithubClientPair, + Owner: repoOwner, + Repo: *eventPayload.Repo.Name, + PrNumber: *eventPayload.Issue.Number, + PrAuthor: *eventPayload.Issue.User.Login, + PrLogger: prLogger, } _ = handleCommentPrEvent(ghPrClientDetails, eventPayload) } else { @@ -246,23 +246,35 @@ func handleCommentPrEvent(ghPrClientDetails GhPrClientDetails, ce *github.IssueC } func commentPlanInPR(ghPrClientDetails GhPrClientDetails, promotions map[string]PromotionInstance) { + _, templateOutput := executeTemplate(ghPrClientDetails.PrLogger, "dryRunMsg", "dry-run-pr-comment.gotmpl", promotions) + _ = commentPR(ghPrClientDetails, templateOutput) +} + +func executeTemplate(logger *log.Entry, templateName string, templateFile string, data interface{}) (error, string) { var templateOutput bytes.Buffer - dryRunMsgTemplate, err := template.New("dryRunMsg").ParseFiles(getEnv("TEMPLATES_PATH", "templates/") + "dry-run-pr-comment.gotmpl") + messageTemplate, err := template.New(templateName).ParseFiles(getEnv("TEMPLATES_PATH", "templates/") + templateFile) if err != nil { - ghPrClientDetails.PrLogger.Errorf("Failed to parse template: err=%v", err) + logger.Errorf("Failed to parse template: err=%v", err) + return err, "" } - err = dryRunMsgTemplate.ExecuteTemplate(&templateOutput, "dryRunMsg", promotions) + err = messageTemplate.ExecuteTemplate(&templateOutput, templateName, data) if err != nil { - ghPrClientDetails.PrLogger.Errorf("Failed to execute template: err=%v", err) + logger.Errorf("Failed to execute template: err=%v", err) + return err, "" } - // templateOutputString := templateOutput.String() - err = ghPrClientDetails.CommentOnPr(templateOutput.String()) + return nil, templateOutput.String() +} + +func commentPR(ghPrClientDetails GhPrClientDetails, commentBody string) error { + err := ghPrClientDetails.CommentOnPr(commentBody) if err != nil { - ghPrClientDetails.PrLogger.Errorf("Failed to comment plan in PR: err=%v", err) + ghPrClientDetails.PrLogger.Errorf("Failed to comment in PR: err=%v", err) + return err } + return nil } -func BumpVersion(ghPrClientDetails GhPrClientDetails, defaultBranch string, filePath string, newFileContent string, triggeringRepo string, triggeringRepoSHA string, triggeringActor string) error { +func BumpVersion(ghPrClientDetails GhPrClientDetails, defaultBranch string, filePath string, newFileContent string, triggeringRepo string, triggeringRepoSHA string, triggeringActor string, autoMerge bool) error { var treeEntries []*github.TreeEntry generateBumpTreeEntiesForCommit(&treeEntries, ghPrClientDetails, defaultBranch, filePath, newFileContent) @@ -288,6 +300,15 @@ func BumpVersion(ghPrClientDetails GhPrClientDetails, defaultBranch string, file ghPrClientDetails.PrLogger.Infof("New PR URL: %s", *pr.HTMLURL) + if autoMerge { + ghPrClientDetails.PrLogger.Infof("Auto-merging PR %d", *pr.Number) + err := MergePr(ghPrClientDetails, pr.Number) + if err != nil { + ghPrClientDetails.PrLogger.Errorf("PR auto merge failed: err=%v", err) + return err + } + } + return nil } @@ -365,6 +386,26 @@ func handleMergedPrEvent(ghPrClientDetails GhPrClientDetails, prApproverGithubCl return err } } + if promotion.Metadata.AutoMerge { + ghPrClientDetails.PrLogger.Infof("Auto-merging PR %d", *pull.Number) + templateData := map[string]interface{}{ + "prNumber": *pull.Number, + } + err, templateOutput := executeTemplate(ghPrClientDetails.PrLogger, "autoMerge", "auto-merge-comment.gotmpl", templateData) + if err != nil { + return err + } + err = commentPR(ghPrClientDetails, templateOutput) + if err != nil { + return err + } + + err = MergePr(ghPrClientDetails, pull.Number) + if err != nil { + ghPrClientDetails.PrLogger.Errorf("PR auto merge failed: err=%v", err) + return err + } + } } } else { commentPlanInPR(ghPrClientDetails, promotions) @@ -372,6 +413,15 @@ func handleMergedPrEvent(ghPrClientDetails GhPrClientDetails, prApproverGithubCl return nil } +func MergePr(details GhPrClientDetails, number *int) error { + _, resp, err := details.GhClientPair.v3Client.PullRequests.Merge(details.Ctx, details.Owner, details.Repo, *number, "Auto-merge", nil) + prom.InstrumentGhCall(resp) + if err != nil { + details.PrLogger.Errorf("Failed to merge PR: err=%v", err) + } + return err +} + func (pm *prMetadata) DeSerialize(s string) error { decoded, err := base64.StdEncoding.DecodeString(s) if err != nil { @@ -389,7 +439,7 @@ func (p GhPrClientDetails) CommentOnPr(commentBody string) error { commentBody = "\n" + commentBody comment := &github.IssueComment{Body: &commentBody} - _, resp, err := p.Ghclient.Issues.CreateComment(p.Ctx, p.Owner, p.Repo, p.PrNumber, comment) + _, resp, err := p.GhClientPair.v3Client.Issues.CreateComment(p.Ctx, p.Owner, p.Repo, p.PrNumber, comment) prom.InstrumentGhCall(resp) if err != nil { p.PrLogger.Errorf("Could not comment in PR: err=%s\n%v\n", err, resp) @@ -412,7 +462,7 @@ func (p *GhPrClientDetails) ToggleCommitStatus(context string, user string) erro var r error listOpts := &github.ListOptions{} - initialStatuses, resp, err := p.Ghclient.Repositories.ListStatuses(p.Ctx, p.Owner, p.Repo, p.Ref, listOpts) + initialStatuses, resp, err := p.GhClientPair.v3Client.Repositories.ListStatuses(p.Ctx, p.Owner, p.Repo, p.Ref, listOpts) prom.InstrumentGhCall(resp) if err != nil { p.PrLogger.Errorf("Failed to fetch existing statuses for commit %s, err=%s", p.Ref, err) @@ -424,7 +474,7 @@ func (p *GhPrClientDetails) ToggleCommitStatus(context string, user string) erro if *commitStatus.State != "success" { p.PrLogger.Infof("%s Toggled %s(%s) to success", user, context, *commitStatus.State) *commitStatus.State = "success" - _, resp, err := p.Ghclient.Repositories.CreateStatus(p.Ctx, p.Owner, p.Repo, p.PrSHA, commitStatus) + _, resp, err := p.GhClientPair.v3Client.Repositories.CreateStatus(p.Ctx, p.Owner, p.Repo, p.PrSHA, commitStatus) prom.InstrumentGhCall(resp) if err != nil { p.PrLogger.Errorf("Failed to create context %s, err=%s", context, err) @@ -433,7 +483,7 @@ func (p *GhPrClientDetails) ToggleCommitStatus(context string, user string) erro } else { p.PrLogger.Infof("%s Toggled %s(%s) to failure", user, context, *commitStatus.State) *commitStatus.State = "failure" - _, resp, err := p.Ghclient.Repositories.CreateStatus(p.Ctx, p.Owner, p.Repo, p.PrSHA, commitStatus) + _, resp, err := p.GhClientPair.v3Client.Repositories.CreateStatus(p.Ctx, p.Owner, p.Repo, p.PrSHA, commitStatus) prom.InstrumentGhCall(resp) if err != nil { p.PrLogger.Errorf("Failed to create context %s, err=%s", context, err) @@ -462,7 +512,7 @@ func SetCommitStatus(ghPrClientDetails GhPrClientDetails, state string) { AvatarURL: &avatarURL, } ghPrClientDetails.PrLogger.Debugf("Setting commit %s status to %s", ghPrClientDetails.PrSHA, state) - _, resp, err := ghPrClientDetails.Ghclient.Repositories.CreateStatus(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, ghPrClientDetails.PrSHA, commitStatus) + _, resp, err := ghPrClientDetails.GhClientPair.v3Client.Repositories.CreateStatus(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, ghPrClientDetails.PrSHA, commitStatus) prom.InstrumentGhCall(resp) if err != nil { ghPrClientDetails.PrLogger.Errorf("Failed to set commit status: err=%s\n%v", err, resp) @@ -471,7 +521,7 @@ func SetCommitStatus(ghPrClientDetails GhPrClientDetails, state string) { func (p *GhPrClientDetails) GetSHA() (string, error) { if p.PrSHA == "" { - prObject, resp, err := p.Ghclient.PullRequests.Get(p.Ctx, p.Owner, p.Repo, p.PrNumber) + prObject, resp, err := p.GhClientPair.v3Client.PullRequests.Get(p.Ctx, p.Owner, p.Repo, p.PrNumber) prom.InstrumentGhCall(resp) if err != nil { p.PrLogger.Errorf("Could not get pr data: err=%s\n%v\n", err, resp) @@ -486,7 +536,7 @@ func (p *GhPrClientDetails) GetSHA() (string, error) { func (p *GhPrClientDetails) GetRef() (string, error) { if p.Ref == "" { - prObject, resp, err := p.Ghclient.PullRequests.Get(p.Ctx, p.Owner, p.Repo, p.PrNumber) + prObject, resp, err := p.GhClientPair.v3Client.PullRequests.Get(p.Ctx, p.Owner, p.Repo, p.PrNumber) prom.InstrumentGhCall(resp) if err != nil { p.PrLogger.Errorf("Could not get pr data: err=%s\n%v\n", err, resp) @@ -501,7 +551,7 @@ func (p *GhPrClientDetails) GetRef() (string, error) { func (p *GhPrClientDetails) GetDefaultBranch() (string, error) { if p.DefaultBranch == "" { - repo, resp, err := p.Ghclient.Repositories.Get(p.Ctx, p.Owner, p.Repo) + repo, resp, err := p.GhClientPair.v3Client.Repositories.Get(p.Ctx, p.Owner, p.Repo) if err != nil { p.PrLogger.Errorf("Could not get repo default branch: err=%s\n%v\n", err, resp) return "", err @@ -520,7 +570,7 @@ func generateDeletionTreeEntries(ghPrClientDetails *GhPrClientDetails, path *str getContentOpts := &github.RepositoryContentGetOptions{ Ref: *branch, } - _, directoryContent, resp, err := ghPrClientDetails.Ghclient.Repositories.GetContents(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, *path, getContentOpts) + _, directoryContent, resp, err := ghPrClientDetails.GhClientPair.v3Client.Repositories.GetContents(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, *path, getContentOpts) prom.InstrumentGhCall(resp) if resp.StatusCode == 404 { ghPrClientDetails.PrLogger.Infof("Skipping deletion of non-existing %s", *path) @@ -568,7 +618,7 @@ func getDirecotyGitObjectSha(ghPrClientDetails GhPrClientDetails, dirPath string direcotyGitObjectSha := "" // in GH API/go-github, to get directory SHA you need to scan the whole parent Dir 🤷 - _, directoryContent, resp, err := ghPrClientDetails.Ghclient.Repositories.GetContents(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, path.Dir(dirPath), &repoContentGetOptions) + _, directoryContent, resp, err := ghPrClientDetails.GhClientPair.v3Client.Repositories.GetContents(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, path.Dir(dirPath), &repoContentGetOptions) prom.InstrumentGhCall(resp) if err != nil && resp.StatusCode != 404 { ghPrClientDetails.PrLogger.Errorf("Could not fetch source directory SHA err=%s\n%v\n", err, resp) @@ -634,21 +684,21 @@ func createCommit(ghPrClientDetails GhPrClientDetails, treeEntries []*github.Tre // To avoid cloning the repo locally, I'm using GitHub low level GIT Tree API to sync the source folder "over" the target folders // This works by getting the source dir git object SHA, and overwriting(Git.CreateTree) the target directory git object SHA with the source's SHA. - ref, resp, err := ghPrClientDetails.Ghclient.Git.GetRef(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, "heads/"+defaultBranch) + ref, resp, err := ghPrClientDetails.GhClientPair.v3Client.Git.GetRef(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, "heads/"+defaultBranch) prom.InstrumentGhCall(resp) if err != nil { ghPrClientDetails.PrLogger.Errorf("Failed to get main branch ref: err=%s\n", err) return nil, err } baseTreeSHA := ref.Object.SHA - tree, resp, err := ghPrClientDetails.Ghclient.Git.CreateTree(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, *baseTreeSHA, treeEntries) + tree, resp, err := ghPrClientDetails.GhClientPair.v3Client.Git.CreateTree(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, *baseTreeSHA, treeEntries) prom.InstrumentGhCall(resp) if err != nil { ghPrClientDetails.PrLogger.Errorf("Failed to create Git Tree object: err=%s\n%+v", err, resp) ghPrClientDetails.PrLogger.Errorf("These are the treeEntries: %+v", treeEntries) return nil, err } - parentCommit, resp, err := ghPrClientDetails.Ghclient.Git.GetCommit(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, *baseTreeSHA) + parentCommit, resp, err := ghPrClientDetails.GhClientPair.v3Client.Git.GetCommit(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, *baseTreeSHA) prom.InstrumentGhCall(resp) if err != nil { ghPrClientDetails.PrLogger.Errorf("Failed to get parent commit: err=%s\n", err) @@ -661,7 +711,7 @@ func createCommit(ghPrClientDetails GhPrClientDetails, treeEntries []*github.Tre Tree: tree, } - commit, resp, err := ghPrClientDetails.Ghclient.Git.CreateCommit(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, newCommitConfig) + commit, resp, err := ghPrClientDetails.GhClientPair.v3Client.Git.CreateCommit(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, newCommitConfig) prom.InstrumentGhCall(resp) if err != nil { ghPrClientDetails.PrLogger.Errorf("Failed to create Git commit: err=%s\n", err) // TODO comment this error to PR @@ -684,12 +734,13 @@ func createBranch(ghPrClientDetails GhPrClientDetails, commit *github.Commit, ne Object: newRefGitObjct, } - _, resp, err := ghPrClientDetails.Ghclient.Git.CreateRef(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, newRefConfig) + _, resp, err := ghPrClientDetails.GhClientPair.v3Client.Git.CreateRef(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, newRefConfig) prom.InstrumentGhCall(resp) if err != nil { ghPrClientDetails.PrLogger.Errorf("Could not create Git Ref: err=%s\n%v\n", err, resp) return "", err } + ghPrClientDetails.PrLogger.Infof("New branch ref: %s", newBranchRef) return newBranchRef, err } @@ -750,7 +801,7 @@ func createPrObject(ghPrClientDetails GhPrClientDetails, newBranchRef string, ne Head: github.String(newBranchRef), } - pull, resp, err := ghPrClientDetails.Ghclient.PullRequests.Create(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, newPrConfig) + pull, resp, err := ghPrClientDetails.GhClientPair.v3Client.PullRequests.Create(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, newPrConfig) prom.InstrumentGhCall(resp) if err != nil { ghPrClientDetails.PrLogger.Errorf("Could not create GitHub PR: err=%s\n%v\n", err, resp) @@ -759,7 +810,7 @@ func createPrObject(ghPrClientDetails GhPrClientDetails, newBranchRef string, ne ghPrClientDetails.PrLogger.Infof("PR %d opened", *pull.Number) } - prLables, resp, err := ghPrClientDetails.Ghclient.Issues.AddLabelsToIssue(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, *pull.Number, []string{"promotion"}) + prLables, resp, err := ghPrClientDetails.GhClientPair.v3Client.Issues.AddLabelsToIssue(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, *pull.Number, []string{"promotion"}) prom.InstrumentGhCall(resp) if err != nil { ghPrClientDetails.PrLogger.Errorf("Could not label GitHub PR: err=%s\n%v\n", err, resp) @@ -768,11 +819,11 @@ func createPrObject(ghPrClientDetails GhPrClientDetails, newBranchRef string, ne ghPrClientDetails.PrLogger.Debugf("PR %v labeled\n%+v", pull.Number, prLables) } - _, resp, err = ghPrClientDetails.Ghclient.Issues.AddAssignees(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, *pull.Number, []string{assignee}) + _, resp, err = ghPrClientDetails.GhClientPair.v3Client.Issues.AddAssignees(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, *pull.Number, []string{assignee}) prom.InstrumentGhCall(resp) if err != nil { - ghPrClientDetails.PrLogger.Errorf("Could set %s as assignee on PR, err=%s", assignee, err) - return pull, err + ghPrClientDetails.PrLogger.Warnf("Could not set %s as assignee on PR, err=%s", assignee, err) + // return pull, err } else { ghPrClientDetails.PrLogger.Debugf(" %s was set as assignee on PR", assignee) } @@ -823,7 +874,7 @@ func GetInRepoConfig(ghPrClientDetails GhPrClientDetails, defaultBranch string) func GetFileContent(ghPrClientDetails GhPrClientDetails, branch string, filePath string) (string, error, int) { rGetContentOps := github.RepositoryContentGetOptions{Ref: branch} - fileContent, _, resp, err := ghPrClientDetails.Ghclient.Repositories.GetContents(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, filePath, &rGetContentOps) + fileContent, _, resp, err := ghPrClientDetails.GhClientPair.v3Client.Repositories.GetContents(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, filePath, &rGetContentOps) prom.InstrumentGhCall(resp) if err != nil { ghPrClientDetails.PrLogger.Errorf("Fail to get file:%s\n%v\n", err, resp) diff --git a/internal/pkg/githubapi/promotion.go b/internal/pkg/githubapi/promotion.go index e64a0c56..71bd96da 100644 --- a/internal/pkg/githubapi/promotion.go +++ b/internal/pkg/githubapi/promotion.go @@ -1,12 +1,10 @@ package githubapi import ( - "bytes" "fmt" "regexp" "sort" "strings" - "text/template" "github.com/google/go-github/v52/github" log "github.com/sirupsen/logrus" @@ -16,7 +14,7 @@ import ( ) type PromotionInstance struct { - Metadata PromotionInstanceMetaData `deep:"-"` // Unit tests ignire Metadata currently + Metadata PromotionInstanceMetaData `deep:"-"` // Unit tests ignore Metadata currently ComputedSyncPaths map[string]string // key is target, value is source } @@ -25,6 +23,7 @@ type PromotionInstanceMetaData struct { TargetPaths []string PerComponentSkippedTargetPaths map[string][]string // ComponentName is the key, ComponentNames []string + AutoMerge bool } func containMatchingRegex(patterns []string, str string) bool { @@ -73,19 +72,12 @@ func DetectDrift(ghPrClientDetails GhPrClientDetails) error { } } if len(diffOutputMap) != 0 { - var templateOutput bytes.Buffer - driftMsgTemplate, err := template.New("driftMsg").ParseFiles(getEnv("TEMPLATES_PATH", "templates/") + "drift-pr-comment.gotmpl") + err, templateOutput := executeTemplate(ghPrClientDetails.PrLogger, "driftMsg", "drift-pr-comment.gotmpl", diffOutputMap) if err != nil { - ghPrClientDetails.PrLogger.Errorf("Failed to parse template: err=%v", err) - return err - } - err = driftMsgTemplate.ExecuteTemplate(&templateOutput, "driftMsg", diffOutputMap) - if err != nil { - ghPrClientDetails.PrLogger.Errorf("Failed to execute template: err=%v", err) return err } - err = ghPrClientDetails.CommentOnPr(templateOutput.String()) + err = commentPR(ghPrClientDetails, templateOutput) if err != nil { return err } @@ -99,7 +91,7 @@ func DetectDrift(ghPrClientDetails GhPrClientDetails) error { func getComponentConfig(ghPrClientDetails GhPrClientDetails, componentPath string, branch string) (*cfg.ComponentConfig, error) { componentConfig := &cfg.ComponentConfig{} rGetContentOps := &github.RepositoryContentGetOptions{Ref: branch} - componentConfigFileContent, _, resp, err := ghPrClientDetails.Ghclient.Repositories.GetContents(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, componentPath+"/telefonistka.yaml", rGetContentOps) + componentConfigFileContent, _, resp, err := ghPrClientDetails.GhClientPair.v3Client.Repositories.GetContents(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, componentPath+"/telefonistka.yaml", rGetContentOps) prom.InstrumentGhCall(resp) if (err != nil) && (resp.StatusCode != 404) { // The file is optional ghPrClientDetails.PrLogger.Errorf("could not get file list from GH API: err=%s\nresponse=%v", err, resp) @@ -120,7 +112,7 @@ func getComponentConfig(ghPrClientDetails GhPrClientDetails, componentPath strin func GeneratePromotionPlan(ghPrClientDetails GhPrClientDetails, config *cfg.Config, configBranch string) (map[string]PromotionInstance, error) { promotions := make(map[string]PromotionInstance) - prFiles, resp, err := ghPrClientDetails.Ghclient.PullRequests.ListFiles(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, ghPrClientDetails.PrNumber, &github.ListOptions{}) + prFiles, resp, err := ghPrClientDetails.GhClientPair.v3Client.PullRequests.ListFiles(ghPrClientDetails.Ctx, ghPrClientDetails.Owner, ghPrClientDetails.Repo, ghPrClientDetails.PrNumber, &github.ListOptions{}) prom.InstrumentGhCall(resp) if err != nil { ghPrClientDetails.PrLogger.Errorf("could not get file list from GH API: err=%s\nresponse=%v", err, resp) @@ -132,6 +124,7 @@ func GeneratePromotionPlan(ghPrClientDetails GhPrClientDetails, config *cfg.Conf type relevantComponent struct { SourcePath string ComponentName string + AutoMerge bool } relevantComponents := map[relevantComponent]bool{} @@ -148,6 +141,7 @@ func GeneratePromotionPlan(ghPrClientDetails GhPrClientDetails, config *cfg.Conf relevantComponentsElement := relevantComponent{ SourcePath: compiledSourcePath, ComponentName: componentName, + AutoMerge: promotionPathConfig.Conditions.AutoMerge, } relevantComponents[relevantComponentsElement] = true break // a file can only be a single "source dir" @@ -190,6 +184,7 @@ func GeneratePromotionPlan(ghPrClientDetails GhPrClientDetails, config *cfg.Conf SourcePath: componentToPromote.SourcePath, ComponentNames: []string{componentToPromote.ComponentName}, PerComponentSkippedTargetPaths: map[string][]string{}, + AutoMerge: componentToPromote.AutoMerge, }, ComputedSyncPaths: map[string]string{}, } diff --git a/internal/pkg/githubapi/promotion_test.go b/internal/pkg/githubapi/promotion_test.go index f9fed2ba..8743d9fd 100644 --- a/internal/pkg/githubapi/promotion_test.go +++ b/internal/pkg/githubapi/promotion_test.go @@ -15,16 +15,16 @@ import ( func generatePromotionPlanTestHelper(t *testing.T, config *cfg.Config, expectedPromotion map[string]PromotionInstance, mockedHTTPClient *http.Client) { t.Helper() ctx := context.Background() - ghclient := github.NewClient(mockedHTTPClient) + ghClientPair := GhClientPair{v3Client: github.NewClient(mockedHTTPClient)} labelName := "fast-promotion" ghPrClientDetails := GhPrClientDetails{ - Ctx: ctx, - Ghclient: ghclient, - Owner: "AnOwner", - Repo: "Arepo", - PrNumber: 120, - Ref: "Abranch", + Ctx: ctx, + GhClientPair: &ghClientPair, + Owner: "AnOwner", + Repo: "Arepo", + PrNumber: 120, + Ref: "Abranch", PrLogger: log.WithFields(log.Fields{ "repo": "AnOwner/Arepo", "prNumber": 120, diff --git a/templates/auto-merge-comment.gotmpl b/templates/auto-merge-comment.gotmpl new file mode 100644 index 00000000..b90ed991 --- /dev/null +++ b/templates/auto-merge-comment.gotmpl @@ -0,0 +1,5 @@ +{{define "autoMerge"}} +✅ Auto merge is enabled +🚀 Merging promotion PR: #{{.prNumber}} +{{ end }} +