跳转到主要内容

部署与 CI/CD

当前 Astro 站点的 CI/CD 配置说明

记录当前项目通过 Gitee Go 构建、下发压缩包、Linux 自动解压并由 Nginx 托管静态页面的完整流程。

  • CI/CD
  • Gitee Go
  • Linux
  • Nginx
  • Astro

这篇文档记录当前站点的实际 CI/CD 配置。目标是本地提交代码后,Gitee 自动构建 Astro 静态产物并同步到 Linux 服务器,再由服务器自动解压到 Nginx 网站目录。

整体链路

当前部署链路是:

本地 git push origin main
-> Gitee Go Push 触发流水线
-> Node 24 环境通过 pnpm 安装依赖并执行 pnpm run build
-> 把 ./dist 打成 output.tar.gz
-> Gitee Go 主机部署把压缩包下发到服务器
-> 服务器执行 /opt/hal-astro/deploy.sh
-> 解压 dist 并 rsync 到 /usr/share/nginx/html
-> Nginx 直接托管静态页面

服务器上还有一层 systemd.path 监听兜底:只要 Gitee Go 更新了 output.tar.gz,系统也会自动触发部署脚本。

Gitee 流水线配置

流水线文件在仓库的 .workflow/ 目录下,当前为:

.workflow/流水线-202607132148.yml

文件名带时间戳,在 Gitee 上重新保存流水线后会生成新的 yml,以 .workflow/ 目录下实际文件为准。

当前流水线分为两个阶段:

  1. 构建:使用 Node 24.13.0,通过 corepack 激活 pnpm,安装依赖并构建 Astro。
  2. 部署:把构建产物下发到服务器,并执行部署脚本。

关键配置如下,已与仓库 .workflow/流水线-202607132148.yml 核对一致,省略了部分展示名和策略等元数据:

stages:
  - name: 构建
    displayName: 构建
    steps:
      - step: build@nodejs
        displayName: Nodejs 构建
        nodeVersion: 24.13.0
        commands:
          - npm config set registry https://registry.npmmirror.com
          - corepack enable
          - corepack prepare pnpm@10.33.0 --activate
          - rm -rf dist
          - pnpm install --frozen-lockfile
          - pnpm run build
        artifacts:
          - name: BUILD_ARTIFACT
            path:
              - ./dist
            type: .tar.gz
        caches:
          - ~/.local/share/pnpm

  - name: 部署
    displayName: 部署
    steps:
      - step: deploy@agent
        displayName: 主机部署
        deployArtifact:
          - name: output
            target: "~/gitee_go/deploy"
            source: build
            dependArtifact: BUILD_ARTIFACT
            artifactRepository: default
            artifactName: output
            artifactVersion: latest
        script: |-
          bash /opt/hal-astro/deploy.sh
        hostGroupID:
          ID: hal-astro

构建使用 pnpm 而非 npm:通过 corepack 激活 package.jsonpackageManager 声明的 pnpm 版本,依赖用 pnpm install --frozen-lockfilepnpm-lock.yaml 锁定安装,缓存目录 ~/.local/share/pnpm 用于加速后续构建。部署阶段把产物下发到主机组 hal-astro 对应的服务器。

触发条件是 main 分支 Push:

triggers:
  push:
    branches:
      precise:
        - main

只要本地执行:

git add .
git commit -m "更新内容"
git push origin main

Gitee Go 就会自动构建和部署。

构建产物位置

Gitee Go 的构建阶段把 Astro 的 dist 目录打成压缩包,部署阶段会把压缩包下发到服务器:

/root/gitee_go/deploy/output.tar.gz

可以在服务器上查看压缩包内容:

tar -tzf /root/gitee_go/deploy/output.tar.gz | sed -n '1,80p'

正常情况下能看到:

./dist/
./dist/index.html
./dist/topics/
./dist/docs/
./dist/about/

如果压缩包里出现了已删除的目录(例如本次清理掉的 docs/astro/markdown-guide/),说明 Gitee 的构建产物不是最新代码,需要检查流水线是否执行到了最新提交。

Linux 部署脚本

服务器部署脚本在:

/opt/hal-astro/deploy.sh

脚本按以下顺序执行:

  1. 等待 output.tar.gz 写入稳定,避免文件还没传完就解压。
  2. flock 加锁,避免多个部署同时执行。
  3. 校验压缩包是否是合法 tar.gz
  4. 解压到临时目录 /tmp/hal-astro-release
  5. 找到 dist 目录。
  6. rsync --delete 同步到 Nginx 根目录。
  7. 修正文件权限。

脚本内容:

#!/usr/bin/env bash
set -euo pipefail

ARTIFACT="/root/gitee_go/deploy/output.tar.gz"
TMP_DIR="/tmp/hal-astro-release"
WEB_DIR="/usr/share/nginx/html"
LOCK_FILE="/run/hal-astro-deploy.lock"

log() {
  printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
}

wait_for_stable_artifact() {
  local previous_size=""
  local stable_count=0

  for _ in $(seq 1 30); do
    if [ ! -f "$ARTIFACT" ]; then
      sleep 1
      continue
    fi

    local current_size
    current_size="$(stat -c '%s' "$ARTIFACT")"

    if [ "$current_size" = "$previous_size" ]; then
      stable_count=$((stable_count + 1))
      if [ "$stable_count" -ge 3 ]; then
        return 0
      fi
    else
      stable_count=0
      previous_size="$current_size"
    fi

    sleep 1
  done

  log "Artifact did not become stable in time: $ARTIFACT"
  return 1
}

main() {
  exec 9>"$LOCK_FILE"
  if ! flock -n 9; then
    log "Deploy is already running, skip this trigger."
    exit 0
  fi

  log "Deploy started."
  wait_for_stable_artifact

  if ! tar -tzf "$ARTIFACT" >/dev/null; then
    log "Artifact is not a valid tar.gz: $ARTIFACT"
    exit 1
  fi

  rm -rf "$TMP_DIR"
  mkdir -p "$TMP_DIR" "$WEB_DIR"

  tar -xzf "$ARTIFACT" -C "$TMP_DIR"

  local source_dir
  if [ -d "$TMP_DIR/dist" ]; then
    source_dir="$TMP_DIR/dist"
  elif [ -f "$TMP_DIR/index.html" ]; then
    source_dir="$TMP_DIR"
  else
    log "No dist directory or index.html found after extraction."
    find "$TMP_DIR" -maxdepth 3 -print
    exit 1
  fi

  rsync -a --delete "$source_dir/" "$WEB_DIR/"

  find "$WEB_DIR" -type d -exec chmod 755 {} +
  find "$WEB_DIR" -type f -exec chmod 644 {} +
  restorecon -R "$WEB_DIR" 2>/dev/null || true

  log "Deploy finished: $source_dir -> $WEB_DIR"
}

main "$@"

手动执行部署:

bash /opt/hal-astro/deploy.sh

查看发布后的文件:

find /usr/share/nginx/html -maxdepth 3 -type f | sort | sed -n '1,120p'

systemd 自动监听

服务器使用 systemd.path 监听压缩包变化。这样即使 Gitee Go 只是更新了 output.tar.gz,系统也会自动执行部署脚本。

Service 文件:

/etc/systemd/system/hal-astro-deploy.service

内容:

[Unit]
Description=Deploy HAL Astro static site from Gitee Go artifact
After=network.target nginx.service

[Service]
Type=oneshot
User=root
ExecStart=/opt/hal-astro/deploy.sh

Path 文件:

/etc/systemd/system/hal-astro-deploy.path

内容:

[Unit]
Description=Watch Gitee Go output.tar.gz for HAL Astro deploy

[Path]
PathChanged=/root/gitee_go/deploy
PathChanged=/root/gitee_go/deploy/output.tar.gz
PathModified=/root/gitee_go/deploy/output.tar.gz
Unit=hal-astro-deploy.service

[Install]
WantedBy=multi-user.target

启用监听:

systemctl daemon-reload
systemctl enable --now hal-astro-deploy.path

查看监听状态:

systemctl status hal-astro-deploy.path

查看部署日志:

journalctl -u hal-astro-deploy.service -n 80 --no-pager

持续查看日志:

journalctl -u hal-astro-deploy.service -f

Nginx 配置

当前 Nginx 站点配置文件:

/etc/nginx/conf.d/memolab.cn.conf

当前配置:

# memolab.cn site — points to default welcome page
server {
    listen      80;
    listen      [::]:80;
    server_name www.memolab.cn memolab.cn;

    root        /usr/share/nginx/html;
    index       index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    error_page  500 502 503 504  /50x.html;
    location = /50x.html {
        root /usr/share/nginx/html;
    }
}

关键点:

  • root /usr/share/nginx/html; 必须和部署脚本里的 WEB_DIR 一致。
  • try_files $uri $uri/ =404; 适合当前 Astro 静态站点,因为 Astro 会生成真实目录和 index.html
  • 修改 Nginx 配置后,需要先测试再重载。

测试 Nginx 配置:

nginx -t

重载 Nginx:

systemctl reload nginx

查看 Nginx 状态:

systemctl status nginx

发布验证

一次完整发布后,在服务器上检查:

stat /root/gitee_go/deploy/output.tar.gz
tar -tzf /root/gitee_go/deploy/output.tar.gz | sed -n '1,80p'
journalctl -u hal-astro-deploy.service -n 40 --no-pager

检查 Nginx 本机访问:

curl -I http://127.0.0.1/
curl -I http://127.0.0.1/topics/
curl -I http://127.0.0.1/docs/nest/typescript/

正常情况下应该返回:

HTTP/1.1 200 OK

已经删除的页面会变成 404,例如本次清理掉的 docs/astro/markdown-guide/

curl -I http://127.0.0.1/docs/astro/markdown-guide/

常见问题

流水线成功,但页面还是旧的

先检查服务器上的压缩包内容:

tar -tzf /root/gitee_go/deploy/output.tar.gz | sed -n '1,80p'

如果压缩包本身是旧内容,问题在 Gitee 构建阶段,需要检查流水线是否构建了最新提交。

如果压缩包是新内容,但 Nginx 目录还是旧内容,检查部署脚本日志:

journalctl -u hal-astro-deploy.service -n 80 --no-pager

output.tar.gz 更新了,但没有自动解压

检查 path 是否在运行:

systemctl status hal-astro-deploy.path

如果没有运行,重新启用:

systemctl daemon-reload
systemctl enable --now hal-astro-deploy.path

Nginx 返回 404

先确认文件是否存在:

find /usr/share/nginx/html -maxdepth 3 -type f | sort | sed -n '1,120p'

再确认 Nginx root 是否还是:

root /usr/share/nginx/html;

如果 root 指向了别的目录,即使部署成功,Nginx 也不会读到最新页面。

不要把密钥写进文档

文档和仓库里不应该保存服务器密码、AccessKey、Secret、私钥或 Token。

当前服务器连接使用本机 SSH key:

ssh alias: hal-astro-server
identity file: ~/.ssh/hal_astro_server_ed25519

这里记录的是本机连接方式,不记录私钥内容。