Browse Source

feat: slack - gitea 메시지 정의

main
지대한 2 weeks ago
parent
commit
edd25cff75
  1. 69
      internal/gitea/gitea.go
  2. 7
      internal/slack/slack.go

69
internal/gitea/gitea.go

@ -14,6 +14,7 @@ type user struct {
Login string `json:"login"` Login string `json:"login"`
Username string `json:"username"` Username string `json:"username"`
Email string `json:"email"` Email string `json:"email"`
Name string `json:"name"` // commit author/committer 표시 이름
} }
func (u *user) login() string { func (u *user) login() string {
@ -63,8 +64,10 @@ type review struct {
} }
type commitInfo struct { type commitInfo struct {
ID string `json:"id"`
Message string `json:"message"` Message string `json:"message"`
URL string `json:"url"` URL string `json:"url"`
Author *user `json:"author"`
} }
type payload struct { type payload struct {
@ -72,6 +75,7 @@ type payload struct {
Number int `json:"number"` Number int `json:"number"`
Repository struct { Repository struct {
FullName string `json:"full_name"` FullName string `json:"full_name"`
HTMLURL string `json:"html_url"`
} `json:"repository"` } `json:"repository"`
Sender *user `json:"sender"` Sender *user `json:"sender"`
PullRequest *pullRequest `json:"pull_request"` PullRequest *pullRequest `json:"pull_request"`
@ -146,28 +150,55 @@ func BuildChannelMessage(event string, body []byte) (notify.Notification, bool)
switch event { switch event {
case "push": case "push":
branch := strings.TrimPrefix(p.Ref, "refs/heads/") branch := strings.TrimPrefix(p.Ref, "refs/heads/")
title := fmt.Sprintf("📤 [%s] %s에 커밋 %d개 push", repo, branch, len(p.Commits)) n := len(p.Commits)
lines := make([]string, 0, len(p.Commits)) plural := ""
if n != 1 {
plural = "s"
}
// 헤더: [repo:branch] N new commit(s) pushed by {actor} (Gitea 기본 형식과 유사)
repoRef := fmt.Sprintf("%s:%s", repo, branch)
if p.Repository.HTMLURL != "" {
repoRef = fmt.Sprintf("<%s/src/branch/%s|%s:%s>", p.Repository.HTMLURL, branch, repo, branch)
}
head := fmt.Sprintf("[%s] %d new commit%s pushed by %s", repoRef, n, plural, actor)
// 커밋 줄: <url|shortsha>: 메시지 - 작성자명
lines := make([]string, 0, n)
for _, cm := range p.Commits { for _, cm := range p.Commits {
short := cm.ID
if len(short) > 10 {
short = short[:10]
}
msg := strings.SplitN(cm.Message, "\n", 2)[0] msg := strings.SplitN(cm.Message, "\n", 2)[0]
lines = append(lines, fmt.Sprintf("• <%s|%s>", cm.URL, msg)) // 링크는 sha에만 — 메시지의 '>' 등이 링크를 중간에 끊지 않도록 평문은 이스케이프.
line := fmt.Sprintf("<%s|%s>: %s", cm.URL, short, escapeMrkdwn(msg))
if cm.Author != nil && cm.Author.Name != "" {
line += " - " + escapeMrkdwn(cm.Author.Name)
}
lines = append(lines, line)
}
body := head
if p.Repository.HTMLURL != "" {
body += "\n" + p.Repository.HTMLURL
} }
bodyText := strings.Join(lines, "\n") if len(lines) > 0 {
if bodyText == "" { body += "\n" + strings.Join(lines, "\n")
bodyText = "(커밋 정보 없음)"
} }
return channelNote(title, bodyText, ctx), true text := fmt.Sprintf("[%s:%s] %d new commit%s pushed by %s", repo, branch, n, plural, actor)
return pushNote(text, body), true
case "pull_request": case "pull_request":
if pr := p.PullRequest; pr != nil { if pr := p.PullRequest; pr != nil {
title := fmt.Sprintf("🔀 [%s] PR #%d %s", repo, pr.Number, withAction(p.Action)) title := fmt.Sprintf("🔀 [%s] PR #%d %s", repo, pr.Number, withAction(p.Action))
return channelNote(title, fmt.Sprintf("<%s|%s>", pr.HTMLURL, pr.Title), ctx), true return channelNote(title, fmt.Sprintf("<%s|%s>", pr.HTMLURL, escapeMrkdwn(pr.Title)), ctx), true
} }
case "issues": case "issues":
if iss := p.Issue; iss != nil { if iss := p.Issue; iss != nil {
title := fmt.Sprintf("📋 [%s] 이슈 #%d %s", repo, iss.Number, withAction(p.Action)) title := fmt.Sprintf("📋 [%s] 이슈 #%d %s", repo, iss.Number, withAction(p.Action))
return channelNote(title, fmt.Sprintf("<%s|%s>", iss.HTMLURL, iss.Title), ctx), true return channelNote(title, fmt.Sprintf("<%s|%s>", iss.HTMLURL, escapeMrkdwn(iss.Title)), ctx), true
} }
case "issue_comment": case "issue_comment":
@ -177,13 +208,13 @@ func BuildChannelMessage(event string, body []byte) (notify.Notification, bool)
url = p.Comment.HTMLURL url = p.Comment.HTMLURL
} }
title := fmt.Sprintf("💬 [%s] #%d 새 댓글", repo, iss.Number) title := fmt.Sprintf("💬 [%s] #%d 새 댓글", repo, iss.Number)
return channelNote(title, fmt.Sprintf("<%s|%s>", url, iss.Title), ctx), true return channelNote(title, fmt.Sprintf("<%s|%s>", url, escapeMrkdwn(iss.Title)), ctx), true
} }
case "pull_request_review": case "pull_request_review":
if pr := p.PullRequest; pr != nil { if pr := p.PullRequest; pr != nil {
title := fmt.Sprintf("📝 [%s] PR #%d 리뷰", repo, pr.Number) title := fmt.Sprintf("📝 [%s] PR #%d 리뷰", repo, pr.Number)
return channelNote(title, fmt.Sprintf("<%s|%s>", pr.HTMLURL, pr.Title), ctx), true return channelNote(title, fmt.Sprintf("<%s|%s>", pr.HTMLURL, escapeMrkdwn(pr.Title)), ctx), true
} }
} }
@ -196,6 +227,12 @@ func BuildChannelMessage(event string, body []byte) (notify.Notification, bool)
return channelNote(title, detail, ctx), true return channelNote(title, detail, ctx), true
} }
// escapeMrkdwn은 Slack mrkdwn에서 의미를 갖는 문자를 이스케이프한다.
// 특히 '>'를 그대로 두면 <url|텍스트> 링크가 중간에서 끊긴다.
func escapeMrkdwn(s string) string {
return strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;").Replace(s)
}
// withAction은 액션이 있으면 "(action)" 형태로 덧붙인다. // withAction은 액션이 있으면 "(action)" 형태로 덧붙인다.
func withAction(action string) string { func withAction(action string) string {
if action == "" { if action == "" {
@ -211,6 +248,16 @@ func channelNote(title, body, context string) notify.Notification {
} }
} }
// pushNote는 헤더+커밋 목록을 한 섹션에 담는다(Gitea 기본 메시지와 유사한 단순 형식).
func pushNote(text, body string) notify.Notification {
return notify.Notification{
Text: text,
Blocks: []notify.Block{
{"type": "section", "text": notify.Block{"type": "mrkdwn", "text": body}},
},
}
}
func handlePullRequest(p *payload, repo, sender string) []notify.Notification { func handlePullRequest(p *payload, repo, sender string) []notify.Notification {
pr := p.PullRequest pr := p.PullRequest
if pr == nil { if pr == nil {

7
internal/slack/slack.go

@ -90,7 +90,12 @@ func (c *Client) PostAlert(ctx context.Context, channel, text string, blocks []n
} }
func (c *Client) post(ctx context.Context, channel, text string, blocks []notify.Block) error { func (c *Client) post(ctx context.Context, channel, text string, blocks []notify.Block) error {
payload := map[string]any{"channel": channel} payload := map[string]any{
"channel": channel,
// 메시지 안의 링크/미디어 자동 미리보기(unfurl) 끔 — Block Kit 링크만 깔끔히 표시.
"unfurl_links": false,
"unfurl_media": false,
}
if text != "" { if text != "" {
payload["text"] = text payload["text"] = text
} }

Loading…
Cancel
Save