Add commit URL to Slack notifications with link and backtick formatting

- Add commit_url column to webhook_events and deployments tables
- Extract commit URL from webhook payload (from commit object or repo URL)
- Format Slack messages with backticks for branch and commit SHA
- Link commit SHA to the actual commit URL on the git server
- Keep plain text format for ntfy notifications
This commit is contained in:
2025-12-31 16:29:22 -08:00
parent 4cd12d717c
commit c4362c3143
6 changed files with 109 additions and 45 deletions

View File

@@ -4,6 +4,7 @@ package notify
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
@@ -73,14 +74,24 @@ func (svc *Service) NotifyBuildStart(
deployment *models.Deployment,
) {
title := "Build started: " + app.Name
message := "Building from branch " + app.Branch
// Plain text message for ntfy
ntfyMessage := "Building from branch " + app.Branch
if deployment.CommitSHA.Valid {
shortSHA := deployment.CommitSHA.String[:minInt(shortCommitLength, len(deployment.CommitSHA.String))]
message += " at " + shortSHA
shortSHA := truncateSHA(deployment.CommitSHA.String)
ntfyMessage += " at " + shortSHA
}
svc.sendNotifications(ctx, app, title, message, "info")
// Slack message with formatting
slackMessage := "Building from branch `" + app.Branch + "`"
if deployment.CommitSHA.Valid {
shortSHA := truncateSHA(deployment.CommitSHA.String)
slackMessage += " at " + formatCommitLink(shortSHA, deployment.CommitURL)
}
svc.sendNotifications(ctx, app, title, ntfyMessage, slackMessage, "info")
}
// NotifyBuildSuccess sends a build success notification.
@@ -93,7 +104,7 @@ func (svc *Service) NotifyBuildSuccess(
title := "Build success: " + app.Name
message := "Image built successfully in " + formatDuration(duration)
svc.sendNotifications(ctx, app, title, message, "success")
svc.sendNotifications(ctx, app, title, message, message, "success")
}
// NotifyBuildFailed sends a build failed notification.
@@ -107,7 +118,7 @@ func (svc *Service) NotifyBuildFailed(
title := "Build failed: " + app.Name
message := "Build failed after " + formatDuration(duration) + ": " + buildErr.Error()
svc.sendNotifications(ctx, app, title, message, "error")
svc.sendNotifications(ctx, app, title, message, message, "error")
}
// NotifyDeploySuccess sends a deploy success notification.
@@ -118,14 +129,24 @@ func (svc *Service) NotifyDeploySuccess(
) {
duration := time.Since(deployment.StartedAt)
title := "Deploy success: " + app.Name
message := "Successfully deployed in " + formatDuration(duration)
// Plain text message for ntfy
ntfyMessage := "Successfully deployed in " + formatDuration(duration)
if deployment.CommitSHA.Valid {
shortSHA := deployment.CommitSHA.String[:minInt(shortCommitLength, len(deployment.CommitSHA.String))]
message += " (commit " + shortSHA + ")"
shortSHA := truncateSHA(deployment.CommitSHA.String)
ntfyMessage += " (commit " + shortSHA + ")"
}
svc.sendNotifications(ctx, app, title, message, "success")
// Slack message with formatting
slackMessage := "Successfully deployed in " + formatDuration(duration)
if deployment.CommitSHA.Valid {
shortSHA := truncateSHA(deployment.CommitSHA.String)
slackMessage += " (commit " + formatCommitLink(shortSHA, deployment.CommitURL) + ")"
}
svc.sendNotifications(ctx, app, title, ntfyMessage, slackMessage, "success")
}
// NotifyDeployFailed sends a deploy failed notification.
@@ -139,7 +160,7 @@ func (svc *Service) NotifyDeployFailed(
title := "Deploy failed: " + app.Name
message := "Deployment failed after " + formatDuration(duration) + ": " + deployErr.Error()
svc.sendNotifications(ctx, app, title, message, "error")
svc.sendNotifications(ctx, app, title, message, message, "error")
}
// formatDuration formats a duration for display.
@@ -154,19 +175,28 @@ func formatDuration(d time.Duration) string {
return fmt.Sprintf("%dm %ds", minutes, seconds)
}
// minInt returns the smaller of two integers.
func minInt(a, b int) int {
if a < b {
return a
// truncateSHA truncates a commit SHA to shortCommitLength characters.
func truncateSHA(sha string) string {
if len(sha) > shortCommitLength {
return sha[:shortCommitLength]
}
return b
return sha
}
// formatCommitLink formats a commit SHA as a Slack link if URL is available.
func formatCommitLink(shortSHA string, commitURL sql.NullString) string {
if commitURL.Valid && commitURL.String != "" {
return "<" + commitURL.String + "|`" + shortSHA + "`>"
}
return "`" + shortSHA + "`"
}
func (svc *Service) sendNotifications(
ctx context.Context,
app *models.App,
title, message, priority string,
title, ntfyMessage, slackMessage, priority string,
) {
// Send to ntfy if configured
if app.NtfyTopic.Valid && app.NtfyTopic.String != "" {
@@ -178,7 +208,7 @@ func (svc *Service) sendNotifications(
// even if the parent context is cancelled.
notifyCtx := context.WithoutCancel(ctx)
ntfyErr := svc.sendNtfy(notifyCtx, ntfyTopic, title, message, priority)
ntfyErr := svc.sendNtfy(notifyCtx, ntfyTopic, title, ntfyMessage, priority)
if ntfyErr != nil {
svc.log.Error(
"failed to send ntfy notification",
@@ -199,7 +229,7 @@ func (svc *Service) sendNotifications(
// even if the parent context is cancelled.
notifyCtx := context.WithoutCancel(ctx)
slackErr := svc.sendSlack(notifyCtx, slackWebhook, title, message, priority)
slackErr := svc.sendSlack(notifyCtx, slackWebhook, title, slackMessage, priority)
if slackErr != nil {
svc.log.Error(
"failed to send slack notification",