# ToolBox:Vue 前端 CI(Gitea Actions) # # 单 job:npm test + Docker 多架构构建/推送 # 约定:Dockerfile 在仓库根目录 # # 前置:act_runner(ubuntu-latest + docker.sock)、Secret REGISTRY_TOKEN # Variables:REGISTRY、IMAGE_NAME、DOCKER_IMAGE_PREFIX、RUN_NPM_TEST 等 name: CI on: push: branches: [main, master] tags: ['v*'] pull_request: workflow_dispatch: env: REGISTRY: ${{ vars.REGISTRY }} GITEA_ROOT_URL: ${{ vars.GITEA_ROOT_URL }} IMAGE_NAME: ${{ vars.IMAGE_NAME }} DOCKERFILE: ${{ vars.DOCKERFILE }} DOCKER_PLATFORMS: ${{ vars.DOCKER_PLATFORMS }} RUN_NPM_TEST: ${{ vars.RUN_NPM_TEST }} DOCKER_IMAGE_PREFIX: ${{ vars.DOCKER_IMAGE_PREFIX }} jobs: docker: runs-on: ubuntu-latest container: image: catthehacker/ubuntu:act-22.04 steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Docker run: | set -eux if ! command -v docker >/dev/null 2>&1; then apt-get update DEBIAN_FRONTEND=noninteractive apt-get install -y docker.io fi if [ ! -S /var/run/docker.sock ]; then echo "ERROR: /var/run/docker.sock 未挂载到 job 容器。" >&2 echo "请在 act_runner config.yaml 中配置:" >&2 echo " container.options: -v /var/run/docker.sock:/var/run/docker.sock" >&2 echo " container.valid_volumes: [/var/run/docker.sock]" >&2 exit 1 fi docker info - name: Run npm tests if: vars.RUN_NPM_TEST != 'false' shell: bash run: | set -euo pipefail NODE_VERSION=20.20.2 ARCH=$(uname -m | sed 's/x86_64/x64/; s/aarch64/arm64/') TAR="node-v${NODE_VERSION}-linux-${ARCH}.tar.gz" if ! command -v node >/dev/null 2>&1 || ! node -v | grep -qE "v20\."; then downloaded=0 for url in \ "https://npmmirror.com/mirrors/node/v${NODE_VERSION}/${TAR}" \ "https://mirrors.aliyun.com/nodejs-release/v${NODE_VERSION}/${TAR}" \ "https://nodejs.org/dist/v${NODE_VERSION}/${TAR}"; do echo "trying ${url}" if curl -fsSL --connect-timeout 20 --retry 3 --retry-delay 5 --max-time 600 \ "$url" -o /tmp/node.tar.gz; then downloaded=1 break fi done [ "$downloaded" -eq 1 ] || { echo "failed to download Node ${NODE_VERSION}" >&2; exit 1; } rm -rf /usr/local/node mkdir -p /usr/local/node tar -xzf /tmp/node.tar.gz -C /usr/local/node --strip-components=1 export PATH="/usr/local/node/bin:${PATH}" fi node -v npm -v # act-runner 工作区 volume 可能残留旧 lockfile,强制与当前 commit 一致 git checkout HEAD -- package-lock.json package.json .npmrc echo "commit: $(git rev-parse --short HEAD)" echo "lockfile sample:" grep -m3 '"resolved"' package-lock.json || true if grep -qE 'artifactory|xiaohongshu' package-lock.json; then echo "ERROR: package-lock.json 仍含内网源,请检查 checkout" >&2 exit 1 fi # 清除 runner 注入的内网 npm 配置,使用独立缓存目录 unset NPM_CONFIG_REGISTRY NPM_REGISTRY npm_config_registry NODE_AUTH_TOKEN 2>/dev/null || true export CI=true export NPM_CONFIG_USERCONFIG=/tmp/toolbox-npmrc export NPM_CONFIG_GLOBALCONFIG=/tmp/toolbox-npmrc-global export npm_config_cache=/tmp/toolbox-npm-cache export npm_config_progress=true export npm_config_loglevel=info export npm_config_registry=https://registry.npmmirror.com export npm_config_esbuild_binary_host_mirror=https://npmmirror.com/mirrors/esbuild printf '%s\n' 'registry=https://registry.npmmirror.com' > /tmp/toolbox-npmrc-global printf '%s\n' \ 'registry=https://registry.npmmirror.com' \ 'esbuild_binary_host_mirror=https://npmmirror.com/mirrors/esbuild' \ 'always-auth=false' \ > /tmp/toolbox-npmrc rm -rf node_modules /tmp/toolbox-npm-cache mkdir -p /tmp/toolbox-npm-cache npm cache clean --force echo "npm registry: $(npm config get registry)" echo "=== npm ci ===" npm ci --userconfig=/tmp/toolbox-npmrc --registry=https://registry.npmmirror.com echo "=== npm test ===" npm run test:run -- --pool=forks --maxWorkers=2 --reporter=verbose - name: Resolve registry id: reg if: gitea.event_name == 'push' shell: bash env: GITEA_SERVER_URL: ${{ gitea.server_url }} run: | set -euo pipefail url_to_host() { echo "$1" | sed -e 's|^https://||' -e 's|^http://||' -e 's|/.*||' } is_internal_host() { local host="${1%%:*}" case "$host" in localhost|127.*|10.*|192.168.*) return 0 ;; 172.*) local second second=$(echo "$host" | cut -d. -f2) [ "$second" -ge 16 ] && [ "$second" -le 31 ] return ;; *) return 1 ;; esac } registry="${REGISTRY:-}" if [ -z "$registry" ] && [ -n "${GITEA_ROOT_URL:-}" ]; then registry=$(url_to_host "${GITEA_ROOT_URL}") fi if [ -z "$registry" ]; then registry=$(url_to_host "${GITEA_SERVER_URL}") fi if is_internal_host "$registry"; then echo "ERROR: Registry 主机 '${registry}' 是内网地址。" >&2 echo "请设置 Variable REGISTRY=公网 Gitea 域名(如 git.example.com)。" >&2 exit 1 fi echo "registry host: ${registry}" auth_header="" if auth_header=$(curl -fsS -D - "https://${registry}/v2/" -o /dev/null 2>&1 | grep -i '^www-authenticate:'); then if echo "$auth_header" | grep -qiE '127\.0\.0\.1|172\.(1[6-9]|2[0-9]|3[01])\.|localhost|:13827'; then echo "ERROR: Registry token realm 指向内网地址:${auth_header}" >&2 exit 1 fi fi echo "host=${registry}" >> "${GITHUB_OUTPUT}" - name: Log in to Container Registry if: gitea.event_name == 'push' shell: bash run: | set -euo pipefail echo "${{ secrets.REGISTRY_TOKEN }}" | \ docker login "${{ steps.reg.outputs.host }}" -u "${{ gitea.actor }}" --password-stdin - name: Build image shell: bash env: GITEA_SHA: ${{ gitea.sha }} GITEA_REF: ${{ gitea.ref }} GITEA_REPOSITORY: ${{ gitea.repository }} GITEA_REPOSITORY_OWNER: ${{ gitea.repository_owner }} GITEA_EVENT_NAME: ${{ gitea.event_name }} REGISTRY_HOST: ${{ steps.reg.outputs.host }} run: | set -euo pipefail dockerfile="${DOCKERFILE:-Dockerfile}" if [ "${DOCKER_IMAGE_PREFIX:-}" = "hub" ] || [ "${DOCKER_IMAGE_PREFIX:-}" = "docker.io" ]; then image_prefix="" elif [ -z "${DOCKER_IMAGE_PREFIX:-}" ] || [ "${DOCKER_IMAGE_PREFIX}" = "1panel" ]; then image_prefix="docker.1panel.live/library/" elif [ "${DOCKER_IMAGE_PREFIX}" = "daocloud" ]; then image_prefix="docker.m.daocloud.io/library/" else image_prefix="${DOCKER_IMAGE_PREFIX}" [[ "${image_prefix}" == */ ]] || image_prefix="${image_prefix}/" fi echo "using IMAGE_PREFIX=${image_prefix:-}" build_args=(--build-arg "IMAGE_PREFIX=${image_prefix}") builder_id="gitea-buildx-${GITEA_REPOSITORY//\//-}" if [ "${GITEA_EVENT_NAME}" = "push" ]; then install_binfmt() { for img in \ "docker.1panel.live/tonistiigi/binfmt:latest" \ "docker.m.daocloud.io/tonistiigi/binfmt:latest" \ "tonistiigi/binfmt:latest"; do echo "trying binfmt image: ${img}" if docker run --privileged --rm "${img}" --install all; then return 0 fi done return 1 } install_binfmt || true registry="${REGISTRY_HOST}" image_name="${IMAGE_NAME:-$(echo "${GITEA_REPOSITORY##*/}" | tr '[:upper:]' '[:lower:]')}" platforms="${DOCKER_PLATFORMS:-linux/amd64,linux/arm64}" image="${registry}/${GITEA_REPOSITORY_OWNER}/${image_name}" tags="${image}:sha-${GITEA_SHA:0:7}" case "${GITEA_REF}" in refs/heads/main|refs/heads/master) tags="${tags},${image}:latest" ;; refs/tags/v*) tags="${tags},${image}:${GITEA_REF#refs/tags/}" ;; esac docker buildx create --name "${builder_id}" --use 2>/dev/null || docker buildx use "${builder_id}" docker buildx inspect --bootstrap tag_args=() IFS=',' read -r -a tag_list <<< "$tags" for t in "${tag_list[@]}"; do tag_args+=(-t "$t") done docker buildx build \ --platform "${platforms}" \ -f "${dockerfile}" \ "${build_args[@]}" \ "${tag_args[@]}" \ --push \ . else # PR:仅单架构构建验证 Dockerfile,不推送 docker buildx create --name "${builder_id}" --use 2>/dev/null || docker buildx use "${builder_id}" docker buildx inspect --bootstrap docker buildx build \ --platform linux/amd64 \ -f "${dockerfile}" \ "${build_args[@]}" \ --load \ . fi