diff --git a/README.md b/README.md index b9b4679..a14b2d9 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,9 @@ See [here](docs/observability.md) To publish container images from a forked repo set the `IMAGE_NAME` and `REGISTRY` GitHub Action Repository variables to use GitHub packages. `REGISTRY` should be `ghcr.io` and `IMAGE_NAME` should match the repository slug, like so: like so: + image + ## Roadmap diff --git a/docs/installation.md b/docs/installation.md index e5611ba..db097c6 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -85,10 +85,12 @@ Pulled from `telefonistka.yaml` file in the repo root directory(default branch) Configuration keys: + |key|desc| |---|---| |`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].componentPathExtraDepth`| The number of extra nesting levels to add to the "components" being promoted, this allows nesting components in subdirectories while keeping them distinct.
A `2` value will mean the component name includes the 3 subdirectories under the `sourcePath`| |`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.| |`promotionPaths[0].conditions.autoMerge`| Boolean value. If set to true, PR will be automatically merged after it is created.| @@ -97,6 +99,7 @@ Configuration keys: |`dryRunMode`| if true, the bot will just comment the planned promotion on the merged PR| |`autoApprovePromotionPrs`| if true the bot will auto-approve all promotion PRs, with the assumption the original PR was peer reviewed and is promoted verbatim. Required additional GH token via APPROVER_GITHUB_OAUTH_TOKEN env variable| |`toggleCommitStatus`| Map of strings, allow (non-repo-admin) users to change the [Github commit status](https://docs.github.com/en/rest/commits/statuses) state(from failure to success and back). This can be used to continue promotion of a change that doesn't pass repo checks. the keys are strings commented in the PRs, values are [Github commit status context](https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28#create-a-commit-status) to be overridden| + Example: diff --git a/internal/pkg/configuration/config.go b/internal/pkg/configuration/config.go index c9fa292..4018f67 100644 --- a/internal/pkg/configuration/config.go +++ b/internal/pkg/configuration/config.go @@ -24,9 +24,10 @@ type PromotionPr struct { } type PromotionPath struct { - Conditions Condition `yaml:"conditions"` - SourcePath string `yaml:"sourcePath"` - PromotionPrs []PromotionPr `yaml:"promotionPrs"` + Conditions Condition `yaml:"conditions"` + ComponentPathExtraDepth int `yaml:"componentPathExtraDepth"` + SourcePath string `yaml:"sourcePath"` + PromotionPrs []PromotionPr `yaml:"promotionPrs"` } type Config struct { diff --git a/internal/pkg/githubapi/promotion.go b/internal/pkg/githubapi/promotion.go index 71bd96d..b2f8211 100644 --- a/internal/pkg/githubapi/promotion.go +++ b/internal/pkg/githubapi/promotion.go @@ -132,12 +132,18 @@ func GeneratePromotionPlan(ghPrClientDetails GhPrClientDetails, config *cfg.Conf for _, promotionPathConfig := range config.PromotionPaths { if match, _ := regexp.MatchString("^"+promotionPathConfig.SourcePath+".*", *changedFile.Filename); match { // "components" here are the sub directories of the SourcePath - getComponentRegexString := regexp.MustCompile("^" + promotionPathConfig.SourcePath + "([^/]*)/.*") + // but with promotionPathConfig.ComponentPathExtraDepth we can grab multiple levels of subdirectories, + // to support cases where components are nested deeper(e.g. [SourcePath]/owningTeam/namespace/component1) + componentPathRegexSubSstrings := []string{} + for i := 0; i <= promotionPathConfig.ComponentPathExtraDepth; i++ { + componentPathRegexSubSstrings = append(componentPathRegexSubSstrings, "[^/]*") + } + componentPathRegexSubString := strings.Join(componentPathRegexSubSstrings, "/") + getComponentRegexString := regexp.MustCompile("^" + promotionPathConfig.SourcePath + "(" + componentPathRegexSubString + ")/.*") componentName := getComponentRegexString.ReplaceAllString(*changedFile.Filename, "${1}") getSourcePathRegexString := regexp.MustCompile("^(" + promotionPathConfig.SourcePath + ")" + componentName + "/.*") compiledSourcePath := getSourcePathRegexString.ReplaceAllString(*changedFile.Filename, "${1}") - relevantComponentsElement := relevantComponent{ SourcePath: compiledSourcePath, ComponentName: componentName, diff --git a/internal/pkg/githubapi/promotion_test.go b/internal/pkg/githubapi/promotion_test.go index 8743d9f..411171a 100644 --- a/internal/pkg/githubapi/promotion_test.go +++ b/internal/pkg/githubapi/promotion_test.go @@ -434,3 +434,50 @@ func TestGeneratePromotionPlanTwoComponents(t *testing.T) { ) generatePromotionPlanTestHelper(t, config, expectedPromotion, mockedHTTPClient) } + +func TestGenerateNestedSourceRegexPromotionPlan(t *testing.T) { + t.Parallel() + config := &cfg.Config{ + PromotionPaths: []cfg.PromotionPath{ + { + SourcePath: "prod/us-east-4/", + ComponentPathExtraDepth: 2, + PromotionPrs: []cfg.PromotionPr{ + { + TargetPaths: []string{ + "prod/eu-west-1/", + }, + }, + }, + }, + }, + } + expectedPromotion := map[string]PromotionInstance{ + "prod/us-east-4/>prod/eu-west-1/": { + ComputedSyncPaths: map[string]string{ + "prod/eu-west-1/teamA/namespaceB/componentA": "prod/us-east-4/teamA/namespaceB/componentA", + }, + }, + } + + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetReposPullsFilesByOwnerByRepoByPullNumber, + []github.CommitFile{ + {Filename: github.String("prod/us-east-4/teamA/namespaceB/componentA/file.yaml")}, + {Filename: github.String("prod/us-east-4/teamA/namespaceB/componentA/aSubDir/file3.yaml")}, + }, + ), + mock.WithRequestMatchHandler( + mock.GetReposContentsByOwnerByRepoByPath, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mock.WriteError( + w, + http.StatusNotFound, + "no *optional* in-component telefonistka config file", + ) + }), + ), + ) + generatePromotionPlanTestHelper(t, config, expectedPromotion, mockedHTTPClient) +}