From edd25cff75b13cd00553cf871098d90a63a4dc3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dhji=28=EC=A7=80=EB=8C=80=ED=95=9C=29?= Date: Thu, 11 Jun 2026 15:54:52 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20slack=20-=20gitea=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/gitea/gitea.go | 69 ++++++++++++++++++++++++++++++++++------- internal/slack/slack.go | 7 ++++- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/internal/gitea/gitea.go b/internal/gitea/gitea.go index 875ab45..8c40877 100644 --- a/internal/gitea/gitea.go +++ b/internal/gitea/gitea.go @@ -14,6 +14,7 @@ type user struct { Login string `json:"login"` Username string `json:"username"` Email string `json:"email"` + Name string `json:"name"` // commit author/committer ํ‘œ์‹œ ์ด๋ฆ„ } func (u *user) login() string { @@ -63,8 +64,10 @@ type review struct { } type commitInfo struct { + ID string `json:"id"` Message string `json:"message"` URL string `json:"url"` + Author *user `json:"author"` } type payload struct { @@ -72,6 +75,7 @@ type payload struct { Number int `json:"number"` Repository struct { FullName string `json:"full_name"` + HTMLURL string `json:"html_url"` } `json:"repository"` Sender *user `json:"sender"` PullRequest *pullRequest `json:"pull_request"` @@ -146,28 +150,55 @@ func BuildChannelMessage(event string, body []byte) (notify.Notification, bool) switch event { case "push": branch := strings.TrimPrefix(p.Ref, "refs/heads/") - title := fmt.Sprintf("๐Ÿ“ค [%s] %s์— ์ปค๋ฐ‹ %d๊ฐœ push", repo, branch, len(p.Commits)) - lines := make([]string, 0, len(p.Commits)) + n := 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) + + // ์ปค๋ฐ‹ ์ค„: : ๋ฉ”์‹œ์ง€ - ์ž‘์„ฑ์ž๋ช… + lines := make([]string, 0, n) for _, cm := range p.Commits { + short := cm.ID + if len(short) > 10 { + short = short[:10] + } 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 bodyText == "" { - bodyText = "(์ปค๋ฐ‹ ์ •๋ณด ์—†์Œ)" + if len(lines) > 0 { + body += "\n" + strings.Join(lines, "\n") } - 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": if pr := p.PullRequest; pr != nil { 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": if iss := p.Issue; iss != nil { 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": @@ -177,13 +208,13 @@ func BuildChannelMessage(event string, body []byte) (notify.Notification, bool) url = p.Comment.HTMLURL } 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": if pr := p.PullRequest; pr != nil { 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 } +// escapeMrkdwn์€ Slack mrkdwn์—์„œ ์˜๋ฏธ๋ฅผ ๊ฐ–๋Š” ๋ฌธ์ž๋ฅผ ์ด์Šค์ผ€์ดํ”„ํ•œ๋‹ค. +// ํŠนํžˆ '>'๋ฅผ ๊ทธ๋Œ€๋กœ ๋‘๋ฉด ๋งํฌ๊ฐ€ ์ค‘๊ฐ„์—์„œ ๋Š๊ธด๋‹ค. +func escapeMrkdwn(s string) string { + return strings.NewReplacer("&", "&", "<", "<", ">", ">").Replace(s) +} + // withAction์€ ์•ก์…˜์ด ์žˆ์œผ๋ฉด "(action)" ํ˜•ํƒœ๋กœ ๋ง๋ถ™์ธ๋‹ค. func withAction(action string) string { 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 { pr := p.PullRequest if pr == nil { diff --git a/internal/slack/slack.go b/internal/slack/slack.go index 058941c..14ae994 100644 --- a/internal/slack/slack.go +++ b/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 { - payload := map[string]any{"channel": channel} + payload := map[string]any{ + "channel": channel, + // ๋ฉ”์‹œ์ง€ ์•ˆ์˜ ๋งํฌ/๋ฏธ๋””์–ด ์ž๋™ ๋ฏธ๋ฆฌ๋ณด๊ธฐ(unfurl) ๋” โ€” Block Kit ๋งํฌ๋งŒ ๊น”๋”ํžˆ ํ‘œ์‹œ. + "unfurl_links": false, + "unfurl_media": false, + } if text != "" { payload["text"] = text }