Skip to content
This repository has been archived by the owner on Sep 30, 2024. It is now read-only.

ci: add owners awareness to deployment-notifier #33968

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions enterprise/dev/deployment-notifier/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ deployment-notifier -environment $MY_ENV -slack.token=$SLACK_TOKEN -slack.webhoo
- `-slack.token` Slack Token used to find the matching Slack handle for pull request authors.
- `-slack.webhook` Slack webhook URL to post the notifications on.
- `-honeycomb.token` Honeycomb API token that is used to upload deployment traces.
- `-codeowners` (defaults to `false`) computes the codeowners for each PR and annotate the pull requests on Slack with mentions (expensive, it requires a full clone).
- requires `-slack.token` to resolve to Slack handles, will fall back to GitHub handles otherwise.

## How it works

Expand Down
108 changes: 108 additions & 0 deletions enterprise/dev/deployment-notifier/code_owner_resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package main

import (
"bufio"
"bytes"
"os"
"os/exec"
"strings"
"sync"

"github.com/sourcegraph/codenotify"
"github.com/sourcegraph/sourcegraph/lib/errors"
)

// CodeOwnerResolver returns the list of GitHub users who subscribed themselves
// to changes on particular paths, through the OWNERS file.
//
// See https://github.com/sourcegraph/codenotify for more informations.
type CodeOwnerResolver interface {
Resolve(ref string) ([]string, error)
}

func NewMockCodeOwnerResolver(mapping map[string][]string) CodeOwnerResolver {
if mapping == nil {
mapping = map[string][]string{}
}
return &mockCodeOwnerResolver{
mapping: mapping,
}
}

type mockCodeOwnerResolver struct {
mapping map[string][]string
}

func (m *mockCodeOwnerResolver) Resolve(ref string) ([]string, error) {
return m.mapping[ref], nil
}

func NewGitCodeOwnerResolver(cloneURL string, clonePath string) CodeOwnerResolver {
return &gitCodeOwnerResolver{
cloneURL: cloneURL,
clonePath: clonePath,
}
}

type gitCodeOwnerResolver struct {
cloneURL string
clonePath string
once sync.Once
}

func (g *gitCodeOwnerResolver) Resolve(ref string) ([]string, error) {
var err error
g.once.Do(func() {
_, err = exec.Command("git", "clone", "--quiet", "[email protected]:sourcegraph/sourcegraph.git", g.clonePath).Output()
})
if err != nil {
return nil, err
}

cwd, err := os.Getwd()
if err != nil {
return nil, err
}
os.Chdir(g.clonePath)
defer func() { _ = os.Chdir(cwd) }()

owners := []string{}
diff, err := exec.Command("git", "show", "--name-only", ref).CombinedOutput()
if err != nil {
return nil, errors.Wrapf(err, "error getting diff for %s", ref)
}
paths, err := readLines(diff)
if err != nil {
return nil, errors.Errorf("error scanning lines from diff %s: %s\n%s", ref, err, string(diff))
}
for _, path := range paths {
fs := codenotify.NewGitFS(g.clonePath, ref)
ownersList, err := codenotify.Subscribers(fs, path, "CODENOTIFY")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

temp thing, because there aren't much OWNERS files, so this makes testing easier.

if err != nil {
return nil, errors.Wrapf(err, "error computing the subscribers for %s", path)
}
owners = append(owners, ownersList...)
}
return uniqAndSanitize(owners), nil
}

func uniqAndSanitize(strs []string) []string {
set := map[string]struct{}{}
for _, str := range strs {
set[str] = struct{}{}
}
uniqStrs := make([]string, 0, len(set))
for str := range set {
uniqStrs = append(uniqStrs, strings.TrimLeft(str, "@"))
}
return uniqStrs
}

func readLines(b []byte) ([]string, error) {
lines := []string{}
scanner := bufio.NewScanner(bytes.NewBuffer(b))
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
return lines, scanner.Err()
}
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type ServiceVersionDiff struct {
type DeploymentDiffer interface {
Services() (map[string]*ServiceVersionDiff, error)
}

type manifestDeploymentDiffer struct {
changedFiles []string
diffs map[string]*ServiceVersionDiff
Expand Down
37 changes: 28 additions & 9 deletions enterprise/dev/deployment-notifier/deployment_notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@ var (
type DeploymentNotifier struct {
dd DeploymentDiffer
ghc *github.Client
or CodeOwnerResolver
environment string
manifestRevision string
}

func NewDeploymentNotifier(ghc *github.Client, dd DeploymentDiffer, environment, manifestRevision string) *DeploymentNotifier {
func NewDeploymentNotifier(ghc *github.Client, dd DeploymentDiffer, or CodeOwnerResolver, environment string, manifestRevision string) *DeploymentNotifier {
return &DeploymentNotifier{
dd: dd,
ghc: ghc,
or: or,
environment: environment,
manifestRevision: manifestRevision,
}
Expand All @@ -52,8 +54,9 @@ type DeploymentReport struct {
// Services, PullRequests are a summary of all services and pull requests included in
// this deployment. For more accurate association of PRs to which services got deployed,
// use ServicesPerPullRequest instead.
Services []string
PullRequests []*github.PullRequest
Services []string
PullRequests []*github.PullRequest
PullRequestsCodeOwners map[int][]string

// ServicesPerPullRequest is an accurate representation of exactly which pull requests
// are associated with each service, because each service might be deployed with a
Expand Down Expand Up @@ -116,13 +119,29 @@ func (dn *DeploymentNotifier) Report(ctx context.Context) (*DeploymentReport, er
return nil, ErrNoRelevantChanges
}

// tmpFolder, err := os.MkdirTemp("", "sourcegraph-xxxxxx")
// defer func() {
// _ = os.RemoveAll(tmpFolder)
// }()

prOwners := map[int][]string{}
for _, pr := range prs {
// get the owners
ref := pr.GetMergeCommitSHA()
prOwners[pr.GetNumber()], err = dn.or.Resolve(ref)
if err != nil {
return nil, err
}
}

return &DeploymentReport{
Environment: dn.environment,
PullRequests: prs,
DeployedAt: time.Now().In(time.UTC).Format(time.RFC822Z),
Services: deployedServices,
BuildkiteBuildURL: os.Getenv("BUILDKITE_BUILD_URL"),
ManifestRevision: dn.manifestRevision,
Environment: dn.environment,
PullRequests: prs,
PullRequestsCodeOwners: prOwners,
DeployedAt: time.Now().In(time.UTC).Format(time.RFC822Z),
Services: deployedServices,
BuildkiteBuildURL: os.Getenv("BUILDKITE_BUILD_URL"),
ManifestRevision: dn.manifestRevision,

ServicesPerPullRequest: makeServicesPerPullRequest(prServicesMap),
}, nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func TestDeploymentNotifier(t *testing.T) {
dn := NewDeploymentNotifier(
ghc,
NewMockManifestDeployementsDiffer(m),
NewMockCodeOwnerResolver(nil),
"tests",
"",
)
Expand All @@ -99,6 +100,7 @@ func TestDeploymentNotifier(t *testing.T) {
dn := NewDeploymentNotifier(
ghc,
NewMockManifestDeployementsDiffer(m),
NewMockCodeOwnerResolver(nil),
"tests",
"",
)
Expand Down Expand Up @@ -133,6 +135,7 @@ func TestDeploymentNotifier(t *testing.T) {
dn := NewDeploymentNotifier(
ghc,
NewMockManifestDeployementsDiffer(m),
NewMockCodeOwnerResolver(nil),
"tests",
"",
)
Expand Down Expand Up @@ -166,6 +169,7 @@ func TestDeploymentNotifier(t *testing.T) {
dn := NewDeploymentNotifier(
ghc,
NewMockManifestDeployementsDiffer(m),
NewMockCodeOwnerResolver(nil),
"tests",
"",
)
Expand Down
23 changes: 20 additions & 3 deletions enterprise/dev/deployment-notifier/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
type Flags struct {
GitHubToken string
DryRun bool
CodeOwners bool
Environment string
SlackToken string
SlackAnnounceWebhook string
Expand All @@ -34,6 +35,7 @@ func (f *Flags) Parse() {
flag.StringVar(&f.GitHubToken, "github.token", os.Getenv("GITHUB_TOKEN"), "mandatory github token")
flag.StringVar(&f.Environment, "environment", "", "Environment being deployed")
flag.BoolVar(&f.DryRun, "dry", false, "Pretend to post notifications, printing to stdout instead")
flag.BoolVar(&f.CodeOwners, "codeowners", false, "Mention code owners for each PR on Slack")
flag.StringVar(&f.SlackToken, "slack.token", "", "mandatory slack api token")
flag.StringVar(&f.SlackAnnounceWebhook, "slack.webhook", "", "Slack Webhook URL to post the results on")
flag.StringVar(&f.HoneycombToken, "honeycomb.token", "", "mandatory honeycomb api token")
Expand Down Expand Up @@ -71,10 +73,24 @@ func main() {
log.Fatal(err)
}

dd := NewManifestDeploymentDiffer(changedFiles)
var or CodeOwnerResolver
if flags.CodeOwners {
tmpDir, err := os.MkdirTemp("", "sourcegraph-clone-xxxxx")
if err != nil {
log.Fatal(err)
}
defer func() {
os.RemoveAll(tmpDir)
}()
or = NewGitCodeOwnerResolver("[email protected]:sourcegraph/sourcegraph.git", tmpDir)
} else {
or = NewMockCodeOwnerResolver(nil)
}

dn := NewDeploymentNotifier(
ghc,
dd,
NewManifestDeploymentDiffer(changedFiles),
or,
flags.Environment,
manifestRevision,
)
Expand All @@ -97,13 +113,14 @@ func main() {
}
}

// Notifcations
// Notifications
slc := slack.New(flags.SlackToken)
teammates := team.NewTeammateResolver(ghc, slc)
if flags.DryRun {
fmt.Println("Github\n---")
for _, pr := range report.PullRequests {
fmt.Println("-", pr.GetNumber())
fmt.Println(" - code owners:", strings.Join(report.PullRequestsCodeOwners[pr.GetNumber()], ", "))
}
out, err := renderComment(report, traceURL)
if err != nil {
Expand Down
17 changes: 17 additions & 0 deletions enterprise/dev/deployment-notifier/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ var slackTemplate = `:arrow_left: *{{.Environment}}* deployment (<{{.BuildURL}}|
- Pull Requests:
{{- range .PullRequests }}
- <{{ .WebURL }}|{{ .Name }}> {{ .AuthorSlackID }}
{{- if .Owners }}
- Owners: {{- range .Owners}}{{ . }}{{" "}}{{- end}}
{{- end }}
Comment on lines +27 to +29
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes me nervous because there is no opt-out, and with staggered deploys this can be several pings a day

Copy link
Contributor Author

@jhchabran jhchabran Apr 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bobheadxi What do you think about adding in the handbook's data the following additional fields for each team member?

- Corgi MacDog 
  github: "woofwoof"
  notifications:
    deployments: 
      preprod: 
        author: true
        codeowners: false 
      cloud:
        author: true
        codeowners: true

With the following defaults, that could be overridden by the GitHub labels on PRs?

  notifications:
    deployments: 
      preprod: 
        author: false
        codeowners: false 
      cloud:
        author: true
        codeowners: true

Side note: presently, there are very few codeowners file in the repo and the team aliases do not ping, therefore they would ping even less.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That might overload the intent of team.yaml 🤔

Copy link
Member

@bobheadxi bobheadxi Apr 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, can we only notify OWNERS if we have notify-on-deploy set? That would make the opt-in explicit

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but right now I don't see an immediate solution other than this :/ Maybe another file possibly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, can we only notify OWNERS if we have notify-on-deploy set? That would make the opt-in explicit

This would defeat the purpose of giving more visibility to what happens in Cloud, that was mentioned over Slack (https://sourcegraph.slack.com/archives/C89KCDK5J/p1649869394862269).

If we were to decide to revert the changes to the team.yml that's a rather easy fix though. And if we start to see many PRs dropping the notifications, that would also be a strong factual signal that we're too noisy as well.

I also want to run a spike on guessing how a given PR changed the services, that would make the signal VS noise much higher and notifications would be much more useful.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I missed that discussion!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's try this then, and see how it goes. I wonder if that means we should revert notify-on-deploy as well, as well as the service-based notifications

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or introduce the team.yaml field you propose like so:

  notifications:
    deployments:
       opt_in: true

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{{- end }}`

type slackSummaryPresenter struct {
Expand All @@ -37,6 +40,7 @@ type pullRequestPresenter struct {
Name string
AuthorSlackID string
WebURL string
Owners []string
}

func slackSummary(ctx context.Context, teammates team.TeammateResolver, report *DeploymentReport, traceURL string) (string, error) {
Expand All @@ -63,10 +67,23 @@ func slackSummary(ctx context.Context, teammates team.TeammateResolver, report *
}
}

ghOwners := report.PullRequestsCodeOwners[pr.GetNumber()]
slackOwners := make([]string, 0, len(ghOwners))
for _, ghOwner := range ghOwners {
teammate, err := teammates.ResolveByGitHubHandle(ctx, ghOwner)
if err != nil {
// TODO handle teams
slackOwners = append(slackOwners, "`"+ghOwner+"`")
} else {
slackOwners = append(slackOwners, fmt.Sprintf("<@%s>", teammate.SlackID))
}
}

presenter.PullRequests = append(presenter.PullRequests, pullRequestPresenter{
Name: pr.GetTitle(),
WebURL: pr.GetHTMLURL(),
AuthorSlackID: authorSlackID,
Owners: slackOwners,
})
}

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ require (
github.com/slack-go/slack v0.10.1
github.com/smacker/go-tree-sitter v0.0.0-20220209044044-0d3022e933c3
github.com/snabb/sitemap v1.0.0
github.com/sourcegraph/codenotify v0.5.2-0.20220415141925-41450faf3c5e
github.com/sourcegraph/ctxvfs v0.0.0-20180418081416-2b65f1b1ea81
github.com/sourcegraph/go-ctags v0.0.0-20220404085534-f974026334d7
github.com/sourcegraph/go-diff v0.6.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2183,6 +2183,8 @@ github.com/sourcegraph/alertmanager v0.21.1-0.20211110092431-863f5b1ee51b h1:Mly
github.com/sourcegraph/alertmanager v0.21.1-0.20211110092431-863f5b1ee51b/go.mod h1:0MLTrjQI8EuVmvykEhcfr/7X0xmaDAZrqMgxIq3OXHk=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/codenotify v0.5.2-0.20220415141925-41450faf3c5e h1:4ecubQEIntQAXTqiVUH21XfIXY/CZUt87s5tMb0uQH8=
github.com/sourcegraph/codenotify v0.5.2-0.20220415141925-41450faf3c5e/go.mod h1:JUDBqComAfChmUf1EULTIB5BvzM0YP5pIf3WZE3UHlE=
github.com/sourcegraph/ctxvfs v0.0.0-20180418081416-2b65f1b1ea81 h1:v4/JVxZSPWifxmICRqgXK7khThjw03RfdGhyeA2S4EQ=
github.com/sourcegraph/ctxvfs v0.0.0-20180418081416-2b65f1b1ea81/go.mod h1:xIvvI5FiHLxhv8prbzVpaMHaaGPFPFQSuTcxC91ryOo=
github.com/sourcegraph/go-ctags v0.0.0-20220404085534-f974026334d7 h1:jl6zTZRCzF4V2ZjBBlrW2Sscd639mHKhGtMYKaBgEiQ=
Expand Down