Skip to content

Commit

Permalink
Add YAML translate release action. (#34)
Browse files Browse the repository at this point in the history
Allows to read values from source repositories and apply modifiers with this read value to an aggregation target repository.
  • Loading branch information
Gerrit91 authored Dec 14, 2020
1 parent 89c68f3 commit b865937
Show file tree
Hide file tree
Showing 5 changed files with 296 additions and 3 deletions.
17 changes: 17 additions & 0 deletions metal-robot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,23 @@ webhooks:
pull-request-title: "Next release of metal-stack"
repos:

- type: yaml-translate-releases
client: metal-stack-github
args:
repository: releases
repository-url: https://github.com/metal-stack/releases
pull-request-title: "Next release of metal-stack"
repos:
helm-charts:
- from:
file: "charts/metal-control-plane/Chart.yaml"
yaml-path: "version"
to:
- type: yaml-path-version-patch
args:
file: "release.yaml"
yaml-path: "helm-charts.metal-stack.metal-control-plane.tag"

- type: aggregate-releases
client: fi-ts-github
args:
Expand Down
21 changes: 20 additions & 1 deletion pkg/config/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,26 @@ type DistributeReleasesConfig struct {
BranchTemplate *string `mapstructure:"branch-template" description:"the branch to push in the target repos"`
CommitMsgTemplate *string `mapstructure:"commit-tpl" description:"template of the commit message in the target repos"`
PullRequestTitle *string `mapstructure:"pull-request-title" description:"title of the pull request"`
TargetRepos []TargetRepo `mapstructure:"repos" description:"the repositories that will be updated"`
TargetRepos []TargetRepo `mapstructure:"repos" description:"the repositories that will be updated"`
}

type YAMLTranslateReleasesConfig struct {
TargetRepositoryName string `mapstructure:"repository" description:"the name of the taget repo"`
TargetRepositoryURL string `mapstructure:"repository-url" description:"the url of the target repo"`
Branch *string `mapstructure:"branch" description:"the branch to push in the target repo"`
CommitMsgTemplate *string `mapstructure:"commit-tpl" description:"template of the commit message"`
PullRequestTitle *string `mapstructure:"pull-request-title" description:"title of the pull request"`
SourceRepos map[string][]YAMLTranslation `mapstructure:"repos" description:"the source repositories to trigger this action"`
}

type YAMLTranslation struct {
From YAMLTranslationRead `mapstructure:"from" description:"the yaml path from where to read the replacement value"`
To []Modifier `mapstructure:"to" description:"the actions to take on the traget repo with the read the replacement value"`
}

type YAMLTranslationRead struct {
File string `mapstructure:"file" description:"the name of the file to be patched"`
YAMLPath string `mapstructure:"yaml-path" description:"the yaml path to the version"`
}

type TargetRepo struct {
Expand Down
29 changes: 29 additions & 0 deletions pkg/webhooks/github/actions/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

const (
ActionAggregateReleases string = "aggregate-releases"
ActionYAMLTranslateReleases string = "yaml-translate-releases"
ActionDocsPreviewComment string = "docs-preview-comment"
ActionCreateRepositoryMaintainers string = "create-repository-maintainers"
ActionDistributeReleases string = "distribute-releases"
Expand All @@ -29,6 +30,7 @@ type WebhookActions struct {
ar []*AggregateReleases
dr []*distributeReleases
rd []*releaseDrafter
yr []*yamlTranslateReleases
}

func InitActions(logger *zap.SugaredLogger, cs clients.ClientMap, config config.WebhookActions) (*WebhookActions, error) {
Expand Down Expand Up @@ -79,6 +81,12 @@ func InitActions(logger *zap.SugaredLogger, cs clients.ClientMap, config config.
return nil, err
}
actions.rd = append(actions.rd, h)
case ActionYAMLTranslateReleases:
h, err := newYAMLTranslateReleases(logger, c.(*clients.Github), spec.Args)
if err != nil {
return nil, err
}
actions.yr = append(actions.yr, h)

default:
return nil, fmt.Errorf("handler type not supported: %s", t)
Expand Down Expand Up @@ -136,6 +144,27 @@ func (w *WebhookActions) ProcessReleaseEvent(payload *ghwebhooks.ReleasePayload)
})
}

for _, a := range w.yr {
a := a
g.Go(func() error {
if payload.Action != "released" {
return nil
}
params := &yamlTranslateReleaseParams{
RepositoryName: payload.Repository.Name,
RepositoryURL: payload.Repository.CloneURL,
TagName: payload.Release.TagName,
}
err := a.translateRelease(ctx, params)
if err != nil {
w.logger.Errorw("error creating translating release", "repo", a.repoName, "tag", params.TagName, "error", err)
return err
}

return nil
})
}

if err := g.Wait(); err != nil {
w.logger.Errorw("errors processing event", "error", err)
}
Expand Down
228 changes: 228 additions & 0 deletions pkg/webhooks/github/actions/yaml_translate_releases.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package actions

import (
"context"
"fmt"
"net/url"
"strings"
"sync"

"github.com/atedja/go-multilock"
"github.com/blang/semver"
v3 "github.com/google/go-github/v32/github"
"github.com/metal-stack/metal-robot/pkg/clients"
"github.com/metal-stack/metal-robot/pkg/config"
"github.com/metal-stack/metal-robot/pkg/git"
filepatchers "github.com/metal-stack/metal-robot/pkg/webhooks/modifiers/file-patchers"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"go.uber.org/zap"
)

type yamlTranslateReleases struct {
logger *zap.SugaredLogger
client *clients.Github
branch string
commitMessageTemplate string
translationMap map[string][]yamlTranslation
repoURL string
repoName string
pullRequestTitle string
}

type yamlTranslation struct {
from yamlFrom
to []filepatchers.Patcher
}

type yamlFrom struct {
file string
yamlPath string
}

type yamlTranslateReleaseParams struct {
RepositoryName string
RepositoryURL string
TagName string
}

func newYAMLTranslateReleases(logger *zap.SugaredLogger, client *clients.Github, rawConfig map[string]interface{}) (*yamlTranslateReleases, error) {
var (
branch = "develop"
commitMessageTemplate = "Bump %s to version %s"
pullRequestTitle = "Next release"
)

var typedConfig config.YAMLTranslateReleasesConfig
err := mapstructure.Decode(rawConfig, &typedConfig)
if err != nil {
return nil, err
}

if typedConfig.TargetRepositoryName == "" {
return nil, fmt.Errorf("target repository name must be specified")
}
if typedConfig.TargetRepositoryURL == "" {
return nil, fmt.Errorf("target repository-url must be specified")
}
if typedConfig.Branch != nil {
branch = *typedConfig.Branch
}
if typedConfig.CommitMsgTemplate != nil {
commitMessageTemplate = *typedConfig.CommitMsgTemplate
}
if typedConfig.PullRequestTitle != nil {
pullRequestTitle = *typedConfig.PullRequestTitle
}

translationMap := make(map[string][]yamlTranslation)
for n, translations := range typedConfig.SourceRepos {
for _, t := range translations {
from := yamlFrom{
file: t.From.File,
yamlPath: t.From.YAMLPath,
}

yt := yamlTranslation{
from: from,
to: []filepatchers.Patcher{},
}

for _, m := range t.To {
to, err := filepatchers.InitPatcher(m)
if err != nil {
fmt.Println("1")
return nil, err
}
yt.to = append(yt.to, to)
}
fmt.Println("2")

ts, ok := translationMap[n]
if !ok {
ts = []yamlTranslation{}
}
ts = append(ts, yt)
translationMap[n] = ts
}
}

return &yamlTranslateReleases{
logger: logger,
client: client,
branch: branch,
commitMessageTemplate: commitMessageTemplate,
translationMap: translationMap,
repoURL: typedConfig.TargetRepositoryURL,
repoName: typedConfig.TargetRepositoryName,
pullRequestTitle: pullRequestTitle,
}, nil
}

// TranslateRelease translates contents from one repository to another repository
func (r *yamlTranslateReleases) translateRelease(ctx context.Context, p *yamlTranslateReleaseParams) error {
translations, ok := r.translationMap[p.RepositoryName]
if !ok {
r.logger.Debugw("skip applying translate release actions to aggregation repo, not in list of source repositories", "target-repo", r.repoName, "source-repo", p.RepositoryName, "tag", p.TagName)
return nil
}

tag := p.TagName
trimmed := strings.TrimPrefix(tag, "v")
_, err := semver.Make(trimmed)
if err != nil {
r.logger.Infow("skip applying translate release actions to aggregation repo because not a valid semver release tag", "target-repo", r.repoName, "source-repo", p.RepositoryName, "tag", p.TagName)
return nil
}

// preventing concurrent git repo modifications
var once sync.Once
lock := multilock.New(r.repoName, p.RepositoryName)
lock.Lock()
defer once.Do(func() { lock.Unlock() })

token, err := r.client.GitToken(ctx)
if err != nil {
return errors.Wrap(err, "error creating git token")
}

sourceRepoURL, err := url.Parse(p.RepositoryURL)
if err != nil {
return err
}
sourceRepoURL.User = url.UserPassword("x-access-token", token)

sourceRepository, err := git.ShallowClone(sourceRepoURL.String(), r.branch, 1)
if err != nil {
return err
}

targetRepoURL, err := url.Parse(r.repoURL)
if err != nil {
return err
}
targetRepoURL.User = url.UserPassword("x-access-token", token)

targetRepository, err := git.ShallowClone(targetRepoURL.String(), r.branch, 1)
if err != nil {
return err
}

reader := func(file string) ([]byte, error) {
return git.ReadRepoFile(targetRepository, file)
}

writer := func(file string, content []byte) error {
return git.WriteRepoFile(targetRepository, file, content)
}

for _, translation := range translations {
content, err := git.ReadRepoFile(sourceRepository, translation.from.file)
if err != nil {
return errors.Wrap(err, "error reading content from source repository file")
}

value, err := filepatchers.GetYAML(content, translation.from.yamlPath)
if err != nil {
return errors.Wrap(err, "error reading value from source repository file")
}

for _, patch := range translation.to {
err = patch.Apply(reader, writer, value)
if err != nil {
return errors.Wrap(err, "error applying translate updates")
}
}
}

commitMessage := fmt.Sprintf(r.commitMessageTemplate, p.RepositoryName, tag)
hash, err := git.CommitAndPush(targetRepository, commitMessage)
if err != nil {
if err == git.NoChangesError {
r.logger.Debugw("skip push to target repository because nothing changed", "target-repo", p.RepositoryName, "source-repo", p.RepositoryName, "release", tag)
return nil
}
return errors.Wrap(err, "error pushing to target repository")
}

r.logger.Infow("pushed to translate target repo", "target-repo", p.RepositoryName, "source-repo", p.RepositoryName, "release", tag, "branch", r.branch, "hash", hash)

once.Do(func() { lock.Unlock() })

pr, _, err := r.client.GetV3Client().PullRequests.Create(ctx, r.client.Organization(), r.repoName, &v3.NewPullRequest{
Title: v3.String("Next release"),
Head: v3.String(r.branch),
Base: v3.String("master"),
Body: v3.String(r.pullRequestTitle),
MaintainerCanModify: v3.Bool(true),
})
if err != nil {
if !strings.Contains(err.Error(), "A pull request already exists") {
return err
}
} else {
r.logger.Infow("created pull request", "target-repo", p.RepositoryName, "url", pr.GetURL())
}

return nil
}
4 changes: 2 additions & 2 deletions pkg/webhooks/modifiers/file-patchers/yaml_path_patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func (p YAMLPathPatch) Apply(cn ContentReader, cw ContentWriter, newValue string
return err
}

old, err := getYAML(content, p.yamlPath)
old, err := GetYAML(content, p.yamlPath)
if err != nil {
return errors.Wrap(err, "error retrieving yaml path from file")
}
Expand Down Expand Up @@ -118,7 +118,7 @@ func setYAML(data []byte, path string, value interface{}) ([]byte, error) {
return res, nil
}

func getYAML(data []byte, path string) (string, error) {
func GetYAML(data []byte, path string) (string, error) {
json, err := yamlconv.YAMLToJSON(data)
if err != nil {
return "", err
Expand Down

0 comments on commit b865937

Please sign in to comment.