feat(notify): release v1.8.0 — Slack/Teams/Google Chat/Telegram/webhook notifications
- Add internal/notify package: best-effort release notifications to Slack, Microsoft Teams, Google Chat, Telegram, and a generic JSON webhook; every target is independently opt-in and a failed notification never fails the release - Add notify config section (slack_webhook_url, teams_webhook_url, google_chat_webhook_url, telegram_bot_token/telegram_chat_id, webhook_url) with matching env var fallbacks; 100% coverage plus FuzzMessagePayloads - Wire notification sending into run() — fires after tag+push (including --no-release), skipped on --no-push/--no-commit since nothing was published yet - Document the notify section in README, Hugo docs, and .releaser.yml template; update CLAUDE.md architecture/fuzzing tables - Also commit the Apache License 2.0 docs-site footer link (docs/hugo.toml geekdocContentLicense), left uncommitted from a prior change Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ type Config struct {
|
||||
Python PythonConfig `yaml:"python"`
|
||||
GitLab GitLabConfig `yaml:"gitlab"`
|
||||
GitHub GitHubConfig `yaml:"github"`
|
||||
Notify NotifyConfig `yaml:"notify"`
|
||||
}
|
||||
|
||||
type GitConfig struct {
|
||||
@@ -120,6 +121,17 @@ type GitHubConfig struct {
|
||||
Repo string `yaml:"repo"` // "owner/repo"
|
||||
}
|
||||
|
||||
// NotifyConfig configures best-effort release notifications. Every field is
|
||||
// opt-in — a target is only used when its required fields are non-empty.
|
||||
type NotifyConfig struct {
|
||||
SlackWebhookURL string `yaml:"slack_webhook_url"`
|
||||
TeamsWebhookURL string `yaml:"teams_webhook_url"`
|
||||
GoogleChatWebhookURL string `yaml:"google_chat_webhook_url"`
|
||||
TelegramBotToken string `yaml:"telegram_bot_token"`
|
||||
TelegramChatID string `yaml:"telegram_chat_id"`
|
||||
WebhookURL string `yaml:"webhook_url"`
|
||||
}
|
||||
|
||||
func defaults() Config {
|
||||
return Config{
|
||||
Git: GitConfig{
|
||||
@@ -162,6 +174,12 @@ func defaultSources() Sources {
|
||||
"gitlab.project": "default",
|
||||
"github.token": "default",
|
||||
"github.repo": "default",
|
||||
"notify.slack_webhook_url": "default",
|
||||
"notify.teams_webhook_url": "default",
|
||||
"notify.google_chat_webhook_url": "default",
|
||||
"notify.telegram_bot_token": "default",
|
||||
"notify.telegram_chat_id": "default",
|
||||
"notify.webhook_url": "default",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,6 +278,24 @@ func LoadWithSources(dir string) (Config, Sources, error) {
|
||||
if overlay.GitHub.Repo != "" {
|
||||
src["github.repo"] = "config file"
|
||||
}
|
||||
if overlay.Notify.SlackWebhookURL != "" {
|
||||
src["notify.slack_webhook_url"] = "config file"
|
||||
}
|
||||
if overlay.Notify.TeamsWebhookURL != "" {
|
||||
src["notify.teams_webhook_url"] = "config file"
|
||||
}
|
||||
if overlay.Notify.GoogleChatWebhookURL != "" {
|
||||
src["notify.google_chat_webhook_url"] = "config file"
|
||||
}
|
||||
if overlay.Notify.TelegramBotToken != "" {
|
||||
src["notify.telegram_bot_token"] = "config file"
|
||||
}
|
||||
if overlay.Notify.TelegramChatID != "" {
|
||||
src["notify.telegram_chat_id"] = "config file"
|
||||
}
|
||||
if overlay.Notify.WebhookURL != "" {
|
||||
src["notify.webhook_url"] = "config file"
|
||||
}
|
||||
|
||||
return cfg, src, nil
|
||||
}
|
||||
@@ -310,4 +346,52 @@ func (c *Config) ApplyEnvWithSources(src Sources) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.Notify.SlackWebhookURL == "" {
|
||||
if v := os.Getenv("SLACK_WEBHOOK_URL"); v != "" {
|
||||
c.Notify.SlackWebhookURL = v
|
||||
if src != nil {
|
||||
src["notify.slack_webhook_url"] = "env: SLACK_WEBHOOK_URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.Notify.TeamsWebhookURL == "" {
|
||||
if v := os.Getenv("TEAMS_WEBHOOK_URL"); v != "" {
|
||||
c.Notify.TeamsWebhookURL = v
|
||||
if src != nil {
|
||||
src["notify.teams_webhook_url"] = "env: TEAMS_WEBHOOK_URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.Notify.GoogleChatWebhookURL == "" {
|
||||
if v := os.Getenv("GOOGLE_CHAT_WEBHOOK_URL"); v != "" {
|
||||
c.Notify.GoogleChatWebhookURL = v
|
||||
if src != nil {
|
||||
src["notify.google_chat_webhook_url"] = "env: GOOGLE_CHAT_WEBHOOK_URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.Notify.TelegramBotToken == "" {
|
||||
if v := os.Getenv("TELEGRAM_BOT_TOKEN"); v != "" {
|
||||
c.Notify.TelegramBotToken = v
|
||||
if src != nil {
|
||||
src["notify.telegram_bot_token"] = "env: TELEGRAM_BOT_TOKEN"
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.Notify.TelegramChatID == "" {
|
||||
if v := os.Getenv("TELEGRAM_CHAT_ID"); v != "" {
|
||||
c.Notify.TelegramChatID = v
|
||||
if src != nil {
|
||||
src["notify.telegram_chat_id"] = "env: TELEGRAM_CHAT_ID"
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.Notify.WebhookURL == "" {
|
||||
if v := os.Getenv("RELEASER_WEBHOOK_URL"); v != "" {
|
||||
c.Notify.WebhookURL = v
|
||||
if src != nil {
|
||||
src["notify.webhook_url"] = "env: RELEASER_WEBHOOK_URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,6 +381,98 @@ func TestLoadPythonPathsSources(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadNotifySources(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
content := `
|
||||
notify:
|
||||
slack_webhook_url: "https://hooks.slack.example/x"
|
||||
teams_webhook_url: "https://outlook.office.example/y"
|
||||
google_chat_webhook_url: "https://chat.googleapis.example/z"
|
||||
telegram_bot_token: "bot-token"
|
||||
telegram_chat_id: "chat-1"
|
||||
webhook_url: "https://example.com/webhook"
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, filename), []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg, src, err := LoadWithSources(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.Notify.SlackWebhookURL != "https://hooks.slack.example/x" {
|
||||
t.Errorf("SlackWebhookURL = %q", cfg.Notify.SlackWebhookURL)
|
||||
}
|
||||
if cfg.Notify.TeamsWebhookURL != "https://outlook.office.example/y" {
|
||||
t.Errorf("TeamsWebhookURL = %q", cfg.Notify.TeamsWebhookURL)
|
||||
}
|
||||
if cfg.Notify.GoogleChatWebhookURL != "https://chat.googleapis.example/z" {
|
||||
t.Errorf("GoogleChatWebhookURL = %q", cfg.Notify.GoogleChatWebhookURL)
|
||||
}
|
||||
if cfg.Notify.TelegramBotToken != "bot-token" {
|
||||
t.Errorf("TelegramBotToken = %q", cfg.Notify.TelegramBotToken)
|
||||
}
|
||||
if cfg.Notify.TelegramChatID != "chat-1" {
|
||||
t.Errorf("TelegramChatID = %q", cfg.Notify.TelegramChatID)
|
||||
}
|
||||
if cfg.Notify.WebhookURL != "https://example.com/webhook" {
|
||||
t.Errorf("WebhookURL = %q", cfg.Notify.WebhookURL)
|
||||
}
|
||||
|
||||
wantConfigFile := []string{
|
||||
"notify.slack_webhook_url", "notify.teams_webhook_url", "notify.google_chat_webhook_url",
|
||||
"notify.telegram_bot_token", "notify.telegram_chat_id", "notify.webhook_url",
|
||||
}
|
||||
for _, key := range wantConfigFile {
|
||||
if got := src[key]; got != "config file" {
|
||||
t.Errorf("src[%q] = %q, want %q", key, got, "config file")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvWithSourcesNotify(t *testing.T) {
|
||||
t.Setenv("SLACK_WEBHOOK_URL", "https://hooks.slack.example/env")
|
||||
t.Setenv("TEAMS_WEBHOOK_URL", "https://outlook.office.example/env")
|
||||
t.Setenv("GOOGLE_CHAT_WEBHOOK_URL", "https://chat.googleapis.example/env")
|
||||
t.Setenv("TELEGRAM_BOT_TOKEN", "env-bot-token")
|
||||
t.Setenv("TELEGRAM_CHAT_ID", "env-chat-id")
|
||||
t.Setenv("RELEASER_WEBHOOK_URL", "https://example.com/env-webhook")
|
||||
|
||||
cfg := defaults()
|
||||
src := defaultSources()
|
||||
cfg.ApplyEnvWithSources(src)
|
||||
|
||||
cases := []struct {
|
||||
got, want, srcKey, wantSrc string
|
||||
}{
|
||||
{cfg.Notify.SlackWebhookURL, "https://hooks.slack.example/env", "notify.slack_webhook_url", "env: SLACK_WEBHOOK_URL"},
|
||||
{cfg.Notify.TeamsWebhookURL, "https://outlook.office.example/env", "notify.teams_webhook_url", "env: TEAMS_WEBHOOK_URL"},
|
||||
{cfg.Notify.GoogleChatWebhookURL, "https://chat.googleapis.example/env", "notify.google_chat_webhook_url", "env: GOOGLE_CHAT_WEBHOOK_URL"},
|
||||
{cfg.Notify.TelegramBotToken, "env-bot-token", "notify.telegram_bot_token", "env: TELEGRAM_BOT_TOKEN"},
|
||||
{cfg.Notify.TelegramChatID, "env-chat-id", "notify.telegram_chat_id", "env: TELEGRAM_CHAT_ID"},
|
||||
{cfg.Notify.WebhookURL, "https://example.com/env-webhook", "notify.webhook_url", "env: RELEASER_WEBHOOK_URL"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if c.got != c.want {
|
||||
t.Errorf("got %q, want %q", c.got, c.want)
|
||||
}
|
||||
if src[c.srcKey] != c.wantSrc {
|
||||
t.Errorf("src[%q] = %q, want %q", c.srcKey, src[c.srcKey], c.wantSrc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEnvNotifyDoesNotOverwrite(t *testing.T) {
|
||||
t.Setenv("SLACK_WEBHOOK_URL", "https://hooks.slack.example/env")
|
||||
|
||||
cfg := defaults()
|
||||
cfg.Notify.SlackWebhookURL = "https://hooks.slack.example/config"
|
||||
cfg.ApplyEnv()
|
||||
|
||||
if cfg.Notify.SlackWebhookURL != "https://hooks.slack.example/config" {
|
||||
t.Errorf("SlackWebhookURL overwritten: got %q", cfg.Notify.SlackWebhookURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPartialOverride(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// Only override tag_prefix — commit_message should keep its default
|
||||
|
||||
Reference in New Issue
Block a user