mirror of
https://github.com/nocobase/nocobase.git
synced 2026-10-16 15:07:08 +08:00
175 lines
6.3 KiB
Bash
Executable File
175 lines
6.3 KiB
Bash
Executable File
#!/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"
|