diff --git a/.gitea/scripts/prepare-workspace.sh b/.gitea/scripts/prepare-workspace.sh deleted file mode 100644 index 929651d..0000000 --- a/.gitea/scripts/prepare-workspace.sh +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Reusable bootstrap for Gitea workflows: -# 1) checkout target commit -# 2) ensure Node version (install from domestic mirror if missing/mismatch) -# 3) ensure Yarn is available (and optionally pin version) -# 通用准备脚本:用于 CI/Deploy 前的代码检出、Node 准备、Yarn 准备。 - -REPO_URL="${1:?repo url required}" -GIT_SHA="${2:?git sha required}" -# 优先使用精确版本 NODE_VERSION;未指定时按 NODE_MAJOR 自动解析最新小版本。 -NODE_MAJOR="${NODE_MAJOR:-22}" -NODE_VERSION="${NODE_VERSION:-}" -YARN_VERSION="${YARN_VERSION:-stable}" -# 默认使用国内镜像,减少下载失败概率。 -NODEJS_MIRROR="${NODEJS_MIRROR:-https://npmmirror.com/mirrors/node}" -NPM_REGISTRY="${NPM_REGISTRY:-https://registry.npmmirror.com}" -# 安装到用户目录,避免依赖 /opt 写权限。 -NODE_INSTALL_ROOT="${NODE_INSTALL_ROOT:-$HOME/.local/node-ci}" - -resolve_node_version() { - # 指定了精确版本时,直接使用。 - if [ -n "${NODE_VERSION}" ]; then - echo "${NODE_VERSION#v}" - return - fi - - # 未指定精确版本时,从镜像索引中选定 NODE_MAJOR 对应的最新版本。 - local resolved - resolved="$(curl -fsSL "${NODEJS_MIRROR}/index.tab" | awk -F'\t' -v major="${NODE_MAJOR}" ' - NR > 1 && $1 ~ ("^v" major "\\.") { print substr($1, 2); exit } - ')" - if [ -z "${resolved}" ]; then - echo "failed to resolve latest Node ${NODE_MAJOR}.x from ${NODEJS_MIRROR}" - exit 1 - fi - echo "${resolved}" -} - -detect_arch() { - # 仅支持常见 Linux 架构;其他架构显式失败,避免下载错误包。 - case "$(uname -m)" in - x86_64) echo "x64" ;; - aarch64 | arm64) echo "arm64" ;; - *) - echo "unsupported architecture: $(uname -m)" - exit 1 - ;; - esac -} - -install_node() { - local target_version="$1" - local arch name url tarball install_dir bindir - arch="$(detect_arch)" - name="node-v${target_version}-linux-${arch}" - url="${NODEJS_MIRROR}/v${target_version}/${name}.tar.xz" - tarball="/tmp/${name}.tar.xz" - install_dir="${NODE_INSTALL_ROOT}/${name}" - bindir="${install_dir}/bin" - - if [ ! -x "${bindir}/node" ]; then - # 下载并解压 Node 二进制包到本地缓存目录。 - mkdir -p "${NODE_INSTALL_ROOT}" - curl -fsSL "${url}" -o "${tarball}" - tar -xJf "${tarball}" -C "${NODE_INSTALL_ROOT}" - rm -f "${tarball}" - fi - - export PATH="${bindir}:${PATH}" -} - -ensure_node() { - local target_version node_version - target_version="$(resolve_node_version)" - - # 版本完全一致则直接复用,避免重复下载。 - if command -v node >/dev/null 2>&1; then - node_version="$(node -v | sed 's/^v//')" - if [ "${node_version}" = "${target_version}" ]; then - return - fi - fi - - install_node "${target_version}" - - node_version="$(node -v | sed 's/^v//')" - if [ "${node_version}" != "${target_version}" ]; then - echo "failed to switch to Node ${target_version}, current=${node_version}" - exit 1 - fi -} - -# 首次执行时检出目标提交;已存在仓库则更新并强制切到指定 SHA。 -if [ ! -d .git ]; then - git clone "${REPO_URL}" . -fi - -git fetch --all --tags --prune -git checkout -f "${GIT_SHA}" - -ensure_node - -# Use domestic npm mirror for package-manager metadata/download. -npm config set registry "${NPM_REGISTRY}" >/dev/null 2>&1 || true - -if ! command -v yarn >/dev/null 2>&1; then - # 通过 corepack 激活指定 Yarn 版本。 - corepack enable - export COREPACK_NPM_REGISTRY="${NPM_REGISTRY}" - corepack prepare "yarn@${YARN_VERSION}" --activate -fi - -echo "Node: $(node -v)" -echo "Yarn: $(yarn -v)" diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 46e687a..bdbd747 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -6,31 +6,15 @@ on: pull_request: jobs: - # workflow_call 调用示例(先保留注释,不启用): - # ci: - # uses: ./.gitea/workflows/reusable-ci.yml - # with: - # node_major: "22" - # yarn_version: "stable" - # run_lint: true - # run_tsc: true - ci: - runs-on: ubuntu-latest - env: - NODE_MAJOR: "22" - YARN_VERSION: "stable" - steps: - - name: Prepare workspace (checkout + node/yarn) - run: | - set -euo pipefail - chmod +x .gitea/scripts/prepare-workspace.sh - ./.gitea/scripts/prepare-workspace.sh \ - "${{ github.server_url }}/${{ github.repository }}.git" \ - "${{ github.sha }}" - - name: Install dependencies - run: yarn install --frozen-lockfile - - name: Lint - run: yarn lint - - name: TypeScript check - run: yarn tsc --noEmit + uses: platform/workflow/.gitea/workflows/reusable-prepare-workspace.yml@1.1 + with: + repo_url: "${{ github.server_url }}/${{ github.repository }}.git" + git_sha: "${{ github.sha }}" + node_major: "22" + yarn_version: "stable" + run_commands: | + set -euo pipefail + yarn install --frozen-lockfile + yarn lint + yarn tsc --noEmit diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 6f6a589..762b818 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -8,73 +8,57 @@ on: jobs: deploy: if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') }} - runs-on: ubuntu-latest - env: - NODE_MAJOR: "22" - YARN_VERSION: "stable" - DEPLOY_HOST: ${{ vars.DEPLOY_HOST }} - DEPLOY_PORT: ${{ vars.DEPLOY_PORT }} - DEPLOY_USER: ${{ vars.DEPLOY_USER }} - DEPLOY_PATH: ${{ vars.DEPLOY_PATH }} - IMAGE_REPO: ${{ vars.IMAGE_REPO }} - REGISTRY: ${{ vars.REGISTRY }} + uses: platform/workflow/.gitea/workflows/reusable-prepare-workspace.yml@1.1 + secrets: inherit + with: + repo_url: "${{ github.server_url }}/${{ github.repository }}.git" + git_sha: "${{ github.sha }}" + node_major: "22" + yarn_version: "stable" + run_commands: | + set -euo pipefail - steps: - - name: Prepare workspace (checkout + node/yarn) - run: | - set -euo pipefail - chmod +x .gitea/scripts/prepare-workspace.sh - ./.gitea/scripts/prepare-workspace.sh \ - "${{ github.server_url }}/${{ github.repository }}.git" \ - "${{ github.sha }}" + DEPLOY_HOST="${{ vars.DEPLOY_HOST }}" + DEPLOY_PORT="${{ vars.DEPLOY_PORT }}" + DEPLOY_USER="${{ vars.DEPLOY_USER }}" + DEPLOY_PATH="${{ vars.DEPLOY_PATH }}" + IMAGE_REPO="${{ vars.IMAGE_REPO }}" + REGISTRY="${{ vars.REGISTRY }}" - - name: Setup SSH - run: | - set -euo pipefail - mkdir -p ~/.ssh - echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - ssh-keyscan -p "${DEPLOY_PORT:-22}" "${DEPLOY_HOST}" >> ~/.ssh/known_hosts + mkdir -p ~/.ssh + echo "${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -p "${DEPLOY_PORT:-22}" "${DEPLOY_HOST}" >> ~/.ssh/known_hosts - - name: Prepare image tag - run: | - set -euo pipefail - if [[ "${GITHUB_REF:-}" == refs/tags/* ]]; then - IMAGE_TAG="${GITHUB_REF_NAME}" - else - IMAGE_TAG="${GITHUB_SHA:0:12}" - fi - echo "IMAGE_TAG=${IMAGE_TAG}" >> "$GITHUB_ENV" + if [[ "${GITHUB_REF:-}" == refs/tags/* ]]; then + IMAGE_TAG="${GITHUB_REF_NAME}" + else + IMAGE_TAG="${GITHUB_SHA:0:12}" + fi - - name: Build and push image - run: | - set -euo pipefail - if [ -z "${REGISTRY}" ] || [ -z "${IMAGE_REPO}" ]; then - echo "REGISTRY and IMAGE_REPO vars are required" - exit 1 - fi + if [ -z "${REGISTRY}" ] || [ -z "${IMAGE_REPO}" ]; then + echo "REGISTRY and IMAGE_REPO vars are required" + exit 1 + fi - echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "${REGISTRY}" -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin - docker build -f deploy/docker/Dockerfile -t "${IMAGE_REPO}:${IMAGE_TAG}" . - docker push "${IMAGE_REPO}:${IMAGE_TAG}" + echo "${REGISTRY_PASSWORD}" | docker login "${REGISTRY}" -u "${REGISTRY_USERNAME}" --password-stdin + docker build -f deploy/docker/Dockerfile -t "${IMAGE_REPO}:${IMAGE_TAG}" . + docker push "${IMAGE_REPO}:${IMAGE_TAG}" - - name: Deploy - run: | - set -euo pipefail - ssh -p "${DEPLOY_PORT:-22}" "${DEPLOY_USER}@${DEPLOY_HOST}" < deploy/docker/.env < deploy/docker/.env </dev/null - EOF + echo "${REGISTRY_PASSWORD}" | docker login "${REGISTRY}" -u "${REGISTRY_USERNAME}" --password-stdin + docker compose -f deploy/docker/docker-compose.yml --env-file deploy/docker/.env pull app + docker compose -f deploy/docker/docker-compose.yml --env-file deploy/docker/.env up -d app + docker compose -f deploy/docker/docker-compose.yml ps + curl -fsS "http://127.0.0.1:\${HOST_BIND_PORT:-3000}/api/docs" >/dev/null + EOF diff --git a/.gitea/workflows/reusable-ci.yml b/.gitea/workflows/reusable-ci.yml deleted file mode 100644 index d20ca6a..0000000 --- a/.gitea/workflows/reusable-ci.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Reusable CI - -on: - workflow_call: - inputs: - node_major: - description: "Node 主版本(例如 22)" - required: false - type: string - default: "22" - yarn_version: - description: "Yarn 版本(例如 stable 或 1.22.22)" - required: false - type: string - default: "stable" - run_lint: - description: "是否执行 yarn lint" - required: false - type: boolean - default: true - run_tsc: - description: "是否执行 yarn tsc --noEmit" - required: false - type: boolean - default: true - -jobs: - ci: - runs-on: ubuntu-latest - env: - NODE_MAJOR: ${{ inputs.node_major }} - YARN_VERSION: ${{ inputs.yarn_version }} - steps: - - name: Prepare workspace (checkout + node/yarn) - run: | - set -euo pipefail - chmod +x .gitea/scripts/prepare-workspace.sh - ./.gitea/scripts/prepare-workspace.sh \ - "${{ github.server_url }}/${{ github.repository }}.git" \ - "${{ github.sha }}" - - - name: Install dependencies - run: yarn install --frozen-lockfile - - - name: Lint - if: ${{ inputs.run_lint }} - run: yarn lint - - - name: TypeScript check - if: ${{ inputs.run_tsc }} - run: yarn tsc --noEmit diff --git a/.gitea/workflows/reusable-prepare-workspace.yml b/.gitea/workflows/reusable-prepare-workspace.yml new file mode 100644 index 0000000..4988696 --- /dev/null +++ b/.gitea/workflows/reusable-prepare-workspace.yml @@ -0,0 +1,177 @@ +name: Reusable Prepare Workspace + +on: + workflow_call: + inputs: + repo_url: + description: "Git 仓库地址" + required: true + type: string + git_sha: + description: "目标提交 SHA" + required: true + type: string + node_major: + description: "Node 主版本(例如 22)" + required: false + type: string + default: "22" + node_version: + description: "Node 精确版本(例如 22.12.0,优先于 node_major)" + required: false + type: string + default: "" + yarn_version: + description: "Yarn 版本(例如 stable 或 1.22.22)" + required: false + type: string + default: "stable" + nodejs_mirror: + description: "Node 二进制镜像地址" + required: false + type: string + default: "https://npmmirror.com/mirrors/node" + npm_registry: + description: "NPM 镜像地址" + required: false + type: string + default: "https://registry.npmmirror.com" + node_install_root: + description: "Node 安装缓存目录" + required: false + type: string + default: "" + run_commands: + description: "准备完成后执行的额外命令(多行 shell)" + required: false + type: string + default: "" + secrets: + SSH_PRIVATE_KEY: + required: false + REGISTRY_USERNAME: + required: false + REGISTRY_PASSWORD: + required: false + +jobs: + prepare: + runs-on: ubuntu-latest + env: + REPO_URL: ${{ inputs.repo_url }} + GIT_SHA: ${{ inputs.git_sha }} + NODE_MAJOR: ${{ inputs.node_major }} + NODE_VERSION: ${{ inputs.node_version }} + YARN_VERSION: ${{ inputs.yarn_version }} + NODEJS_MIRROR: ${{ inputs.nodejs_mirror }} + NPM_REGISTRY: ${{ inputs.npm_registry }} + NODE_INSTALL_ROOT: ${{ inputs.node_install_root }} + RUN_COMMANDS: ${{ inputs.run_commands }} + steps: + - name: Prepare workspace (checkout + node/yarn) + run: | + set -euo pipefail + + resolve_node_version() { + if [ -n "${NODE_VERSION}" ]; then + echo "${NODE_VERSION#v}" + return + fi + + local resolved + resolved="$(curl -fsSL "${NODEJS_MIRROR}/index.tab" | awk -F'\t' -v major="${NODE_MAJOR}" ' + NR > 1 && $1 ~ ("^v" major "\\.") { print substr($1, 2); exit } + ')" + if [ -z "${resolved}" ]; then + echo "failed to resolve latest Node ${NODE_MAJOR}.x from ${NODEJS_MIRROR}" + exit 1 + fi + echo "${resolved}" + } + + if [ -z "${NODE_INSTALL_ROOT}" ]; then + NODE_INSTALL_ROOT="${HOME}/.local/node-ci" + fi + + detect_arch() { + case "$(uname -m)" in + x86_64) echo "x64" ;; + aarch64 | arm64) echo "arm64" ;; + *) + echo "unsupported architecture: $(uname -m)" + exit 1 + ;; + esac + } + + install_node() { + local target_version="$1" + local arch name url tarball install_dir bindir + arch="$(detect_arch)" + name="node-v${target_version}-linux-${arch}" + url="${NODEJS_MIRROR}/v${target_version}/${name}.tar.xz" + tarball="/tmp/${name}.tar.xz" + install_dir="${NODE_INSTALL_ROOT}/${name}" + bindir="${install_dir}/bin" + + if [ ! -x "${bindir}/node" ]; then + mkdir -p "${NODE_INSTALL_ROOT}" + curl -fsSL "${url}" -o "${tarball}" + tar -xJf "${tarball}" -C "${NODE_INSTALL_ROOT}" + rm -f "${tarball}" + fi + + export PATH="${bindir}:${PATH}" + } + + ensure_node() { + local target_version node_version + target_version="$(resolve_node_version)" + + if command -v node >/dev/null 2>&1; then + node_version="$(node -v | sed 's/^v//')" + if [ "${node_version}" = "${target_version}" ]; then + return + fi + fi + + install_node "${target_version}" + + node_version="$(node -v | sed 's/^v//')" + if [ "${node_version}" != "${target_version}" ]; then + echo "failed to switch to Node ${target_version}, current=${node_version}" + exit 1 + fi + } + + if [ ! -d .git ]; then + git clone "${REPO_URL}" . + fi + + git fetch --all --tags --prune + git checkout -f "${GIT_SHA}" + + ensure_node + + npm config set registry "${NPM_REGISTRY}" >/dev/null 2>&1 || true + + if ! command -v yarn >/dev/null 2>&1; then + corepack enable + export COREPACK_NPM_REGISTRY="${NPM_REGISTRY}" + corepack prepare "yarn@${YARN_VERSION}" --activate + fi + + echo "Node: $(node -v)" + echo "Yarn: $(yarn -v)" + + - name: Run extra commands + if: ${{ inputs.run_commands != '' }} + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + run: | + set -euo pipefail + printf '%s\n' "${RUN_COMMANDS}" > /tmp/reusable-prepare-run.sh + chmod +x /tmp/reusable-prepare-run.sh + /tmp/reusable-prepare-run.sh