296 lines
11 KiB
YAML
296 lines
11 KiB
YAML
name: 🧰 准备环境
|
||
|
||
on:
|
||
push:
|
||
# 主分支:下游 sync / thirdparty 消费者只关心 main。
|
||
# 其它分支(含 ci bot 推送的 tsl-playbook / thirdparty/skill 分支)跑 Prepare 纯属空耗,故在此过滤。
|
||
branches:
|
||
- main
|
||
pull_request:
|
||
# PR:为下游 checks 消费者(standards + tests)预备 bare 仓库。
|
||
# PR 的 github.sha 是 Gitea 生成的 merge commit,不在 refs/heads/*,
|
||
# 故下面的 fetch 额外取 PR ref 并按事件类型选校验目标。
|
||
branches:
|
||
- main
|
||
workflow_dispatch:
|
||
schedule:
|
||
# thirdparty 快照每日轮询:Prepare 定时成功后,update-thirdparty-skills 通过 workflow_run 触发。
|
||
- cron: "17 3 * * *"
|
||
|
||
concurrency:
|
||
group: prepare-${{ github.repository }}
|
||
cancel-in-progress: false
|
||
|
||
env:
|
||
ACCESS_TOKEN: ${{ secrets.WORKFLOW }}
|
||
FNM_ARCHIVE_SHA256: "7807664f39d39fc518da1c35ba0181e4b3267603c4b1dedeb4b5fc6ae440a224"
|
||
FNM_BIN: "/data/bin/fnm"
|
||
FNM_DIR: "/data/fnm"
|
||
FNM_VERSION: "1.39.0"
|
||
WORKSPACE_ROOT: "/data/workspace"
|
||
|
||
jobs:
|
||
prepare:
|
||
runs-on: standard-ubuntu-22
|
||
permissions:
|
||
contents: read
|
||
|
||
steps:
|
||
- name: 🔧 检查并安装共享工具
|
||
id: tools
|
||
shell: bash
|
||
run: |
|
||
set -euo pipefail
|
||
|
||
declare -A package_for=(
|
||
[git]="git"
|
||
[python3]="python3"
|
||
[python3.11]="python3.11"
|
||
[curl]="curl"
|
||
[flock]="util-linux"
|
||
[realpath]="coreutils"
|
||
[unzip]="unzip"
|
||
)
|
||
required_commands=(git python3 python3.11 curl flock realpath unzip)
|
||
missing_commands=()
|
||
missing_packages=()
|
||
declare -A seen_packages=()
|
||
|
||
for command in "${required_commands[@]}"; do
|
||
if ! command -v "$command" >/dev/null 2>&1; then
|
||
package="${package_for[$command]}"
|
||
missing_commands+=("$command")
|
||
if [[ -z "${seen_packages[$package]+x}" ]]; then
|
||
missing_packages+=("$package")
|
||
seen_packages[$package]=1
|
||
fi
|
||
fi
|
||
done
|
||
|
||
installed=false
|
||
if [ "${#missing_packages[@]}" -gt 0 ]; then
|
||
echo "缺失命令: ${missing_commands[*]}"
|
||
echo "准备安装: ${missing_packages[*]}"
|
||
|
||
if [ "$(id -u)" -eq 0 ]; then
|
||
APT=(apt-get)
|
||
elif command -v sudo >/dev/null 2>&1; then
|
||
APT=(sudo apt-get)
|
||
else
|
||
echo "无法安装缺失命令;当前用户不是 root 且没有 sudo" >&2
|
||
echo "命令: ${missing_commands[*]}" >&2
|
||
echo "包: ${missing_packages[*]}" >&2
|
||
exit 1
|
||
fi
|
||
|
||
"${APT[@]}" update -qq
|
||
"${APT[@]}" install -y -qq "${missing_packages[@]}"
|
||
installed=true
|
||
fi
|
||
|
||
for command in "${required_commands[@]}"; do
|
||
if ! command -v "$command" >/dev/null 2>&1; then
|
||
echo "安装后仍找不到命令: $command (${package_for[$command]})" >&2
|
||
exit 1
|
||
fi
|
||
done
|
||
|
||
python3.11 -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 11) else "Python 3.11 or newer is required")'
|
||
|
||
fnm_installed=false
|
||
expected_fnm_version="fnm $FNM_VERSION"
|
||
current_fnm_version=""
|
||
if [ -x "$FNM_BIN" ]; then
|
||
current_fnm_version=$($FNM_BIN --version 2>/dev/null || true)
|
||
fi
|
||
|
||
if [ "$current_fnm_version" != "$expected_fnm_version" ]; then
|
||
echo "准备安装 $expected_fnm_version"
|
||
mkdir -p "$(dirname "$FNM_BIN")"
|
||
fnm_tmp_dir=$(mktemp -d)
|
||
trap 'rm -rf "$fnm_tmp_dir"' EXIT
|
||
fnm_archive="$fnm_tmp_dir/fnm-linux.zip"
|
||
curl -fsSL --retry 5 --retry-all-errors \
|
||
"https://github.com/Schniz/fnm/releases/download/v${FNM_VERSION}/fnm-linux.zip" \
|
||
-o "$fnm_archive"
|
||
printf '%s %s\n' "$FNM_ARCHIVE_SHA256" "$fnm_archive" | sha256sum -c -
|
||
unzip -q "$fnm_archive" -d "$fnm_tmp_dir"
|
||
install -m 0755 "$fnm_tmp_dir/fnm" "${FNM_BIN}.new"
|
||
mv -f "${FNM_BIN}.new" "$FNM_BIN"
|
||
fnm_installed=true
|
||
fi
|
||
|
||
if [ "$($FNM_BIN --version)" != "$expected_fnm_version" ]; then
|
||
echo "fnm 安装后版本不符合要求: $($FNM_BIN --version 2>&1)" >&2
|
||
exit 1
|
||
fi
|
||
|
||
{
|
||
echo "## Prepare 环境"
|
||
echo ""
|
||
echo "| 命令 | 版本 |"
|
||
echo "| --- | --- |"
|
||
for command in "${required_commands[@]}"; do
|
||
version_output=$("$command" --version 2>&1 || true)
|
||
version_line=${version_output%%$'\n'*}
|
||
version_line=${version_line//|/\\|}
|
||
printf '| `%s` | `%s` |\n' "$command" "$version_line"
|
||
done
|
||
printf '| `%s` | `%s` |\n' "fnm" "$($FNM_BIN --version)"
|
||
echo ""
|
||
if [ "$installed" = true ]; then
|
||
echo "安装的包:\`${missing_packages[*]}\`"
|
||
else
|
||
echo "安装的包:无,所有命令已存在。"
|
||
fi
|
||
if [ "$fnm_installed" = true ]; then
|
||
echo "安装的版本管理器:\`$expected_fnm_version\`"
|
||
else
|
||
echo "安装的版本管理器:无,\`$expected_fnm_version\` 已存在。"
|
||
fi
|
||
} >> "$GITHUB_STEP_SUMMARY"
|
||
|
||
echo "installed=$installed" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: 📥 准备共享 bare 仓库
|
||
shell: bash
|
||
run: |
|
||
set -euo pipefail
|
||
|
||
REPO_NAME="${{ github.event.repository.name }}"
|
||
REMOTE_URL="${{ github.server_url }}/${{ github.repository }}.git"
|
||
EVENT_NAME="${{ github.event_name }}"
|
||
REPOSITORY_DIR="${WORKSPACE_ROOT}/${REPO_NAME}/repository.git"
|
||
|
||
# 校验目标 SHA 因事件而异:
|
||
# - pull_request:github.sha 是 Gitea 生成的 merge commit,不在
|
||
# refs/heads/* 里,直接校验会失败;改用 PR head SHA,并在下方额外
|
||
# fetch PR ref 把 head 提交拉进 bare 仓库。下游 workflow_run 消费者
|
||
# 读到的 head_sha 也正是这个 PR head,两者一致。
|
||
# - 其它事件(push / schedule / dispatch):沿用 github.sha。
|
||
if [ "$EVENT_NAME" = "pull_request" ]; then
|
||
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
|
||
PR_NUMBER="${{ github.event.pull_request.number }}"
|
||
else
|
||
HEAD_SHA="${{ github.sha }}"
|
||
PR_NUMBER=""
|
||
fi
|
||
|
||
case "$HEAD_SHA" in
|
||
''|*[!0-9a-fA-F]*)
|
||
echo "无效的触发提交 SHA: $HEAD_SHA" >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
case "$REMOTE_URL" in
|
||
https://*) ;;
|
||
*)
|
||
echo "拒绝通过非 HTTPS remote 使用 WORKFLOW token: $REMOTE_URL" >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
if [ -z "$ACCESS_TOKEN" ]; then
|
||
echo "未配置 WORKFLOW secret,无法读取仓库" >&2
|
||
exit 1
|
||
fi
|
||
|
||
mkdir -p "$(dirname "$REPOSITORY_DIR")"
|
||
if [ ! -e "$REPOSITORY_DIR" ]; then
|
||
git init --bare "$REPOSITORY_DIR"
|
||
else
|
||
if [ ! -d "$REPOSITORY_DIR" ]; then
|
||
echo "共享仓库路径存在但不是目录: $REPOSITORY_DIR" >&2
|
||
exit 1
|
||
fi
|
||
if ! is_bare=$(git --git-dir="$REPOSITORY_DIR" rev-parse --is-bare-repository 2>/dev/null) || \
|
||
[ "$is_bare" != "true" ]; then
|
||
echo "共享仓库路径不是有效 bare 仓库: $REPOSITORY_DIR" >&2
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
if git --git-dir="$REPOSITORY_DIR" remote get-url origin >/dev/null 2>&1; then
|
||
git --git-dir="$REPOSITORY_DIR" remote set-url origin "$REMOTE_URL"
|
||
else
|
||
git --git-dir="$REPOSITORY_DIR" remote add origin "$REMOTE_URL"
|
||
fi
|
||
|
||
git \
|
||
-c credential.helper= \
|
||
-c 'credential.helper=!f() { test "$1" = get || exit 0; printf "%s\n" username=oauth2; printf "%s\n" "password=${ACCESS_TOKEN}"; }; f' \
|
||
-c credential.interactive=never \
|
||
--git-dir="$REPOSITORY_DIR" fetch \
|
||
--prune --prune-tags --tags --force origin \
|
||
'+refs/heads/*:refs/remotes/origin/*'
|
||
|
||
# PR head 不在 refs/heads/*,按需补取 PR ref 把 head 提交拉进 bare 仓库。
|
||
# 先试 PR head SHA 是否已在库(同仓库分支 PR 的 head 常已随 refs/heads/*
|
||
# 到位),不在再按 PR 号 fetch Gitea 的 refs/pull/<n>/head。
|
||
if [ -n "$PR_NUMBER" ] && \
|
||
! git --git-dir="$REPOSITORY_DIR" cat-file -e "${HEAD_SHA}^{commit}" 2>/dev/null; then
|
||
git \
|
||
-c credential.helper= \
|
||
-c 'credential.helper=!f() { test "$1" = get || exit 0; printf "%s\n" username=oauth2; printf "%s\n" "password=${ACCESS_TOKEN}"; }; f' \
|
||
-c credential.interactive=never \
|
||
--git-dir="$REPOSITORY_DIR" fetch --force origin \
|
||
"+refs/pull/${PR_NUMBER}/head:refs/remotes/origin/pull/${PR_NUMBER}/head" || true
|
||
fi
|
||
|
||
if ! git --git-dir="$REPOSITORY_DIR" cat-file -e "${HEAD_SHA}^{commit}"; then
|
||
echo "共享仓库中不存在触发提交: $HEAD_SHA(事件: $EVENT_NAME)" >&2
|
||
exit 1
|
||
fi
|
||
|
||
resolved_sha=$(git --git-dir="$REPOSITORY_DIR" rev-parse "${HEAD_SHA}^{commit}")
|
||
echo "PREPARED_REPOSITORY_DIR=$REPOSITORY_DIR" >> "$GITHUB_ENV"
|
||
echo "PREPARED_SHA=$resolved_sha" >> "$GITHUB_ENV"
|
||
{
|
||
echo ""
|
||
echo "## Prepare 仓库"
|
||
echo ""
|
||
echo "- bare 仓库:\`$REPOSITORY_DIR\`"
|
||
echo "- 触发提交:\`$resolved_sha\`"
|
||
echo "- 远端:\`$REMOTE_URL\`"
|
||
} >> "$GITHUB_STEP_SUMMARY"
|
||
|
||
echo "Prepare 完成: $REPOSITORY_DIR @ $resolved_sha"
|
||
|
||
- name: 🟢 准备 Node.js
|
||
shell: bash
|
||
run: |
|
||
set -euo pipefail
|
||
|
||
node_version=$(git --git-dir="$PREPARED_REPOSITORY_DIR" show "${PREPARED_SHA}:.node-version" | tr -d '[:space:]')
|
||
if ! [[ "$node_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||
echo ".node-version 必须是完整的 Node.js 版本号: $node_version" >&2
|
||
exit 1
|
||
fi
|
||
node_major=${node_version%%.*}
|
||
if [ "$node_major" -lt 20 ]; then
|
||
echo "Node.js 版本必须不低于 20: $node_version" >&2
|
||
exit 1
|
||
fi
|
||
|
||
mkdir -p "$FNM_DIR"
|
||
exec 9>"${FNM_DIR}.install.lock"
|
||
flock 9
|
||
"$FNM_BIN" install "$node_version"
|
||
flock -u 9
|
||
|
||
actual_node_version=$($FNM_BIN exec --using="$node_version" node --version)
|
||
actual_npm_version=$($FNM_BIN exec --using="$node_version" npm --version)
|
||
if [ "$actual_node_version" != "v$node_version" ]; then
|
||
echo "Node.js 安装后版本不符合要求: $actual_node_version" >&2
|
||
exit 1
|
||
fi
|
||
|
||
{
|
||
echo ""
|
||
echo "## Prepare Node.js"
|
||
echo ""
|
||
echo "- fnm:\`$($FNM_BIN --version)\`"
|
||
echo "- Node.js:\`$actual_node_version\`"
|
||
echo "- npm:\`$actual_npm_version\`"
|
||
echo "- 缓存目录:\`$FNM_DIR\`"
|
||
} >> "$GITHUB_STEP_SUMMARY"
|