Skip to content

Commit fce5086

Browse files
committed
Merge remote-tracking branch 'remote/main'
2 parents ecabc6c + 9914027 commit fce5086

File tree

5 files changed

+323
-6
lines changed

5 files changed

+323
-6
lines changed
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
name: Deploy Static Sites
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
products_dir:
7+
description: Root directory that contains product folders.
8+
type: string
9+
required: false
10+
default: products
11+
deploy_path:
12+
description: Remote deploy base path.
13+
type: string
14+
required: false
15+
default: /var/www/static-sites
16+
force_deploy:
17+
description: Deploy all product folders, ignoring git diff.
18+
type: boolean
19+
required: false
20+
default: false
21+
max_parallel:
22+
description: Max parallel rsync jobs.
23+
type: number
24+
required: false
25+
default: 3
26+
reload_nginx:
27+
description: Reload nginx after deploy.
28+
type: boolean
29+
required: false
30+
default: true
31+
nginx_container:
32+
description: Docker container name that runs nginx.
33+
type: string
34+
required: false
35+
default: static-sites
36+
secrets:
37+
SERVER_HOST:
38+
required: true
39+
SERVER_USER:
40+
required: true
41+
SERVER_PASS:
42+
required: true
43+
44+
permissions:
45+
contents: read
46+
47+
jobs:
48+
detect-changes:
49+
runs-on: ubuntu-latest
50+
outputs:
51+
changed_products: ${{ steps.changes.outputs.products }}
52+
has_changes: ${{ steps.changes.outputs.has_changes }}
53+
steps:
54+
- uses: actions/checkout@v4
55+
with:
56+
fetch-depth: 0
57+
58+
- name: Detect changed products
59+
id: changes
60+
shell: bash
61+
run: |
62+
set -euo pipefail
63+
64+
PRODUCTS_DIR="${{ inputs.products_dir }}"
65+
PRODUCTS_DIR="${PRODUCTS_DIR%/}"
66+
67+
if ! [[ "$PRODUCTS_DIR" =~ ^[a-zA-Z0-9._/-]+$ ]]; then
68+
echo "Invalid products_dir: $PRODUCTS_DIR"
69+
exit 1
70+
fi
71+
if [[ "$PRODUCTS_DIR" == -* ]]; then
72+
echo "products_dir must not start with '-'"
73+
exit 1
74+
fi
75+
76+
if [ ! -d "$PRODUCTS_DIR" ]; then
77+
echo "Products dir not found: $PRODUCTS_DIR"
78+
echo 'products=[]' >> "$GITHUB_OUTPUT"
79+
echo 'has_changes=false' >> "$GITHUB_OUTPUT"
80+
exit 0
81+
fi
82+
83+
if [ "${{ inputs.force_deploy }}" = "true" ]; then
84+
PRODUCTS=$(find "$PRODUCTS_DIR" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | jq -R -s -c 'split("\n")[:-1]')
85+
else
86+
EVENT_NAME="${{ github.event_name }}"
87+
BEFORE_SHA="${{ github.event.before }}"
88+
AFTER_SHA="${{ github.sha }}"
89+
90+
if [ "$EVENT_NAME" = "push" ] && [ -n "$BEFORE_SHA" ] && [ "$BEFORE_SHA" != "0000000000000000000000000000000000000000" ]; then
91+
CHANGED_FILES=$(git diff --name-only "$BEFORE_SHA" "$AFTER_SHA" -- "$PRODUCTS_DIR/" || true)
92+
elif git rev-parse --verify HEAD~1 >/dev/null 2>&1; then
93+
CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD -- "$PRODUCTS_DIR/" || true)
94+
else
95+
CHANGED_FILES=""
96+
fi
97+
98+
if [ -z "${CHANGED_FILES:-}" ]; then
99+
PRODUCTS='[]'
100+
else
101+
PRODUCTS=$(echo "$CHANGED_FILES" | awk -v dir="$PRODUCTS_DIR" '
102+
index($0, dir"/")==1 {
103+
rest = substr($0, length(dir)+2)
104+
split(rest, parts, "/")
105+
if (parts[1] != "") print parts[1]
106+
}' | sort -u | jq -R -s -c 'split("\n")[:-1]')
107+
fi
108+
fi
109+
110+
PRODUCTS=$(echo "$PRODUCTS" | jq -r '.[]' | while IFS= read -r product; do
111+
[ -z "$product" ] && continue
112+
if [[ "$product" =~ ^[a-zA-Z0-9._][a-zA-Z0-9._-]*$ ]] && [ -d "$PRODUCTS_DIR/$product" ]; then
113+
echo "$product"
114+
fi
115+
done | jq -R -s -c 'split("\n")[:-1]')
116+
117+
echo "products=$PRODUCTS" >> "$GITHUB_OUTPUT"
118+
119+
PRODUCT_COUNT=$(echo "$PRODUCTS" | jq 'length')
120+
if [ "$PRODUCT_COUNT" -eq 0 ]; then
121+
echo "has_changes=false" >> "$GITHUB_OUTPUT"
122+
else
123+
echo "has_changes=true" >> "$GITHUB_OUTPUT"
124+
fi
125+
126+
echo "Changed products: $PRODUCTS"
127+
128+
deploy:
129+
needs: detect-changes
130+
if: needs.detect-changes.outputs.has_changes == 'true'
131+
runs-on: ubuntu-latest
132+
strategy:
133+
matrix:
134+
product: ${{ fromJson(needs.detect-changes.outputs.changed_products) }}
135+
max-parallel: ${{ inputs.max_parallel }}
136+
137+
steps:
138+
- uses: actions/checkout@v4
139+
140+
- name: Validate inputs
141+
shell: bash
142+
env:
143+
PRODUCTS_DIR: ${{ inputs.products_dir }}
144+
DEPLOY_PATH: ${{ inputs.deploy_path }}
145+
PRODUCT: ${{ matrix.product }}
146+
run: |
147+
set -euo pipefail
148+
149+
PRODUCTS_DIR="${PRODUCTS_DIR%/}"
150+
DEPLOY_PATH="${DEPLOY_PATH%/}"
151+
152+
fail() { echo "$1" >&2; exit 1; }
153+
154+
[[ -n "$PRODUCTS_DIR" ]] || fail "products_dir is empty"
155+
[[ "$PRODUCTS_DIR" =~ ^[a-zA-Z0-9._/-]+$ ]] || fail "Invalid products_dir: $PRODUCTS_DIR"
156+
[[ "$PRODUCTS_DIR" != -* ]] || fail "products_dir must not start with '-'"
157+
[[ -n "$DEPLOY_PATH" ]] || fail "deploy_path is empty"
158+
[[ "$DEPLOY_PATH" =~ ^[a-zA-Z0-9._/-]+$ ]] || fail "Invalid deploy_path: $DEPLOY_PATH"
159+
[[ "$DEPLOY_PATH" != -* ]] || fail "deploy_path must not start with '-'"
160+
[[ -n "$PRODUCT" ]] || fail "product is empty"
161+
[[ "$PRODUCT" =~ ^[a-zA-Z0-9._][a-zA-Z0-9._-]*$ ]] || fail "Invalid product: $PRODUCT"
162+
163+
echo "PRODUCTS_DIR=$PRODUCTS_DIR" >> "$GITHUB_ENV"
164+
echo "DEPLOY_PATH=$DEPLOY_PATH" >> "$GITHUB_ENV"
165+
echo "PRODUCT=$PRODUCT" >> "$GITHUB_ENV"
166+
167+
if [ ! -d "$PRODUCTS_DIR/$PRODUCT" ]; then
168+
echo "Source directory not found: $PRODUCTS_DIR/$PRODUCT (skipping)"
169+
echo "SKIP_DEPLOY=true" >> "$GITHUB_ENV"
170+
exit 0
171+
fi
172+
173+
- name: Install sshpass
174+
run: |
175+
sudo apt-get update
176+
sudo apt-get install -y sshpass
177+
178+
- name: Add server to known hosts
179+
run: |
180+
mkdir -p ~/.ssh
181+
ssh-keyscan -H "${{ secrets.SERVER_HOST }}" >> ~/.ssh/known_hosts
182+
183+
- name: Deploy ${{ matrix.product }}
184+
env:
185+
SSHPASS: ${{ secrets.SERVER_PASS }}
186+
run: |
187+
set -euo pipefail
188+
189+
if [ "${SKIP_DEPLOY:-false}" = "true" ]; then
190+
echo "Skipping deploy for $PRODUCT"
191+
exit 0
192+
fi
193+
194+
echo "Deploying product: $PRODUCT"
195+
sshpass -e rsync -avz --delete \
196+
--exclude='.git*' \
197+
-e "ssh" \
198+
-- \
199+
"$PRODUCTS_DIR/$PRODUCT/" \
200+
"${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}:$DEPLOY_PATH/$PRODUCT/"
201+
202+
reload-nginx:
203+
needs: deploy
204+
if: ${{ inputs.reload_nginx }}
205+
runs-on: ubuntu-latest
206+
steps:
207+
- name: Install sshpass
208+
run: |
209+
sudo apt-get update
210+
sudo apt-get install -y sshpass
211+
212+
- name: Add server to known hosts
213+
run: |
214+
mkdir -p ~/.ssh
215+
ssh-keyscan -H "${{ secrets.SERVER_HOST }}" >> ~/.ssh/known_hosts
216+
217+
- name: Validate inputs
218+
shell: bash
219+
env:
220+
NGINX_CONTAINER: ${{ inputs.nginx_container }}
221+
run: |
222+
set -euo pipefail
223+
224+
fail() { echo "$1" >&2; exit 1; }
225+
226+
[[ -n "$NGINX_CONTAINER" ]] || fail "nginx_container is empty"
227+
[[ "$NGINX_CONTAINER" =~ ^[a-zA-Z0-9][a-zA-Z0-9_.-]*$ ]] || fail "Invalid nginx_container: $NGINX_CONTAINER"
228+
echo "NGINX_CONTAINER=$NGINX_CONTAINER" >> "$GITHUB_ENV"
229+
230+
- name: Reload Nginx
231+
env:
232+
SSHPASS: ${{ secrets.SERVER_PASS }}
233+
run: |
234+
set -euo pipefail
235+
236+
sshpass -e ssh \
237+
"${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" \
238+
"docker exec $NGINX_CONTAINER nginx -s reload || true"

README.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
|---|---|---|
99
| **Discord Notification** | 发送中文格式的代码提交统计通知到 Discord | [查看文档](docs/discord-notify.md) |
1010

11+
## 🔁 可用 Reusable Workflows 列表
12+
13+
| Workflow 名称 | 描述 | 文档 |
14+
|---|---|---|
15+
| **Deploy Static Sites** |`products/*` 静态站点增量部署到服务器并可选 reload nginx | [查看文档](docs/deploy-static-sites.md) |
16+
1117
## 🚀 如何在其他项目中使用
1218

1319
### 🤖 1. Agent 辅助集成 (推荐)
@@ -48,7 +54,7 @@ steps:
4854
fetch-depth: 2 # 统计变更需要历史记录
4955

5056
- name: 发送 Discord 通知
51-
uses: Time-Machine-Lab/TML-Github-Actions/actions/discord-github-notify@main
57+
uses: Time-Machine-Lab/TML-Github_Actions/actions/discord-github-notify@main
5258
with:
5359
webhook_url: ${{ secrets.DISCORD_WEBHOOK_URL }}
5460
```
@@ -61,10 +67,14 @@ steps:
6167

6268
```yaml
6369
jobs:
64-
build:
65-
uses: Time-Machine-Lab/TML-Github-Actions/.github/workflows/maven-build.yml@main
70+
deploy:
71+
uses: Time-Machine-Lab/TML-Github_Actions/.github/workflows/deploy-static-sites.yml@main
6672
with:
67-
java-version: '17'
73+
force_deploy: false
74+
secrets:
75+
SERVER_HOST: ${{ secrets.SERVER_HOST }}
76+
SERVER_USER: ${{ secrets.SERVER_USER }}
77+
SERVER_PASS: ${{ secrets.SERVER_PASS }}
6878
```
6979

7080
## 目录结构

SKILLS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: github workflow actions
3-
description: This skill provides guidelines for developing GitHub Actions and Reusable Workflows in the TML-Github-Actions repository. Use it when creating new actions, debugging workflows, or writing documentation to ensure compliance with project standards.
3+
description: This skill provides guidelines for developing GitHub Actions and Reusable Workflows in the TML-Github_Actions repository. Use it when creating new actions, debugging workflows, or writing documentation to ensure compliance with project standards.
44
metadata:
55
author: tml
66
---

docs/deploy-static-sites.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Deploy Static Sites Reusable Workflow
2+
3+
该 Reusable Workflow 用于将 `products/<product>/` 下的静态站点内容通过 `rsync` 部署到远程服务器,并在部署完成后可选地执行 `nginx -s reload`
4+
5+
## 必要 Secrets
6+
7+
**调用该 Workflow 的仓库**中配置以下 Secrets(Settings -> Secrets and variables -> Actions):
8+
9+
| 名称 | 描述 |
10+
|---|---|
11+
| `SERVER_HOST` | 服务器地址(IP/域名) |
12+
| `SERVER_USER` | SSH 用户名 |
13+
| `SERVER_PASS` | SSH 密码(用于 `sshpass`|
14+
15+
## Inputs
16+
17+
| 参数 | 类型 | 默认值 | 描述 |
18+
|---|---|---|---|
19+
| `products_dir` | string | `products` | 产品根目录(目录下每个子目录视为一个 product) |
20+
| `deploy_path` | string | `/var/www/static-sites` | 远程部署根路径 |
21+
| `force_deploy` | boolean | `false` |`true` 时部署 `products_dir` 下所有产品 |
22+
| `max_parallel` | number | `3` | 并行部署数量 |
23+
| `reload_nginx` | boolean | `true` | 是否在部署后 reload nginx |
24+
| `nginx_container` | string | `static-sites` | nginx 所在 Docker 容器名 |
25+
26+
## 使用示例(在 TML-site 中)
27+
28+
`TML-site` 仓库创建/修改 `.github/workflows/deploy.yml`
29+
30+
```yaml
31+
name: Deploy Static Sites
32+
33+
on:
34+
push:
35+
branches: [master]
36+
paths:
37+
- 'products/**'
38+
workflow_dispatch:
39+
inputs:
40+
force_deploy:
41+
description: Force deploy all products
42+
type: boolean
43+
required: false
44+
default: false
45+
46+
jobs:
47+
deploy:
48+
uses: Time-Machine-Lab/TML-Github_Actions/.github/workflows/deploy-static-sites.yml@main
49+
with:
50+
force_deploy: ${{ github.event_name == 'workflow_dispatch' && inputs.force_deploy }}
51+
secrets:
52+
SERVER_HOST: ${{ secrets.SERVER_HOST }}
53+
SERVER_USER: ${{ secrets.SERVER_USER }}
54+
SERVER_PASS: ${{ secrets.SERVER_PASS }}
55+
```
56+
57+
## 工作原理
58+
59+
- 默认:
60+
- `push` 事件:比较本次推送范围(`github.event.before..github.sha`)在 `products_dir/` 下的变更,提取受影响的 `<product>` 并按矩阵并行部署。
61+
- 其他事件:若存在上一提交则使用 `HEAD~1..HEAD` 作为兜底 diff 范围。
62+
- `force_deploy: true`:忽略 diff,部署 `products_dir` 下所有产品目录。
63+
64+
## 注意事项
65+
66+
- 该 Workflow 使用 `sshpass` 进行密码登录;如需更安全的 SSH Key 方式,我可以继续帮你改造。
67+
- 远程服务器需安装 `rsync`,并允许 `SERVER_USER` 写入 `deploy_path`。
68+
- `reload_nginx` 会在远程服务器执行:`docker exec <nginx_container> nginx -s reload || true`,请确保容器名与 nginx 命令符合实际环境。
69+
- 部署阶段会将服务器写入 `~/.ssh/known_hosts` 并启用 Host Key 校验;如服务器 Host Key 变更,需要更新对应配置。

docs/discord-notify.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
fetch-depth: 2 # 必须设置为 2 或 0,以便统计 Push 事件的变更
4444

4545
- name: Send Discord Notification
46-
uses: Time-Machine-Lab/TML-Github-Actions/actions/discord-github-notify@main
46+
uses: Time-Machine-Lab/TML-Github_Actions/actions/discord-github-notify@main
4747
with:
4848
webhook_url: ${{ secrets.DISCORD_WEBHOOK_URL }}
4949
```

0 commit comments

Comments
 (0)