diff --git a/.gitea/workflows/prepare.yml b/.gitea/workflows/prepare.yml index 0a00f983..58cd69b9 100644 --- a/.gitea/workflows/prepare.yml +++ b/.gitea/workflows/prepare.yml @@ -13,9 +13,6 @@ on: branches: - main workflow_dispatch: - schedule: - # 北京时间(UTC+8)每日 06:00;Prepare 成功后内部调用 thirdparty updater。 - - cron: "0 22 * * *" concurrency: group: prepare-${{ github.repository }} @@ -167,7 +164,7 @@ jobs: # refs/heads/* 里,直接校验会失败;改用 PR head SHA,并在下方额外 # fetch PR ref 把 head 提交拉进 bare 仓库。下游 workflow_run 消费者 # 读到的 head_sha 也正是这个 PR head,两者一致。 - # - 其它事件(push / schedule / dispatch):沿用 github.sha。 + # - 其它事件(push / dispatch):沿用 github.sha。 if [ "$EVENT_NAME" = "pull_request" ]; then HEAD_SHA="${{ github.event.pull_request.head.sha }}" PR_NUMBER="${{ github.event.pull_request.number }}" @@ -293,19 +290,3 @@ jobs: echo "- npm:\`$actual_npm_version\`" echo "- 缓存目录:\`$FNM_DIR\`" } >> "$GITHUB_STEP_SUMMARY" - - update_thirdparty_scheduled: - # Gitea 1.27 会抑制由 gitea-actions 定时运行产生的后续 workflow_run。 - # 因此 schedule 在 Prepare 成功后直接调用 updater;updater 本身不暴露 - # schedule / workflow_dispatch 入口。push / 手动 Prepare 仍走原 workflow_run 链。 - needs: prepare - if: ${{ github.event_name == 'schedule' }} - uses: ./.gitea/workflows/update-thirdparty-skills.yml - with: - source_branch: ${{ github.ref_name }} - source_sha: ${{ github.sha }} - secrets: - WORKFLOW: ${{ secrets.WORKFLOW }} - concurrency: - group: update-thirdparty-${{ github.repository }} - cancel-in-progress: false diff --git a/.gitea/workflows/update-thirdparty-skills.yml b/.gitea/workflows/update-thirdparty-skills.yml index 7e42269d..ebed7354 100644 --- a/.gitea/workflows/update-thirdparty-skills.yml +++ b/.gitea/workflows/update-thirdparty-skills.yml @@ -1,19 +1,11 @@ name: ♻️ 更新第三方 Skills on: - # schedule 由 prepare.yml 在 prepare job 成功后内部调用;这里不提供独立 - # schedule / workflow_dispatch 入口。 - workflow_call: - inputs: - source_branch: - required: true - type: string - source_sha: - required: true - type: string - secrets: - WORKFLOW: - required: true + # Gitea 1.27 不会从 schedule 触发的 Prepare 继续派生 workflow_run。 + # 定时刷新因此由 updater 直接拥有;push / PR 仍在 Prepare 成功后进入。 + schedule: + # 北京时间(UTC+8)每日 06:00。 + - cron: "0 22 * * *" workflow_run: workflows: ["🧰 准备环境"] types: @@ -24,12 +16,10 @@ concurrency: cancel-in-progress: false env: - # Prepare 维护共享仓库并读取代码;此处 token 用于 worktree 内的 fetch/push。 + # updater 会校准共享仓库;token 用于 fetch/push。 ACCESS_TOKEN: ${{ secrets.WORKFLOW }} WORKSPACE_ROOT: "/data/workspace" WORKSPACE_SLOT: "thirdparty" - SOURCE_BRANCH: ${{ inputs.source_branch || github.event.workflow_run.head_branch }} - SOURCE_SHA: ${{ inputs.source_sha || github.event.workflow_run.head_sha }} THIRDPARTY_BRANCH: "thirdparty/skill" MANIFEST_PATH: ".gitea/ci/thirdparty_skills.json" GIT_USER_NAME: "ci[bot]" @@ -37,9 +27,8 @@ env: jobs: update_and_sync: - # workflow_call 只能由 schedule Prepare 的 needs 成功路径进入; - # workflow_run 仍需要显式校验 Prepare 成功且来自主分支。 - if: ${{ github.event_name == 'workflow_call' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'schedule' && (github.event.workflow_run.head_branch == 'main' || github.event.workflow_run.head_branch == 'master')) }} + # schedule 直接进入;workflow_run 需要 Prepare 成功且来自主分支。 + if: ${{ github.event_name == 'schedule' || (github.event.workflow_run.conclusion == 'success' && (github.event.workflow_run.head_branch == 'main' || github.event.workflow_run.head_branch == 'master')) }} name: 📥 更新快照并同步 main runs-on: standard-ubuntu-22 permissions: @@ -54,9 +43,19 @@ jobs: echo "❌ 未配置 WORKFLOW secret,无法 fetch/push" >&2 exit 1 fi + if [ "$GITHUB_EVENT_NAME" = "schedule" ]; then + SOURCE_BRANCH="$GITHUB_REF_NAME" + SOURCE_SHA="$GITHUB_SHA" + echo "🌟 定时运行;目标分支: $SOURCE_BRANCH,提交: $SOURCE_SHA" + else + SOURCE_BRANCH="${{ github.event.workflow_run.head_branch }}" + SOURCE_SHA="${{ github.event.workflow_run.head_sha }}" + fi + echo "SOURCE_BRANCH=$SOURCE_BRANCH" >> "$GITHUB_ENV" + echo "SOURCE_SHA=$SOURCE_SHA" >> "$GITHUB_ENV" echo "✅ Token 已配置" - echo "🌿 上游分支: ${{ env.SOURCE_BRANCH }}" - echo "📝 上游提交: ${{ env.SOURCE_SHA }}" + echo "🌿 上游分支: $SOURCE_BRANCH" + echo "📝 上游提交: $SOURCE_SHA" - name: 📥 对齐 Worktree id: prepare_worktree @@ -65,7 +64,8 @@ jobs: set -euo pipefail REPO_NAME="${{ github.event.repository.name }}" - HEAD_SHA="${{ env.SOURCE_SHA }}" + HEAD_SHA="$SOURCE_SHA" + REMOTE_URL="${{ github.server_url }}/${{ github.repository }}.git" REPOSITORY_DIR="${WORKSPACE_ROOT}/${REPO_NAME}/repository.git" WORKTREE_DIR="${WORKSPACE_ROOT}/${REPO_NAME}/worktrees/${WORKSPACE_SLOT}" WORKTREE_LOCK="${WORKSPACE_ROOT}/${REPO_NAME}/worktree-admin.lock" @@ -76,13 +76,23 @@ jobs: exit 1 ;; esac - if [ ! -d "$REPOSITORY_DIR" ] || \ - [ "$(git --git-dir="$REPOSITORY_DIR" rev-parse --is-bare-repository 2>/dev/null)" != "true" ]; then - echo "Prepare 未创建有效共享 bare 仓库: $REPOSITORY_DIR" >&2 - exit 1 - fi - if ! git --git-dir="$REPOSITORY_DIR" cat-file -e "${HEAD_SHA}^{commit}"; then - echo "共享仓库中不存在上游提交: $HEAD_SHA" >&2 + case "$REMOTE_URL" in + https://*) ;; + *) + echo "拒绝通过非 HTTPS remote 使用 WORKFLOW token: $REMOTE_URL" >&2 + exit 1 + ;; + esac + + mkdir -p "$(dirname "$REPOSITORY_DIR")" + exec 9>"$WORKTREE_LOCK" + flock 9 + + if [ ! -e "$REPOSITORY_DIR" ]; then + git init --bare "$REPOSITORY_DIR" + elif [ ! -d "$REPOSITORY_DIR" ] || \ + [ "$(git --git-dir="$REPOSITORY_DIR" rev-parse --is-bare-repository 2>/dev/null)" != "true" ]; then + echo "共享仓库路径不是有效 bare 仓库: $REPOSITORY_DIR" >&2 exit 1 fi @@ -99,24 +109,32 @@ jobs: # `git remote set-url origin https://oauth2:$TOKEN@...` 仅在设置了 # WORKFLOW env 时才触发;本 job 严禁设置 WORKFLOW,否则 token 会落盘到 # 共享 bare 仓库的 config 并污染所有其它 slot。此处显式重置以固化约束。 - git --git-dir="$REPOSITORY_DIR" remote set-url origin "${{ github.server_url }}/${{ github.repository }}.git" + 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 --git-dir="$REPOSITORY_DIR" fetch \ + --prune --prune-tags --tags --force origin \ + '+refs/heads/*:refs/remotes/origin/*' + + if ! git --git-dir="$REPOSITORY_DIR" cat-file -e "${HEAD_SHA}^{commit}"; then + echo "共享仓库中不存在上游提交: $HEAD_SHA" >&2 + exit 1 + fi if [ ! -f "$WORKTREE_DIR/.git" ]; then - exec 9>"$WORKTREE_LOCK" - flock 9 - - if [ ! -f "$WORKTREE_DIR/.git" ]; then - if [ -e "$WORKTREE_DIR" ]; then - echo "Worktree 路径已存在但不是 linked worktree: $WORKTREE_DIR" >&2 - exit 1 - fi - mkdir -p "$(dirname "$WORKTREE_DIR")" - git --git-dir="$REPOSITORY_DIR" worktree add --detach "$WORKTREE_DIR" "$HEAD_SHA" + if [ -e "$WORKTREE_DIR" ]; then + echo "Worktree 路径已存在但不是 linked worktree: $WORKTREE_DIR" >&2 + exit 1 fi - - flock -u 9 + mkdir -p "$(dirname "$WORKTREE_DIR")" + git --git-dir="$REPOSITORY_DIR" worktree add --detach "$WORKTREE_DIR" "$HEAD_SHA" fi + flock -u 9 + common_dir=$(git -C "$WORKTREE_DIR" rev-parse --path-format=absolute --git-common-dir) if [ "$common_dir" != "$(realpath "$REPOSITORY_DIR")" ]; then echo "Worktree 不属于共享仓库: $WORKTREE_DIR" >&2 @@ -818,5 +836,5 @@ jobs: # 持久化 worktree 由下次运行前的 detached checkout + reset + clean 复用, # 此处仅在结束时 detach,避免留下已检出的分支阻塞其它 worktree。 if [ -n "${REPO_DIR:-}" ] && [ -f "$REPO_DIR/.git" ]; then - git -C "$REPO_DIR" checkout --detach --force "${SOURCE_SHA}" 2>/dev/null || true + git -C "$REPO_DIR" checkout --detach --force "${SOURCE_SHA:-}" 2>/dev/null || true fi diff --git a/test/README.md b/test/README.md index c1c5184f..5eb11704 100644 --- a/test/README.md +++ b/test/README.md @@ -34,13 +34,15 @@ python test/integration/check_doc_links.py ## 🧭 CI 自动化测试 -测试套件通过 Gitea Actions 自动运行(见 `.gitea/workflows/checks.yml` 的 `tests` job): +测试套件通过 Gitea Actions 自动运行(见 `.gitea/workflows/checks.yml` 的 +`🔎 规范检查 + 全量测试` job): - **触发方式**:`workflow_run`——「🧰 准备环境」(`prepare.yml`)完成后触发。 - Prepare 监听 `push:main`、`pull_request`、`workflow_dispatch` 与每日 `schedule`, - 故推送到 main、对 main 开 PR 或手动运行 Prepare 都会经 Prepare 传导到测试。 + Prepare 监听 `push:main`、`pull_request` 与 `workflow_dispatch`;推送到 main、 + 对 main 开 PR 或手动运行 Prepare 都会经 Prepare 传导到测试。每日 `schedule` + 只属于 thirdparty updater,不会触发这套提交验证。 - **运行平台**:standard-ubuntu-22 -- **注**:`checks.yml` 同时包含 commit message 规范校验(`standards` job)。 +- **注**:同一 job 先校验 commit message,再运行全量测试。 ## 📚 测试详解 diff --git a/test/test_thirdparty_skills_pipeline.py b/test/test_thirdparty_skills_pipeline.py index 3b4bea95..02027168 100644 --- a/test/test_thirdparty_skills_pipeline.py +++ b/test/test_thirdparty_skills_pipeline.py @@ -228,44 +228,36 @@ class ThirdpartySkillsPipelineTests(unittest.TestCase): self.assertIn("update_thirdparty_snapshots", text) self.assertIn("sync_thirdparty_skills", text) - def test_scheduled_prepare_calls_updater_without_an_independent_entry(self): + def test_schedule_runs_updater_without_changing_prepare_run_shape(self): prepare_text = PREPARE_WORKFLOW.read_text(encoding="utf-8") updater_text = WORKFLOW.read_text(encoding="utf-8") + self.assertEqual(workflow_mapping_keys(prepare_text, "jobs"), ["prepare"]) self.assertEqual( - workflow_mapping_keys(prepare_text, "jobs"), - ["prepare", "update_thirdparty_scheduled"], + workflow_mapping_keys(prepare_text, "on"), + ["push", "pull_request", "workflow_dispatch"], ) self.assertEqual( workflow_mapping_keys(updater_text, "on"), - ["workflow_call", "workflow_run"], + ["schedule", "workflow_run"], ) - self.assertIn(" needs: prepare", prepare_text) - self.assertIn(" if: ${{ github.event_name == 'schedule' }}", prepare_text) - self.assertIn( - " uses: ./.gitea/workflows/update-thirdparty-skills.yml", - prepare_text, - ) - self.assertIn(" source_branch: ${{ github.ref_name }}", prepare_text) - self.assertIn(" source_sha: ${{ github.sha }}", prepare_text) - self.assertIn(" WORKFLOW: ${{ secrets.WORKFLOW }}", prepare_text) - self.assertIn(" workflow_call:", updater_text) - self.assertIn(" source_branch:", updater_text) - self.assertIn(" source_sha:", updater_text) - self.assertIn(" WORKFLOW:", updater_text) - self.assertIn(" required: true", updater_text) - self.assertIn( - "SOURCE_BRANCH: ${{ inputs.source_branch || " - "github.event.workflow_run.head_branch }}", - updater_text, - ) - self.assertIn( - "SOURCE_SHA: ${{ inputs.source_sha || " - "github.event.workflow_run.head_sha }}", - updater_text, - ) - self.assertIn("github.event_name == 'workflow_call' ||", updater_text) - self.assertIn("github.event.workflow_run.event != 'schedule'", updater_text) + self.assertNotIn("update_thirdparty_scheduled", prepare_text) + self.assertNotIn("uses: ./.gitea/workflows/", prepare_text) + self.assertNotIn(" schedule:", prepare_text) + self.assertNotIn(" workflow_call:", updater_text) + self.assertIn(' - cron: "0 22 * * *"', updater_text) + self.assertIn("github.event_name == 'schedule' ||", updater_text) + self.assertNotIn("github.event.workflow_run.event != 'schedule'", updater_text) + self.assertIn('SOURCE_BRANCH="$GITHUB_REF_NAME"', updater_text) + self.assertIn('SOURCE_SHA="$GITHUB_SHA"', updater_text) + self.assertIn('SOURCE_BRANCH="${{ github.event.workflow_run.head_branch }}"', updater_text) + self.assertIn('SOURCE_SHA="${{ github.event.workflow_run.head_sha }}"', updater_text) + self.assertIn('echo "SOURCE_BRANCH=$SOURCE_BRANCH" >> "$GITHUB_ENV"', updater_text) + self.assertIn('echo "SOURCE_SHA=$SOURCE_SHA" >> "$GITHUB_ENV"', updater_text) + self.assertIn('HEAD_SHA="$SOURCE_SHA"', updater_text) + self.assertIn('checkout --detach --force "${SOURCE_SHA:-}"', updater_text) + self.assertIn('git init --bare "$REPOSITORY_DIR"', updater_text) + self.assertIn("'+refs/heads/*:refs/remotes/origin/*'", updater_text) def test_inline_workflow_exposes_manifest_and_publish_contract(self): text = WORKFLOW.read_text(encoding="utf-8")