diff --git a/.gitea/workflows/checks.yml b/.gitea/workflows/checks.yml index 9bf59d5e..32e7c6b5 100644 --- a/.gitea/workflows/checks.yml +++ b/.gitea/workflows/checks.yml @@ -26,6 +26,8 @@ env: SOURCE_SHA: ${{ github.event.workflow_run.head_sha }} SOURCE_EVENT: ${{ github.event.workflow_run.event }} COMMIT_LINT_REQUIRE_EMOJI: "1" + FNM_BIN: "/data/bin/fnm" + FNM_DIR: "/data/fnm" jobs: checks: @@ -131,6 +133,29 @@ jobs: echo "========================================" cd "$REPO_DIR" + node_version=$(tr -d '[:space:]' < .node-version) + if ! [[ "$node_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo ".node-version 必须是完整的 Node.js 版本号: $node_version" >&2 + exit 1 + fi + if [ ! -x "$FNM_BIN" ]; then + echo "Prepare 未安装 fnm: $FNM_BIN" >&2 + exit 1 + fi + + eval "$("$FNM_BIN" env --shell bash)" + exec 9>"${FNM_DIR}.install.lock" + flock 9 + "$FNM_BIN" use --install-if-missing "$node_version" + flock -u 9 + + if [ "$(node --version)" != "v$node_version" ]; then + echo "Node.js 切换后版本不符合要求: $(node --version)" >&2 + exit 1 + fi + node --version + npm --version + echo "📦 安装锁定的 Node.js 校验工具" npm ci --ignore-scripts --no-audit --no-fund --no-bin-links diff --git a/.gitea/workflows/prepare.yml b/.gitea/workflows/prepare.yml index 4cf23461..d009a1d2 100644 --- a/.gitea/workflows/prepare.yml +++ b/.gitea/workflows/prepare.yml @@ -23,6 +23,10 @@ concurrency: 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: @@ -45,10 +49,9 @@ jobs: [curl]="curl" [flock]="util-linux" [realpath]="coreutils" - [node]="nodejs" - [npm]="npm" + [unzip]="unzip" ) - required_commands=(git python3 python3.11 curl flock realpath node npm) + required_commands=(git python3 python3.11 curl flock realpath unzip) missing_commands=() missing_packages=() declare -A seen_packages=() @@ -94,6 +97,34 @@ jobs: 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 "" @@ -105,12 +136,18 @@ jobs: 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" @@ -205,6 +242,8 @@ jobs: 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 仓库" @@ -215,3 +254,42 @@ jobs: } >> "$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" diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..60ade1ae --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +24.19.0 diff --git a/package-lock.json b/package-lock.json index 22962084..9d720950 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,9 @@ "packages": { "": { "name": "playbook", + "engines": { + "node": ">=20" + }, "devDependencies": { "markdownlint-cli": "0.48.0", "prettier": "3.9.6" diff --git a/package.json b/package.json index 473f963a..78100410 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,9 @@ { "name": "playbook", "private": true, + "engines": { + "node": ">=20" + }, "devDependencies": { "markdownlint-cli": "0.48.0", "prettier": "3.9.6" diff --git a/test/test_tsl_syntax_reference.py b/test/test_tsl_syntax_reference.py index 0ebd8d73..4fb71d3e 100644 --- a/test/test_tsl_syntax_reference.py +++ b/test/test_tsl_syntax_reference.py @@ -13,6 +13,7 @@ LOOKUP_PATH = SKILL_ROOT / "scripts" / "lookup.py" SKILL_PATH = SKILL_ROOT / "SKILL.md" CI_PATH = ROOT / ".gitea" / "workflows" / "checks.yml" PREPARE_PATH = ROOT / ".gitea" / "workflows" / "prepare.yml" +NODE_VERSION_PATH = ROOT / ".node-version" def load_lookup_module(): @@ -445,8 +446,18 @@ class TslSyntaxReferenceTests(unittest.TestCase): "node_modules/prettier/bin/prettier.cjs --check skills/tsl-syntax-reference", workflow, ) - self.assertIn("[node]=\"nodejs\"", prepare) - self.assertIn("[npm]=\"npm\"", prepare) + self.assertIn("[unzip]=\"unzip\"", prepare) + self.assertIn('FNM_BIN: "/data/bin/fnm"', prepare) + self.assertIn('FNM_DIR: "/data/fnm"', prepare) + self.assertIn('"$FNM_BIN" install "$node_version"', prepare) + self.assertNotIn('[node]="nodejs"', prepare) + self.assertNotIn('[npm]="npm"', prepare) + self.assertIn('"$FNM_BIN" use --install-if-missing "$node_version"', workflow) + self.assertIn("node --version", workflow) + self.assertRegex( + NODE_VERSION_PATH.read_text(encoding="utf-8").strip(), + r"^\d+\.\d+\.\d+$", + ) if __name__ == "__main__":