1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2024-12-31 01:53:50 +02:00

feat: add generic webhook announcer (#2750)

This commit is contained in:
Engin Diri 2022-01-08 18:46:27 +01:00 committed by GitHub
parent bdfb09cfb7
commit 339bcabbb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 419 additions and 0 deletions

View File

@ -17,6 +17,7 @@ import (
"github.com/goreleaser/goreleaser/internal/pipe/teams"
"github.com/goreleaser/goreleaser/internal/pipe/telegram"
"github.com/goreleaser/goreleaser/internal/pipe/twitter"
"github.com/goreleaser/goreleaser/internal/pipe/webhook"
"github.com/goreleaser/goreleaser/internal/tmpl"
"github.com/goreleaser/goreleaser/pkg/context"
)
@ -39,6 +40,7 @@ var announcers = []Announcer{
teams.Pipe{},
telegram.Pipe{},
twitter.Pipe{},
webhook.Pipe{},
}
// Pipe that announces releases.

View File

@ -0,0 +1,119 @@
package webhook
import (
"crypto/tls"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/apex/log"
"github.com/caarlos0/env/v6"
"github.com/goreleaser/goreleaser/internal/tmpl"
"github.com/goreleaser/goreleaser/pkg/context"
)
const (
defaultMessageTemplate = `{ "message": "{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}"}`
ContentTypeHeaderKey = "Content-Type"
UserAgentHeaderKey = "User-Agent"
UserAgentHeaderValue = "gorleaser"
AuthorizationHeaderKey = "Authorization"
DefaultContentType = "application/json; charset=utf-8"
)
type Pipe struct{}
func (Pipe) String() string { return "webhook" }
func (Pipe) Skip(ctx *context.Context) bool { return !ctx.Config.Announce.Webhook.Enabled }
type Config struct {
BasicAuthHeader string `env:"BASIC_AUTH_HEADER_VALUE"`
BearerTokenHeader string `env:"BEARER_TOKEN_HEADER_VALUE"`
}
func (p Pipe) Default(ctx *context.Context) error {
if ctx.Config.Announce.Webhook.MessageTemplate == "" {
ctx.Config.Announce.Webhook.MessageTemplate = defaultMessageTemplate
}
if ctx.Config.Announce.Webhook.ContentType == "" {
ctx.Config.Announce.Webhook.ContentType = DefaultContentType
}
return nil
}
func (p Pipe) Announce(ctx *context.Context) error {
var cfg Config
if err := env.Parse(&cfg); err != nil {
return fmt.Errorf("announce: failed to announce to webhook: %w", err)
}
endpointURLConfig, err := tmpl.New(ctx).Apply(ctx.Config.Announce.Webhook.EndpointURL)
if err != nil {
return fmt.Errorf("announce: failed to announce to webhook: %w", err)
}
if len(endpointURLConfig) == 0 {
return errors.New("announce: failed to announce to webhook: no endpoint url")
}
if _, err := url.ParseRequestURI(endpointURLConfig); err != nil {
return fmt.Errorf("announce: failed to announce to webhook: %w", err)
}
endpointURL, err := url.Parse(endpointURLConfig)
if err != nil {
return fmt.Errorf("announce: failed to announce to webhook: %w", err)
}
msg, err := tmpl.New(ctx).Apply(ctx.Config.Announce.Webhook.MessageTemplate)
if err != nil {
return fmt.Errorf("announce: failed to announce to webhook: %s", err)
}
log.Infof("posting: '%s'", msg)
customTransport := http.DefaultTransport.(*http.Transport).Clone()
customTransport.TLSClientConfig = &tls.Config{
InsecureSkipVerify: ctx.Config.Announce.Webhook.SkipTLSVerify,
}
client := &http.Client{
Transport: customTransport,
}
req, err := http.NewRequest(http.MethodPost, endpointURL.String(), strings.NewReader(msg))
if err != nil {
return fmt.Errorf("announce: failed to announce to webhook: %w", err)
}
req.Header.Add(ContentTypeHeaderKey, ctx.Config.Announce.Webhook.ContentType)
req.Header.Add(UserAgentHeaderKey, UserAgentHeaderValue)
if cfg.BasicAuthHeader != "" {
log.Debugf("set basic auth header")
req.Header.Add(AuthorizationHeaderKey, cfg.BasicAuthHeader)
} else if cfg.BearerTokenHeader != "" {
log.Debugf("set bearer token header")
req.Header.Add(AuthorizationHeaderKey, cfg.BearerTokenHeader)
}
for key, value := range ctx.Config.Announce.Webhook.Headers {
log.Debugf("Header Key %s / Value %s", key, value)
req.Header.Add(key, value)
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("announce: failed to announce to webhook: %w", err)
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK, http.StatusCreated, http.StatusAccepted, http.StatusNoContent:
log.Infof("Post OK: '%v'", resp.StatusCode)
body, _ := io.ReadAll(resp.Body)
log.Infof("Response : %v\n", string(body))
return nil
default:
return fmt.Errorf("request failed with status %v", resp.Status)
}
}

View File

@ -0,0 +1,241 @@
package webhook
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/google/uuid"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
"github.com/stretchr/testify/require"
)
func TestStringer(t *testing.T) {
require.Equal(t, Pipe{}.String(), "webhook")
}
func TestNoEndpoint(t *testing.T) {
ctx := context.New(config.Project{
Announce: config.Announce{
Webhook: config.Webhook{},
},
})
require.EqualError(t, Pipe{}.Announce(ctx), `announce: failed to announce to webhook: no endpoint url`)
}
func TestMalformedEndpoint(t *testing.T) {
ctx := context.New(config.Project{
Announce: config.Announce{
Webhook: config.Webhook{
EndpointURL: "httxxx://example.com",
},
},
})
require.EqualError(t, Pipe{}.Announce(ctx), `announce: failed to announce to webhook: Post "httxxx://example.com": unsupported protocol scheme "httxxx"`)
}
func TestAnnounceInvalidMessageTemplate(t *testing.T) {
ctx := context.New(config.Project{
Announce: config.Announce{
Webhook: config.Webhook{
EndpointURL: "https://example.com/webhook",
MessageTemplate: "{{ .Foo }",
},
},
})
require.EqualError(t, Pipe{}.Announce(ctx), `announce: failed to announce to webhook: template: tmpl:1: unexpected "}" in operand`)
}
type WebHookServerMockMessage struct {
Response string `json:"response"`
UUID uuid.UUID `json:"uuid"`
}
func TestAnnounceWebhook(t *testing.T) {
responseServer := WebHookServerMockMessage{
Response: "Thanks for the announcement!",
UUID: uuid.New(),
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
require.Equal(t, "webhook-test", string(body))
w.WriteHeader(http.StatusCreated)
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(responseServer)
require.NoError(t, err)
}))
defer srv.Close()
ctx := context.New(config.Project{
ProjectName: "webhook-test",
Announce: config.Announce{
Webhook: config.Webhook{
EndpointURL: srv.URL,
MessageTemplate: "{{ .ProjectName }}",
},
},
})
require.NoError(t, Pipe{}.Announce(ctx))
}
func TestAnnounceTLSWebhook(t *testing.T) {
responseServer := WebHookServerMockMessage{
Response: "Thanks for the announcement!",
UUID: uuid.New(),
}
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
require.Equal(t, "webhook-test", string(body))
w.WriteHeader(http.StatusCreated)
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(responseServer)
require.NoError(t, err)
}))
defer srv.Close()
fmt.Println(srv.URL)
ctx := context.New(config.Project{
ProjectName: "webhook-test",
Announce: config.Announce{
Webhook: config.Webhook{
EndpointURL: srv.URL,
MessageTemplate: "{{ .ProjectName }}",
SkipTLSVerify: true,
},
},
})
require.NoError(t, Pipe{}.Announce(ctx))
}
func TestAnnounceTLSCheckCertWebhook(t *testing.T) {
responseServer := WebHookServerMockMessage{
Response: "Thanks for the announcement!",
UUID: uuid.New(),
}
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
w.WriteHeader(http.StatusCreated)
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(responseServer)
require.NoError(t, err)
}))
defer srv.Close()
fmt.Println(srv.URL)
ctx := context.New(config.Project{
ProjectName: "webhook-test",
Announce: config.Announce{
Webhook: config.Webhook{
EndpointURL: srv.URL,
SkipTLSVerify: false,
},
},
})
require.Error(t, Pipe{}.Announce(ctx))
}
func TestAnnounceBasicAuthWebhook(t *testing.T) {
responseServer := WebHookServerMockMessage{
Response: "Thanks for the announcement!",
UUID: uuid.New(),
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
require.Equal(t, "webhook-test", string(body))
auth := r.Header.Get("Authorization")
require.Equal(t, fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte("user:pass"))), auth)
w.WriteHeader(http.StatusCreated)
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(responseServer)
require.NoError(t, err)
}))
defer srv.Close()
ctx := context.New(config.Project{
ProjectName: "webhook-test",
Announce: config.Announce{
Webhook: config.Webhook{
EndpointURL: srv.URL,
MessageTemplate: "{{ .ProjectName }}",
},
},
})
os.Setenv("BASIC_AUTH_HEADER_VALUE", fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte("user:pass"))))
defer os.Unsetenv("BASIC_AUTH_HEADER_VALUE")
require.NoError(t, Pipe{}.Announce(ctx))
}
func TestAnnounceAdditionalHeadersWebhook(t *testing.T) {
responseServer := WebHookServerMockMessage{
Response: "Thanks for the announcement!",
UUID: uuid.New(),
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
require.Equal(t, "webhook-test", string(body))
customHeader := r.Header.Get("X-Custom-Header")
require.Equal(t, "custom-value", customHeader)
w.WriteHeader(http.StatusCreated)
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(responseServer)
require.NoError(t, err)
}))
defer srv.Close()
ctx := context.New(config.Project{
ProjectName: "webhook-test",
Announce: config.Announce{
Webhook: config.Webhook{
EndpointURL: srv.URL,
MessageTemplate: "{{ .ProjectName }}",
Headers: map[string]string{
"X-Custom-Header": "custom-value",
},
},
},
})
require.NoError(t, Pipe{}.Announce(ctx))
}
func TestSkip(t *testing.T) {
t.Run("skip", func(t *testing.T) {
require.True(t, Pipe{}.Skip(context.New(config.Project{})))
})
t.Run("dont skip", func(t *testing.T) {
ctx := context.New(config.Project{
Announce: config.Announce{
Webhook: config.Webhook{
Enabled: true,
},
},
})
require.False(t, Pipe{}.Skip(ctx))
})
}

View File

@ -884,6 +884,16 @@ type Announce struct {
Mattermost Mattermost `yaml:"mattermost,omitempty"`
LinkedIn LinkedIn `yaml:"linkedin,omitempty"`
Telegram Telegram `yaml:"telegram,omitempty"`
Webhook Webhook `yaml:"webhook,omitempty"`
}
type Webhook struct {
Enabled bool `yaml:"enabled,omitempty"`
SkipTLSVerify bool `yaml:"skip_tls_verify,omitempty"`
MessageTemplate string `yaml:"message_template,omitempty"`
EndpointURL string `yaml:"endpoint_url,omitempty"`
Headers map[string]string `yaml:"headers,omitempty"`
ContentType string `yaml:"content_type,omitempty"`
}
type Twitter struct {

View File

@ -35,6 +35,7 @@ import (
"github.com/goreleaser/goreleaser/internal/pipe/telegram"
"github.com/goreleaser/goreleaser/internal/pipe/twitter"
"github.com/goreleaser/goreleaser/internal/pipe/universalbinary"
"github.com/goreleaser/goreleaser/internal/pipe/webhook"
"github.com/goreleaser/goreleaser/pkg/context"
)
@ -82,4 +83,5 @@ var Defaulters = []Defaulter{
milestone.Pipe{},
linkedin.Pipe{},
telegram.Pipe{},
webhook.Pipe{},
}

View File

@ -0,0 +1,44 @@
# Webhook
Webhooks are a way to receive notifications. With this `Goreleaser` functionality, you can send events to any server
exposing a webhook.
If your endpoints are not secure, you can use following environment variables to configure them:
- BASIC_AUTH_HEADER_VALUE like `Basic <base64(username:password)>`
- BEARER_TOKEN_HEADER_VALUE like `Bearer <token>`
Add following to your `.goreleaser.yaml` config to enable the webhook functionality:
```yaml
# .goreleaser.yaml
announce:
webhook:
# Whether its enabled or not.
# Defaults to false.
enabled: true
# Check the certificate of the webhook. Defaults to false.
skip_tls_verify: true
# Message template to use while publishing.
# Defaults to `{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}`
message_template: '{ "title": "Awesome project {{.Tag}} is out!"}'
# Content type to use.
# Defaults to `"application/json; charset=utf-8"`
content_type: "application/json"
# Endpoint to send the webhook to.
endpoint_url: "https://example.com/webhook"
# Headers to send with the webhook.
# For example:
# headers:
# Authorization: "Bearer <token>"
headers:
User-Agent: "goreleaser"
```
!!! tip
Learn more about the [name template engine](/customization/templates/).

View File

@ -122,6 +122,7 @@ nav:
- customization/announce/teams.md
- customization/announce/telegram.md
- customization/announce/twitter.md
- customization/announce/webhook.md
- Command Line Usage:
- goreleaser: cmd/goreleaser.md
- goreleaser init: cmd/goreleaser_init.md