diff --git a/.gitea/workflows/prepare.yml b/.gitea/workflows/prepare.yml index 27e0a968..0a00f983 100644 --- a/.gitea/workflows/prepare.yml +++ b/.gitea/workflows/prepare.yml @@ -14,7 +14,7 @@ on: - main workflow_dispatch: schedule: - # 北京时间(UTC+8)每日 06:00;Prepare 成功后由 workflow_run 触发 thirdparty 快照更新。 + # 北京时间(UTC+8)每日 06:00;Prepare 成功后内部调用 thirdparty updater。 - cron: "0 22 * * *" concurrency: @@ -293,3 +293,19 @@ 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 ea5ff510..7e42269d 100644 --- a/.gitea/workflows/update-thirdparty-skills.yml +++ b/.gitea/workflows/update-thirdparty-skills.yml @@ -1,6 +1,19 @@ 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 workflow_run: workflows: ["🧰 准备环境"] types: @@ -15,8 +28,8 @@ env: ACCESS_TOKEN: ${{ secrets.WORKFLOW }} WORKSPACE_ROOT: "/data/workspace" WORKSPACE_SLOT: "thirdparty" - SOURCE_BRANCH: ${{ github.event.workflow_run.head_branch }} - SOURCE_SHA: ${{ github.event.workflow_run.head_sha }} + 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]" @@ -24,8 +37,9 @@ env: jobs: update_and_sync: - # 仅在 Prepare 于主分支成功后触发(含每日 schedule 触发的 Prepare)。 - if: ${{ github.event.workflow_run.conclusion == 'success' && (github.event.workflow_run.head_branch == 'main' || github.event.workflow_run.head_branch == 'master') }} + # 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')) }} name: 📥 更新快照并同步 main runs-on: standard-ubuntu-22 permissions: diff --git a/test/test_thirdparty_skills_pipeline.py b/test/test_thirdparty_skills_pipeline.py index 2c0affd9..3b4bea95 100644 --- a/test/test_thirdparty_skills_pipeline.py +++ b/test/test_thirdparty_skills_pipeline.py @@ -11,6 +11,7 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[1] MANIFEST = ROOT / ".gitea" / "ci" / "thirdparty_skills.json" WORKFLOW = ROOT / ".gitea" / "workflows" / "update-thirdparty-skills.yml" +PREPARE_WORKFLOW = ROOT / ".gitea" / "workflows" / "prepare.yml" TSL_SYNC_WORKFLOW = ROOT / ".gitea" / "workflows" / "sync-tsl-playbook.yml" LEGACY_WORKFLOW = ROOT / ".gitea" / "workflows" / "update-thirdparty-superpowers.yml" UPDATE_SCRIPT = ROOT / ".gitea" / "ci" / "update_thirdparty_skills.sh" @@ -21,6 +22,21 @@ def load_manifest() -> dict: return json.loads(MANIFEST.read_text(encoding="utf-8")) +def workflow_mapping_keys(text: str, section: str) -> list[str]: + lines = text.splitlines() + section_line = f"{section}:" + start = lines.index(section_line) + 1 + keys = [] + for line in lines[start:]: + if line and not line.startswith(" "): + break + if line.startswith(" ") and not line.startswith(" "): + stripped = line.strip() + if stripped.endswith(":") and not stripped.startswith("#"): + keys.append(stripped[:-1]) + return keys + + def bash_path(path: Path) -> str: resolved = path.resolve() if os.name != "nt": @@ -212,6 +228,45 @@ 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): + 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", "update_thirdparty_scheduled"], + ) + self.assertEqual( + workflow_mapping_keys(updater_text, "on"), + ["workflow_call", "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) + def test_inline_workflow_exposes_manifest_and_publish_contract(self): text = WORKFLOW.read_text(encoding="utf-8") self.assertIn('MANIFEST_PATH="${MANIFEST_PATH:-.gitea/ci/thirdparty_skills.json}"', text)