Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: node-study CI

on:
push:
branches: [main]
pull_request:
branches: [main]

concurrency:
group: node-study-ci-${{ github.ref }}
cancel-in-progress: true

jobs:
build-site:
name: 生成静态站点并校验
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: 安装 Node 20
uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm

- name: 安装构建依赖
# 用 ci 保证与 lock 文件一致(cache: npm 依赖 package-lock.json 存在)
run: npm ci --no-audit --no-fund

- name: 生成静态站点
run: npm run build

- name: 校验产物
run: |
set -eo pipefail
pages=$(find dist -name '*.html' | wc -l | tr -d ' ')
echo "HTML 页面数: $pages"
[ "$pages" -gt 300 ] || { echo "❌ 页面数异常(预期 >300)"; exit 1; }
[ -f dist/index.html ] || { echo "❌ 缺首页"; exit 1; }
[ -f dist/assets/style.css ] || { echo "❌ 缺样式"; exit 1; }
[ -f dist/chapter/01/index.html ] || { echo "❌ 缺章节页"; exit 1; }

# 章节数量应与 chapters 目录一致
chapters=$(ls -d chapters/*/ 2>/dev/null | wc -l | tr -d ' ')
built=$(ls -d dist/chapter/*/ 2>/dev/null | wc -l | tr -d ' ')
echo "源章节数: $chapters 已构建: $built"
[ "$chapters" = "$built" ] || { echo "❌ 章节数不匹配"; exit 1; }
echo "✅ 产物校验通过"

- name: 上传站点产物
uses: actions/upload-artifact@v4
with:
name: node-study-site
path: dist/
retention-days: 7
101 changes: 101 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: node-study Deploy

# 部署通道:阿里云 Cloud Assistant(aliyun ecs RunCommand),不用 SSH。
# 原因同 java-study / python-learning:GitHub Actions runner 属境外出口,
# 直连 47.93.15.207:22 会被阿里云上游静默丢包(安全组规则在该层之下,加了也无效)。

on:
push:
branches: [main]
paths-ignore:
- '**/*.md'
workflow_dispatch:

concurrency:
group: node-study-deploy
cancel-in-progress: false

jobs:
deploy:
name: 部署到 ECS
runs-on: ubuntu-latest
environment: production

steps:
- uses: actions/checkout@v4

- name: 记录版本
id: before
run: |
echo "commit=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
echo "message=$(git log -1 --pretty='%s')" >> "$GITHUB_OUTPUT"

- name: 通过 Cloud Assistant 下发部署脚本
env:
ALICLOUD_ACCESS_KEY_ID: ${{ secrets.ALIYUN_AK }}
ALICLOUD_ACCESS_KEY_SECRET: ${{ secrets.ALIYUN_SK }}
REGION: cn-beijing
INSTANCE_ID: i-2zecv4kgasiw24p97v0t
run: |
set -eo pipefail
curl -fsSL -o aliyun-cli.tgz https://aliyuncli.alicdn.com/aliyun-cli-linux-latest-amd64.tgz
tar -xzf aliyun-cli.tgz
install -m 0755 aliyun /usr/local/bin/aliyun

aliyun configure set --mode AK --profile ecs-deploy \
--region "$REGION" \
--access-key-id "$ALICLOUD_ACCESS_KEY_ID" \
--access-key-secret "$ALICLOUD_ACCESS_KEY_SECRET" >/dev/null

IID=$(aliyun ecs RunCommand --profile ecs-deploy --region "$REGION" \
--Type RunShellScript --Timeout 900 \
--InstanceId.1 "$INSTANCE_ID" \
--ContentEncoding Base64 \
--CommandContent "$(base64 -w0 < deploy/deploy-node.sh)" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['InvokeId'])")
echo "InvokeId=$IID"
echo "::add-mask::$IID"

while :; do
STATUS=$(aliyun ecs DescribeInvocationResults --profile ecs-deploy \
--region "$REGION" --InvokeId "$IID" | python3 deploy/ca_result.py status)
echo " 状态: $STATUS"
case "$STATUS" in Running|Pending) sleep 10 ;; *) break ;; esac
done

# 用仓库内脚本解析,避免内联多行 Python 顶断 YAML 缩进
aliyun ecs DescribeInvocationResults --profile ecs-deploy \
--region "$REGION" --InvokeId "$IID" | python3 deploy/ca_result.py output \
| tee /tmp/ca-output.log

grep -q "NODE-STUDY DEPLOY DONE" /tmp/ca-output.log || {
echo "::error::部署未完成"; exit 1; }

- name: 部署结果
if: always()
run: |
{
echo "### 部署完成"
echo ""
echo "- commit: \`${{ steps.before.outputs.commit }}\` — ${{ steps.before.outputs.message }}"
echo "- 站点: http://47.93.15.207:8087/"
echo ""
echo '```'
tail -25 /tmp/ca-output.log 2>/dev/null || echo '(无输出)'
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

- name: 外网复验
run: |
set -eo pipefail
sleep 3
echo -n "首页 : "
curl -sS --max-time 15 -o /dev/null -w '%{http_code}\n' http://47.93.15.207:8087/
echo -n "章节01 : "
curl -sS --max-time 15 -o /dev/null -w '%{http_code}\n' http://47.93.15.207:8087/chapter/01/index.html
echo -n "样式 : "
curl -sS --max-time 15 -o /dev/null -w '%{http_code}\n' http://47.93.15.207:8087/assets/style.css
# 首页应含 60 个章节链接
n=$(curl -sS --max-time 15 http://47.93.15.207:8087/ | grep -o 'href="/chapter/[0-9]*/index.html"' | sort -u | wc -l | tr -d ' ')
echo "章节链接数: $n"
[ "$n" -ge 55 ] || { echo "❌ 章节链接异常"; exit 1; }
46 changes: 46 additions & 0 deletions deploy/ca_result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""解析 `aliyun ecs DescribeInvocationResults` 的 JSON 输出。

用法:
aliyun ecs DescribeInvocationResults ... | python3 ca_result.py status
aliyun ecs DescribeInvocationResults ... | python3 ca_result.py output
aliyun ecs DescribeInvocationResults ... | python3 ca_result.py both

单独放成文件而不是内联在 workflow 的 run: | 块里,是因为内联多行 Python
必须顶格写,会顶断 YAML 的块标量缩进。
"""
from __future__ import annotations

import base64
import json
import sys


def first_result(payload: dict) -> dict:
invocation = payload["Invocation"]["InvocationResults"]
results = invocation["InvocationResult"]
if not results:
raise SystemExit("DescribeInvocationResults 返回空结果")
return results[0]


def main() -> int:
if len(sys.argv) < 2 or sys.argv[1] not in {"status", "output", "both"}:
print(__doc__, file=sys.stderr)
return 2

mode = sys.argv[1]
result = first_result(json.load(sys.stdin))

if mode in {"status", "both"}:
print(result.get("InvocationStatus", ""))

if mode in {"output", "both"}:
raw = result.get("Output", "") or ""
sys.stdout.write(base64.b64decode(raw).decode(errors="replace"))

return 0


if __name__ == "__main__":
raise SystemExit(main())
85 changes: 85 additions & 0 deletions deploy/deploy-node.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/bin/bash
# node-study 静态站点部署脚本(在 ECS 上执行)
#
# node-study 本就无后端无登录,这里只做:拉源码 → 构建静态站 → 确保 vhost 存在 → reload nginx
set -eo pipefail

SITE_DIR='/www/wwwroot/node-study'
VHOST='/www/server/panel/vhost/nginx/node-study.conf'
PORT=8087

echo "=== 1. 下载源码 ==="
TAR=/tmp/_node-study.tar.gz
rm -f "$TAR"
http=$(curl -sSL -o "$TAR" -w '%{http_code}' --max-time 180 \
"https://codeload.github.com/webxiongda/node-study/tar.gz/refs/heads/main")
[ "$http" = "200" ] || { echo "❌ 下载失败 HTTP=$http"; exit 1; }
tar -tzf "$TAR" >/dev/null && echo "tar OK ($(du -h $TAR | cut -f1))"

echo "=== 2. 替换源码(dist 会重建,node_modules 可留)==="
cd "$SITE_DIR" 2>/dev/null || { mkdir -p "$SITE_DIR"; cd "$SITE_DIR"; }
KEEP=/tmp/_node_keep
rm -rf "$KEEP"; mkdir -p "$KEEP"
[ -d node_modules ] && mv node_modules "$KEEP/" || true
rm -rf chapters nodejs-learning 60-days-nodejs dist
tar -xzf "$TAR" --strip-components=1
rm -f "$TAR"
[ -d "$KEEP/node_modules" ] && mv "$KEEP/node_modules" ./ || true
rm -rf "$KEEP"

echo "=== 3. 安装构建依赖并生成站点 ==="
export PATH=/usr/local/bin:$PATH
npm ci --no-audit --no-fund 2>&1 | tail -2
npm run build 2>&1 | tail -4
echo "HTML 页面数: $(find dist -name '*.html' | wc -l) dist 大小: $(du -sh dist | cut -f1)"

echo "=== 4. 确保 nginx vhost 存在 ===="
if ! grep -q "listen ${PORT};" "$VHOST" 2>/dev/null; then
echo "创建 vhost -> ${VHOST}"
cat > "$VHOST" << NGINX_EOF
server {
listen ${PORT};
server_name _;

root ${SITE_DIR}/dist;
index index.html;

access_log /www/wwwlogs/node-study.access.log;
error_log /www/wwwlogs/node-study.error.log;

location / {
try_files \$uri \$uri/ /index.html;
}

location /raw/ {
default_type text/plain;
charset utf-8;
}

location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|webp|woff2?)\$ {
expires 7d;
add_header Cache-Control "public, max-age=604800";
try_files \$uri =404;
}
}
NGINX_EOF
else
echo "vhost 已存在,跳过创建"
fi

echo "=== 5. 防火墙放行 ${PORT}(仅首次需要,幂等)==="
if ! firewall-cmd --list-ports 2>/dev/null | tr ' ' '\n' | grep -qx "${PORT}/tcp"; then
firewall-cmd --add-port=${PORT}/tcp --permanent >/dev/null 2>&1 && firewall-cmd --reload >/dev/null 2>&1 && echo "已放行 ${PORT}"
else
echo "${PORT} 已放行"
fi

echo "=== 6. 校验并重载 nginx ==="
nginx -t && nginx -s reload && echo "NGINX RELOAD OK"

echo "=== 7. 本机自测 ==="
sleep 2
for p in "/" "/chapter/01/index.html" "/assets/style.css"; do
printf " %-28s %s\n" "$p" "$(curl -s -m 8 -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PORT}$p")"
done
echo "=== NODE-STUDY DEPLOY DONE ==="
39 changes: 39 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "node-study-site",
"version": "1.0.0",
"private": true,
"type": "module",
"description": "Node.js 全栈学习路线 · 静态站点生成器(markdown → HTML)",
"scripts": {
"build": "node build-site.mjs"
},
"devDependencies": {
"highlight.js": "^11.11.1",
"marked": "^14.1.4"
}
}
Loading