From 2c53e0e678cb72563b5807b92a22e2d2b1db4e0f Mon Sep 17 00:00:00 2001 From: Gerrit91 Date: Mon, 26 Feb 2024 10:44:19 +0100 Subject: [PATCH] Implement release freeze. --- go.mod | 15 ++-- go.sum | 38 +++++---- pkg/config/actions.go | 2 + .../github/actions/aggregate_releases.go | 85 +++++++++++++++++-- pkg/webhooks/github/actions/common.go | 2 + pkg/webhooks/github/actions/issues_handler.go | 48 +++++++---- .../github/actions/issues_handler_test.go | 51 +++++++++++ .../github/actions/release_drafter.go | 39 ++++++++- pkg/webhooks/gitlab/actions/common.go | 1 + 9 files changed, 230 insertions(+), 51 deletions(-) create mode 100644 pkg/webhooks/github/actions/issues_handler_test.go diff --git a/go.mod b/go.mod index d7fdf5e..9df9515 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/go-playground/webhooks/v6 v6.3.0 github.com/google/go-cmp v0.6.0 github.com/google/go-github/v57 v57.0.0 + github.com/metal-stack/metal-lib v0.14.4 github.com/metal-stack/v v1.0.3 github.com/mitchellh/mapstructure v1.5.0 github.com/spf13/cobra v1.8.0 @@ -43,7 +44,7 @@ require ( github.com/leodido/go-urn v1.2.4 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sergi/go-diff v1.3.1 // indirect @@ -56,13 +57,13 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.16.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sys v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.13.0 // indirect + golang.org/x/tools v0.17.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index ae45c0f..3011bad 100644 --- a/go.sum +++ b/go.sum @@ -93,6 +93,8 @@ github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/metal-stack/metal-lib v0.14.4 h1:vm2868vcua6khoyWL7d0to8Hq5RayrjMse0FZTyWEec= +github.com/metal-stack/metal-lib v0.14.4/go.mod h1:Z3PAh8dkyWC4B19fXsu6EYwXXee0Lk9JZbjoHPLbDbc= github.com/metal-stack/v v1.0.3 h1:Sh2oBlnxrCUD+mVpzfC8HiqL045YWkxs0gpTvkjppqs= github.com/metal-stack/v v1.0.3/go.mod h1:YTahEu7/ishwpYKnp/VaW/7nf8+PInogkfGwLcGPdXg= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -102,8 +104,8 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= -github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= -github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -161,8 +163,8 @@ github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6 github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= -go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= @@ -170,17 +172,17 @@ go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -195,18 +197,18 @@ golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/config/actions.go b/pkg/config/actions.go index 8f5a73b..56a7f5c 100644 --- a/pkg/config/actions.go +++ b/pkg/config/actions.go @@ -72,6 +72,8 @@ type TargetRepo struct { type ReleaseDraftConfig struct { Repos map[string]any `mapstructure:"repos" description:"the repositories for that a release draft will be pushed"` RepositoryName string `mapstructure:"repository" description:"the name of the release repo"` + Branch *string `mapstructure:"branch" description:"the branch considered for releases"` + BranchBase *string `mapstructure:"branch-base" description:"the base branch to raise the pull request against"` ReleaseTitleTemplate *string `mapstructure:"title-template" description:"custom template for the release title"` DraftHeadline *string `mapstructure:"draft-headline" description:"custom headline for the release draft"` diff --git a/pkg/webhooks/github/actions/aggregate_releases.go b/pkg/webhooks/github/actions/aggregate_releases.go index fabcb94..39e5d5c 100644 --- a/pkg/webhooks/github/actions/aggregate_releases.go +++ b/pkg/webhooks/github/actions/aggregate_releases.go @@ -12,6 +12,7 @@ import ( "github.com/Masterminds/semver/v3" "github.com/atedja/go-multilock" v3 "github.com/google/go-github/v57/github" + "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/metal-stack/metal-robot/pkg/clients" "github.com/metal-stack/metal-robot/pkg/config" "github.com/metal-stack/metal-robot/pkg/git" @@ -37,6 +38,7 @@ type AggregateReleases struct { type AggregateReleaseParams struct { RepositoryName string TagName string + Sender string } func NewAggregateReleases(logger *zap.SugaredLogger, client *clients.Github, rawConfig map[string]any) (*AggregateReleases, error) { @@ -105,9 +107,11 @@ func NewAggregateReleases(logger *zap.SugaredLogger, client *clients.Github, raw // AggregateRelease applies the given actions after push and release trigger of a given list of source repositories to a target repository func (r *AggregateReleases) AggregateRelease(ctx context.Context, p *AggregateReleaseParams) error { + log := r.logger.With("target-repo", r.repoName, "source-repo", p.RepositoryName, "tag", p.TagName) + patches, ok := r.patchMap[p.RepositoryName] if !ok { - r.logger.Debugw("skip applying release actions to aggregation repo, not in list of source repositories", "target-repo", r.repoName, "source-repo", p.RepositoryName, "tag", p.TagName) + log.Debugw("skip applying release actions to aggregation repo, not in list of source repositories") return nil } @@ -115,8 +119,37 @@ func (r *AggregateReleases) AggregateRelease(ctx context.Context, p *AggregateRe trimmed := strings.TrimPrefix(tag, "v") _, err := semver.NewVersion(trimmed) if err != nil { - r.logger.Infow("skip applying 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 //nolint:nilerr + log.Infow("skip applying release actions to aggregation repo because not a valid semver release tag", "error", err) + return nil + } + + openPR, err := findOpenReleasePR(ctx, r.client, r.client.Organization(), r.repoName, r.branch, r.branchBase) + if err != nil { + return err + } + + if openPR != nil { + frozen, err := isReleaseFreeze(ctx, r.client, openPR) + if err != nil { + return err + } + + if frozen { + log.Infow("skip applying release actions to aggregation repo because release is currently frozen") + + _, _, err = r.client.GetV3AppClient().PullRequests.CreateComment(ctx, r.client.Organization(), r.repoName, *openPR.Number, &v3.PullRequestComment{ + Body: v3.String(fmt.Sprintf("Release `%v` in repository %q was rejected because release is currently frozen. Please re-issue the release hook once this branch was merged or unfrozen. /cc @%s", + p.TagName, + p.RepositoryName, + p.Sender, + )), + }) + if err != nil { + return fmt.Errorf("unable to create comment for rejected release aggregation: %w", err) + } + + return nil + } } // preventing concurrent git repo modifications @@ -159,12 +192,12 @@ func (r *AggregateReleases) AggregateRelease(ctx context.Context, p *AggregateRe hash, err := git.CommitAndPush(repository, commitMessage) if err != nil { if errors.Is(err, git.NoChangesError) { - r.logger.Debugw("skip push to target repository because nothing changed", "target-repo", p.RepositoryName, "source-repo", p.RepositoryName, "release", tag) + log.Debugw("skip push to target repository because nothing changed") } else { return fmt.Errorf("error pushing to target repository %w", err) } } else { - r.logger.Infow("pushed to aggregate target repo", "target-repo", p.RepositoryName, "source-repo", p.RepositoryName, "release", tag, "branch", r.branch, "hash", hash) + log.Infow("pushed to aggregate target repo", "branch", r.branch, "hash", hash) once.Do(func() { r.lock.Unlock() }) } @@ -181,8 +214,48 @@ func (r *AggregateReleases) AggregateRelease(ctx context.Context, p *AggregateRe return err } } else { - r.logger.Infow("created pull request", "target-repo", p.RepositoryName, "url", pr.GetURL()) + log.Infow("created pull request", "url", pr.GetURL()) } return nil } + +func findOpenReleasePR(ctx context.Context, client *clients.Github, owner, repo, branch, base string) (*v3.PullRequest, error) { + prs, _, err := client.GetV3AppClient().PullRequests.List(ctx, owner, repo, &v3.PullRequestListOptions{ + State: "open", + Head: branch, + Base: base, + }) + if err != nil { + return nil, fmt.Errorf("unable to list pull requests: %w", err) + } + + if len(prs) == 1 { + return prs[0], nil + } + + return nil, nil +} + +func isReleaseFreeze(ctx context.Context, client *clients.Github, pr *v3.PullRequest) (bool, error) { + comments, _, err := client.GetV3AppClient().PullRequests.ListComments(ctx, *pr.Base.Repo.Owner.Name, *pr.Base.Repo.Name, pointer.SafeDeref(pr.Number), &v3.PullRequestListCommentsOptions{ + Direction: "desc", + }) + if err != nil { + return true, fmt.Errorf("unable to list pull request comments: %w", err) + } + + for _, comment := range comments { + comment := comment + + if ok := searchForCommandInComment(pointer.SafeDeref(comment.Body), IssueCommentReleaseFreeze); ok { + return true, nil + } + + if ok := searchForCommandInComment(pointer.SafeDeref(comment.Body), IssueCommentReleaseUnfreeze); ok { + return false, nil + } + } + + return false, nil +} diff --git a/pkg/webhooks/github/actions/common.go b/pkg/webhooks/github/actions/common.go index e9f4414..6df749b 100644 --- a/pkg/webhooks/github/actions/common.go +++ b/pkg/webhooks/github/actions/common.go @@ -120,6 +120,7 @@ func (w *WebhookActions) ProcessReleaseEvent(ctx context.Context, payload *ghweb params := &AggregateReleaseParams{ RepositoryName: payload.Repository.Name, TagName: payload.Release.TagName, + Sender: payload.Sender.Login, } err := a.AggregateRelease(ctx, params) if err != nil { @@ -249,6 +250,7 @@ func (w *WebhookActions) ProcessPushEvent(ctx context.Context, payload *ghwebhoo params := &AggregateReleaseParams{ RepositoryName: payload.Repository.Name, TagName: extractTag(payload), + Sender: payload.Sender.Login, } err := a.AggregateRelease(ctx, params) diff --git a/pkg/webhooks/github/actions/issues_handler.go b/pkg/webhooks/github/actions/issues_handler.go index f3063cd..202c3a6 100644 --- a/pkg/webhooks/github/actions/issues_handler.go +++ b/pkg/webhooks/github/actions/issues_handler.go @@ -16,13 +16,17 @@ import ( type IssueCommentCommand string const ( - IssueCommentCommandPrefix = "/" - IssueCommentBuildFork IssueCommentCommand = IssueCommentCommandPrefix + "ok-to-build" + IssueCommentCommandPrefix = "/" + IssueCommentBuildFork IssueCommentCommand = IssueCommentCommandPrefix + "ok-to-build" + IssueCommentReleaseFreeze IssueCommentCommand = IssueCommentCommandPrefix + "freeze" + IssueCommentReleaseUnfreeze IssueCommentCommand = IssueCommentCommandPrefix + "unfreeze" ) var ( IssueCommentCommands = map[IssueCommentCommand]bool{ - IssueCommentBuildFork: true, + IssueCommentBuildFork: true, + IssueCommentReleaseFreeze: true, + IssueCommentReleaseUnfreeze: true, } AllowedAuthorAssociations = map[string]bool{ @@ -80,21 +84,14 @@ func (r *IssuesAction) HandleIssueComment(ctx context.Context, p *IssuesActionPa return nil } - comment := strings.TrimSpace(p.Comment) - - _, ok = IssueCommentCommands[IssueCommentCommand(comment)] - if !ok { - r.logger.Debugw("skip handling issues comment action, message does not contain a valid command", "source-repo", p.RepositoryName) - return nil + if ok := searchForCommandInComment(p.Comment, IssueCommentBuildFork); ok { + err := r.buildForkPR(ctx, p) + if err != nil { + return err + } } - switch IssueCommentCommand(comment) { - case IssueCommentBuildFork: - return r.buildForkPR(ctx, p) - default: - r.logger.Debugw("skip handling issues comment action, message does not contain a valid command", "source-repo", p.RepositoryName) - return nil - } + return nil } func (r *IssuesAction) buildForkPR(ctx context.Context, p *IssuesActionParams) error { @@ -140,3 +137,22 @@ func (r *IssuesAction) buildForkPR(ctx context.Context, p *IssuesActionParams) e return nil } + +func searchForCommandInComment(comment string, want IssueCommentCommand) bool { + for _, line := range strings.Split(comment, "\n") { + line = strings.TrimSpace(line) + + cmd := IssueCommentCommand(line) + + _, ok := IssueCommentCommands[cmd] + if !ok { + continue + } + + if cmd == want { + return true + } + } + + return false +} diff --git a/pkg/webhooks/github/actions/issues_handler_test.go b/pkg/webhooks/github/actions/issues_handler_test.go new file mode 100644 index 0000000..bc14cdd --- /dev/null +++ b/pkg/webhooks/github/actions/issues_handler_test.go @@ -0,0 +1,51 @@ +package actions + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func Test_searchForCommandInComment(t *testing.T) { + tests := []struct { + name string + comment string + search IssueCommentCommand + want bool + }{ + { + name: "find in single line", + comment: "/freeze", + search: IssueCommentReleaseFreeze, + want: true, + }, + { + name: "no match", + comment: "/foo", + search: IssueCommentReleaseFreeze, + want: false, + }, + { + name: "find with strip", + comment: " /freeze ", + search: IssueCommentReleaseFreeze, + want: true, + }, + { + name: "find in multi line", + comment: `Release is frozen now. + /freeze + `, + search: IssueCommentReleaseFreeze, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := searchForCommandInComment(tt.comment, tt.search) + if diff := cmp.Diff(got, tt.want); diff != "" { + t.Errorf("diff: %s", diff) + } + }) + } +} diff --git a/pkg/webhooks/github/actions/release_drafter.go b/pkg/webhooks/github/actions/release_drafter.go index 5d2cbdd..cd578e0 100644 --- a/pkg/webhooks/github/actions/release_drafter.go +++ b/pkg/webhooks/github/actions/release_drafter.go @@ -40,6 +40,8 @@ type codeBlock struct { type releaseDrafter struct { logger *zap.SugaredLogger client *clients.Github + branch string + branchBase string titleTemplate string draftHeadline string repoMap map[string]bool @@ -60,6 +62,8 @@ func newReleaseDrafter(logger *zap.SugaredLogger, client *clients.Github, rawCon releaseTitleTemplate = "%s" draftHeadline = "General" prHeadline = "Merged Pull Requests" + branch = "develop" + branchBase = "master" ) var typedConfig config.ReleaseDraftConfig @@ -80,6 +84,12 @@ func newReleaseDrafter(logger *zap.SugaredLogger, client *clients.Github, rawCon if typedConfig.MergedPRsHeadline != nil { prHeadline = *typedConfig.MergedPRsHeadline } + if typedConfig.Branch != nil { + branch = *typedConfig.Branch + } + if typedConfig.BranchBase != nil && *typedConfig.BranchBase != "" { + branchBase = *typedConfig.BranchBase + } repos := make(map[string]bool) for name := range typedConfig.Repos { @@ -93,6 +103,8 @@ func newReleaseDrafter(logger *zap.SugaredLogger, client *clients.Github, rawCon return &releaseDrafter{ logger: logger, client: client, + branch: branch, + branchBase: branchBase, repoMap: repos, repoName: typedConfig.RepositoryName, titleTemplate: releaseTitleTemplate, @@ -104,12 +116,14 @@ func newReleaseDrafter(logger *zap.SugaredLogger, client *clients.Github, rawCon // draft updates a release draft in a release repository func (r *releaseDrafter) draft(ctx context.Context, p *releaseDrafterParams) error { + log := r.logger.With("repo", p.RepositoryName, "release", p.TagName) + _, ok := r.repoMap[p.RepositoryName] if !ok { // if there is an ACTIONS_REQUIRED block, we want to add it (even when it's not a release vector repository) if p.ComponentReleaseInfo == nil { - r.logger.Debugw("skip adding release draft because not a release vector repo and no special sections", "repo", p.RepositoryName, "release", p.TagName) + log.Debugw("skip adding release draft because not a release vector repo and no special sections") return nil } @@ -127,7 +141,7 @@ func (r *releaseDrafter) draft(ctx context.Context, p *releaseDrafterParams) err } err = r.prependCodeBlocks(m, *p.ComponentReleaseInfo, releaseSuffix) if err != nil { - r.logger.Debugw("skip adding release draft", "reason", err, "repo", p.RepositoryName) + log.Debugw("skip adding release draft", "reason", err) return nil } @@ -138,16 +152,33 @@ func (r *releaseDrafter) draft(ctx context.Context, p *releaseDrafterParams) err componentTag := p.TagName if !strings.HasPrefix(componentTag, "v") { - r.logger.Debugw("skip adding release draft because tag not starting with v", "repo", p.RepositoryName, "release", componentTag) + log.Debugw("skip adding release draft because tag not starting with v") return nil } trimmedVersion := strings.TrimPrefix(componentTag, "v") componentSemver, err := semver.NewVersion(trimmedVersion) if err != nil { - r.logger.Debugw("skip adding release draft because tag is not semver compatible", "repo", p.RepositoryName, "release", componentTag) + log.Debugw("skip adding release draft because tag is not semver compatible") return nil //nolint:nilerr } + openPR, err := findOpenReleasePR(ctx, r.client, r.client.Organization(), r.repoName, r.branch, r.branchBase) + if err != nil { + return err + } + + if openPR != nil { + frozen, err := isReleaseFreeze(ctx, r.client, openPR) + if err != nil { + return err + } + + if frozen { + log.Infow("skip adding release draft because release is currently frozen") + return nil + } + } + infos, err := r.releaseInfos(ctx) if err != nil { return err diff --git a/pkg/webhooks/gitlab/actions/common.go b/pkg/webhooks/gitlab/actions/common.go index 64d5c89..707349c 100644 --- a/pkg/webhooks/gitlab/actions/common.go +++ b/pkg/webhooks/gitlab/actions/common.go @@ -63,6 +63,7 @@ func (w *WebhookActions) ProcessTagEvent(ctx context.Context, payload *glwebhook params := &ghactions.AggregateReleaseParams{ RepositoryName: payload.Repository.Name, TagName: extractTag(payload), + Sender: payload.UserUsername, } err := a.AggregateRelease(ctx, params) if err != nil {