Files
nocobase/scripts/feishu-notify.sh
T

175 lines
6.3 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Send a Feishu interactive card notification.
#
# Required environment variables:
# WEBHOOK_URL - Feishu bot webhook URL
# TITLE - Card title
# STATUS - "success" or "failure"
# WORKFLOW_URL - Link to the GitHub Actions run
#
# Optional environment variables:
# STATUS_TEXT_SUCCESS - Custom success text (defaults to "构建成功")
# STATUS_TEXT_FAILURE - Custom failure text (defaults to "构建失败")
# CONTENT - Card body content in Markdown (defaults to auto-generated)
# BUILD_TIME - Build timestamp (defaults to current time in Asia/Shanghai)
# ACTOR - Who triggered the run; appended to the footer when set
# REPOSITORY - owner/repo; appended to the footer when set
# REGISTRY_URL - Package registry; adds a button when set
# RELEASE_URL - GitHub Releases page; adds the primary button when set
# TABLE_JSON - `[{"pkg":…,"ver":…}]`; renders a real table under CONTENT
#
# The card uses schema 2.0. Card 1.0 could not render a package list: its
# `lark_md` has no table syntax, so a Markdown table arrived as literal pipes.
# 2.0 has a `table` element that takes structured rows, which is what TABLE_JSON
# feeds. Note that 2.0 drops the `action` element — buttons live in a
# `column_set` and take `behaviors` instead of a bare `url`.
set -euo pipefail
: "${WEBHOOK_URL:?WEBHOOK_URL is required}"
: "${TITLE:?TITLE is required}"
: "${STATUS:?STATUS must be 'success' or 'failure'}"
: "${WORKFLOW_URL:?WORKFLOW_URL is required}"
BUILD_TIME="${BUILD_TIME:-$(TZ="Asia/Shanghai" date +"%Y-%m-%d %H:%M:%S")}"
if [[ "$STATUS" == "success" ]]; then
HEADER_TEMPLATE="green"
STATUS_TEXT="${STATUS_TEXT_SUCCESS:-构建成功}"
STATUS_EMOJI="✅"
else
HEADER_TEMPLATE="red"
STATUS_TEXT="${STATUS_TEXT_FAILURE:-构建失败}"
STATUS_EMOJI="❌"
fi
# 状态、时间、触发者、仓库这几项每条通知都该有,但自定义 CONTENT 时
# 容易漏掉,所以在脚本里统一追加,调用方只负责写正文。
#
# 冒号写在 `**` 外面:CommonMark 规定右侧的 `**` 前面是标点、后面紧跟非空白
# 字符时不构成有效的加粗结束标记,`**状态:**✅` 因此会原样显示星号。卡片 1.0
# 的 lark_md 解析宽松,看不出问题;2.0 的 markdown 严格按 CommonMark 走。
#
# 这里必须用真换行而不是字面的 \n:CONTENT 从 workflow 的 YAML 块标量
# 传进来时带的就是真换行,jq --arg 会把两者分别转义成 \n 和 \\n,后者
# 在飞书里原样显示成反斜杠加 n。
FOOTER="**状态**:${STATUS_EMOJI} ${STATUS_TEXT}
**时间**:${BUILD_TIME}"
[[ -n "${ACTOR:-}" ]] && FOOTER="${FOOTER}
**触发者**:${ACTOR}"
[[ -n "${REPOSITORY:-}" ]] && FOOTER="${FOOTER}
**仓库**:${REPOSITORY}"
# 表格是独立元素,不能拼进 Markdown 正文,所以正文和页脚分开传给 jq,
# 由它按 CONTENT -> table -> hr -> FOOTER 的顺序组装。
TABLE_JSON="${TABLE_JSON:-}"
if [[ -n "$TABLE_JSON" ]] && ! jq -e 'type == "array"' <<< "$TABLE_JSON" > /dev/null 2>&1; then
echo "TABLE_JSON must be a JSON array; ignoring it" >&2
TABLE_JSON=""
fi
# 空数组和没传是一回事,渲染出来只会是一个空表头。
if [[ -n "$TABLE_JSON" ]] && [[ "$(jq -r 'length' <<< "$TABLE_JSON")" == "0" ]]; then
TABLE_JSON=""
fi
PAYLOAD=$(jq -n \
--arg title "$TITLE" \
--arg template "$HEADER_TEMPLATE" \
--arg content "${CONTENT:-}" \
--arg footer "$FOOTER" \
--arg url "$WORKFLOW_URL" \
--arg registry "${REGISTRY_URL:-}" \
--arg release "${RELEASE_URL:-}" \
--argjson rows "${TABLE_JSON:-null}" \
'
def button($label; $link; $type):
{
tag: "column",
width: "auto",
elements: [
{
tag: "button",
text: { tag: "plain_text", content: $label },
type: $type,
width: "default",
behaviors: [ { type: "open_url", default_url: $link } ]
}
]
};
{
msg_type: "interactive",
card: {
schema: "2.0",
header: {
title: { tag: "plain_text", content: $title },
template: $template
},
body: {
elements: (
(if $content == "" then [] else [
{ tag: "markdown", content: $content }
] end)
+ (if $rows == null then [] else [
{
tag: "table",
page_size: 20,
row_height: "low",
header_style: {
background_style: "grey",
bold: true,
text_size: "normal"
},
columns: [
{ name: "pkg", display_name: "Package", data_type: "text" },
{ name: "ver", display_name: "版本", data_type: "text" }
],
rows: $rows
}
] end)
+ [
{ tag: "hr" },
{ tag: "markdown", content: $footer },
{
tag: "column_set",
horizontal_spacing: "8px",
columns: (
# Release 详情放第一位并设为 primary:发版通知里它是最常点的。
(if $release == "" then [] else [
button("查看 Release 详情"; $release; "primary")
] end)
+ [
button(
"查看构建详情";
$url;
(if $release == "" then "primary" else "default" end)
)
]
+ (if $registry == "" then [] else [
button("打开 Registry"; $registry; "default")
] end)
)
}
]
)
}
}
}')
RESPONSE_BODY=$(mktemp)
HTTP_CODE=$(curl -s -o "$RESPONSE_BODY" -w "%{http_code}" -X POST "$WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "$PAYLOAD")
# 飞书对格式错误的卡片返回 HTTP 200 加一个非零的 code,只看状态码会把
# 「消息没发出去」当成成功。
RESPONSE_CODE=$(jq -r '.code // 0' < "$RESPONSE_BODY" 2>/dev/null || echo 0)
if [[ "$HTTP_CODE" -ge 200 && "$HTTP_CODE" -lt 300 && "$RESPONSE_CODE" == "0" ]]; then
echo "Feishu notification sent successfully (HTTP ${HTTP_CODE})"
else
echo "Failed to send Feishu notification (HTTP ${HTTP_CODE})" >&2
echo "Response: $(cat "$RESPONSE_BODY")" >&2
rm -f "$RESPONSE_BODY"
exit 1
fi
rm -f "$RESPONSE_BODY"