Skip to content

Commit

Permalink
[release tool] Slack notifications (#9668)
Browse files Browse the repository at this point in the history
* slack notifications

* allow setting debug via env var

* cleanups

* address review feedback

* Compile product name into binary

* use constant instead of ldflag
  • Loading branch information
radTuti authored Jan 13, 2025
1 parent c1f485e commit 8ac4f70
Show file tree
Hide file tree
Showing 18 changed files with 444 additions and 236 deletions.
3 changes: 2 additions & 1 deletion release/cmd/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var debugFlag = &cli.BoolFlag{
Name: "debug",
Aliases: []string{"d"},
Usage: "Enable verbose log output",
EnvVars: []string{"DEBUG"},
Value: false,
Destination: &debug,
}
Expand All @@ -58,7 +59,7 @@ var (
Name: "repo",
Usage: "The GitHub repository to use for the release",
EnvVars: []string{"GIT_REPO"},
Value: utils.Calico,
Value: utils.CalicoRepoName,
}
repoRemoteFlag = &cli.StringFlag{
Name: "remote",
Expand Down
13 changes: 10 additions & 3 deletions release/cmd/hashrelease.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,11 +247,18 @@ func hashreleaseSubCommands(cfg *Config) []*cli.Command {
return err
}

if !c.Bool(skipImageScanFlag.Name) {
hashrel.ImageScanResultURL, err = imagescanner.RetrieveResultURL(cfg.TmpDir)
// Only log error as a warning if the image scan result URL could not be retrieved
// as it is not an error that should stop the hashrelease process.
if err != nil {
logrus.WithError(err).Warn("Failed to retrieve image scan result URL")
}
}

// Send a slack message to notify that the hashrelease has been published.
if c.Bool(publishHashreleaseFlag.Name) {
if err := tasks.HashreleaseSlackMessage(slackConfig(c), hashrel, !c.Bool(skipImageScanFlag.Name), ciJobURL(c), cfg.TmpDir); err != nil {
return err
}
return tasks.AnnounceHashrelease(slackConfig(c), hashrel, ciJobURL(c))
}
return nil
},
Expand Down
26 changes: 21 additions & 5 deletions release/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

"github.com/projectcalico/calico/release/internal/command"
"github.com/projectcalico/calico/release/internal/utils"
"github.com/projectcalico/calico/release/pkg/tasks"
)

type Config struct {
Expand Down Expand Up @@ -67,14 +68,29 @@ func main() {
}

app := &cli.App{
Name: "release",
Usage: "a tool for building releases",
Flags: globalFlags,
Commands: Commands(cfg),
Name: "release",
Usage: fmt.Sprintf("release tool for %s", utils.ProductName),
Flags: globalFlags,
Commands: Commands(cfg),
EnableBashCompletion: true,
ExitErrHandler: func(c *cli.Context, err error) {
if err == nil {
return
}
if c.Bool(ciFlag.Name) {
logrus.WithError(err).Info("Sending slack notification")
if err := tasks.SendErrorNotification(slackConfig(c), err, ciJobURL(c), cfg.RepoRootDir); err != nil {
logrus.WithError(err).Error("Failed to send slack notification")
}
} else {
logrus.WithError(err).Debug("Skip sending slack notification, not running in CI")
}
cli.HandleExitCoder(err)
},
}

// Run the app.
if err := app.Run(os.Args); err != nil {
logrus.WithError(err).Fatal("Error running task")
logrus.WithError(err).Fatal("Error running app")
}
}
5 changes: 2 additions & 3 deletions release/internal/hashreleaseserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,6 @@ type Hashrelease struct {
// Stream is the version the hashrelease is for (e.g master, v3.19)
Stream string

// Product is the product in the hashrelease
Product string

// ProductVersion is the product version in the hashrelease
ProductVersion string

Expand All @@ -67,6 +64,8 @@ type Hashrelease struct {

// Latest is if the hashrelease is the latest for the stream
Latest bool

ImageScanResultURL string
}

func (h *Hashrelease) URL() string {
Expand Down
13 changes: 6 additions & 7 deletions release/internal/imagescanner/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,24 +154,23 @@ func writeScanResultToFile(resp *http.Response, outputDir string) error {
}

// RetrieveResultURL retrieves the URL to the image scan result from the scan result file.
func RetrieveResultURL(outputDir string) string {
func RetrieveResultURL(outputDir string) (string, error) {
outputFilePath := filepath.Join(outputDir, scanResultFileName)
if _, err := os.Stat(outputFilePath); os.IsNotExist(err) {
logrus.WithError(err).Error("Image scan result file does not exist")
return ""
return "", fmt.Errorf("image scan result file does not exist")
}
var result map[string]interface{}
resultData, err := os.ReadFile(outputFilePath)
if err != nil {
logrus.WithError(err).Error("Failed to read image scan result file")
return ""
return "", err
}
if err := json.Unmarshal(resultData, &result); err != nil {
logrus.WithError(err).Error("Failed to unmarshal image scan result")
return ""
return "", err
}
if link, ok := result["results_link"].(string); ok {
return link
return link, nil
}
return ""
return "", fmt.Errorf("no results link found in image scan result")
}
2 changes: 1 addition & 1 deletion release/internal/outputs/releasenotes.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ func ReleaseNotes(owner, githubToken, repoRootDir, outputDir string, ver version
outputDir = "."
}
logrus.Infof("Generating release notes for %s", ver.FormattedString())
milestone := ver.Milestone(utils.CalicoProductName())
milestone := ver.Milestone(utils.ProductName)
githubClient := github.NewTokenClient(context.Background(), githubToken)
releaseNoteDataList := []*ReleaseNoteIssueData{}
opts := &github.MilestoneListOptions{
Expand Down
1 change: 0 additions & 1 deletion release/internal/pinnedversion/pinnedversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,6 @@ func LoadHashrelease(repoRootDir, outputDir, hashreleaseSrcBaseDir string, lates
Name: pinnedVersion.ReleaseName,
Hash: pinnedVersion.Hash,
Note: pinnedVersion.Note,
Product: utils.CalicoProductName(),
Stream: version.DeterminePublishStream(productBranch, pinnedVersion.Title),
ProductVersion: pinnedVersion.Title,
OperatorVersion: pinnedVersion.TigeraOperator.Version,
Expand Down
128 changes: 128 additions & 0 deletions release/internal/slack/message.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright (c) 2024-2025 Tigera, Inc. All rights reserved.

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package slack

import (
"bytes"
_ "embed"
"fmt"
"html/template"

"github.com/sirupsen/logrus"
"github.com/slack-go/slack"

"github.com/projectcalico/calico/release/internal/registry"
)

var (
//go:embed templates/hashrelease-published.json.gotmpl
publishedMessageTemplateData string
//go:embed templates/failure.json.gotmpl
failureMessageTemplateData string
//go:embed templates/missing-images.json.gotmpl
missingImagesMessageTemplateData string
)

// BaseMessageData contains the common fields for all messages.
type BaseMessageData struct {
ReleaseName string
Product string
Stream string
ProductVersion string
OperatorVersion string
ReleaseType string
CIURL string
}

// HashreleasePublishedMessageData contains the fields for sending a message about a hashrelease being published.
type HashreleasePublishedMessageData struct {
BaseMessageData
DocsURL string
ImageScanResultURL string
}

// MissingImagesMessageData contains the fields for sending a message about missing images.
type MissingImagesMessageData struct {
BaseMessageData
MissingImages []registry.Component
}

// FailureMessageData contains the fields for sending a message about a failure.
type FailureMessageData struct {
BaseMessageData
Error string
}

// PostHashreleaseAnnouncement sends a message to slack about a hashrelease being published.
func PostHashreleaseAnnouncement(cfg *Config, msg *HashreleasePublishedMessageData) error {
message, err := renderMessage(publishedMessageTemplateData, msg)
if err != nil {
logrus.WithError(err).Error("Failed to render message")
return err
}
return sendToSlack(cfg, message)
}

// PostMissingImagesMessage sends a message to slack about missing images.
func PostMissingImagesMessage(cfg *Config, msg *MissingImagesMessageData) error {
message, err := renderMessage(missingImagesMessageTemplateData, msg)
if err != nil {
logrus.WithError(err).Error("Failed to render message")
return err
}
return sendToSlack(cfg, message)
}

// PostFailureMessage sends a message to slack about a failure.
func PostFailureMessage(cfg *Config, msg *FailureMessageData) error {
message, err := renderMessage(failureMessageTemplateData, msg)
if err != nil {
logrus.WithError(err).Error("Failed to render message")
return err
}
return sendToSlack(cfg, message)
}

// renderMessage renders a message template with the provided data.
func renderMessage(text string, data any) ([]slack.Block, error) {
tmpl, err := template.New("message").Parse(text)
if err != nil {
return nil, err
}

var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return nil, err
}
blocks := slack.Blocks{}
if err := blocks.UnmarshalJSON(buf.Bytes()); err != nil {
return nil, err
}
return blocks.BlockSet, nil
}

// sendToSlack sends a message to slack.
func sendToSlack(cfg *Config, message []slack.Block) error {
if cfg == nil {
return fmt.Errorf("no configuration provided")
}
if !cfg.Valid() {
return fmt.Errorf("invalid or missing configuration")
}

client := slack.New(cfg.Token, slack.OptionDebug(logrus.IsLevelEnabled(logrus.DebugLevel)))
_, _, err := client.PostMessage(cfg.Channel, slack.MsgOptionBlocks(message...))
return err
}
Loading

0 comments on commit 8ac4f70

Please sign in to comment.