diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 8034f81..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: 发布扩展 - -on: - push: - tags: - - "v*" - -permissions: - contents: write - -jobs: - release: - runs-on: ubuntu-latest - steps: - - name: 检出代码 - uses: actions/checkout@v6 - - - name: 配置 uv - uses: astral-sh/setup-uv@v7 - with: - python-version: "3.11" - - - name: 运行测试 - run: uv run --frozen pytest - - - name: 构建扩展 - run: uv run --frozen pack - - - name: 发布扩展包 - uses: https://gitea.com/actions/gitea-release-action@v1.3.6 - with: - files: |- - dist/*.zip diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index aa41e51..0000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: 构建并部署文档 - -on: - push: - branches: - - main - paths: - - docs/** - - mkdocs.yml - - .github/workflows/docs.yml - workflow_dispatch: - -permissions: - contents: read - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: 检出代码 - uses: actions/checkout@v6 - - - name: 配置 uv - uses: astral-sh/setup-uv@v7 - - - name: 构建文档 - run: uv run --frozen --only-group docs mkdocs build --strict --site-dir site - - - name: 整理发布目录 - run: | - mkdir -p public/anyimage - cp -a site/. public/anyimage/ - - - name: 配置 Node.js - uses: actions/setup-node@v6 - with: - node-version: "24" - - - name: 部署到 Cloudflare Workers - env: - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_WORKER_NAME: ${{ vars.CLOUDFLARE_WORKER_NAME }} - run: | - test -n "$CLOUDFLARE_ACCOUNT_ID" - test -n "$CLOUDFLARE_API_TOKEN" - test -n "$CLOUDFLARE_WORKER_NAME" - npx --yes wrangler@4 deploy \ - --assets ./public \ - --name "$CLOUDFLARE_WORKER_NAME" \ - --compatibility-date 2026-08-19 \ - --keep-vars \ - --message "AnyImage docs $GITHUB_SHA" \ - --autoconfig=false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..74bb294 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,70 @@ +name: Release + +on: + push: + tags: + # Publish on any tag starting with a `v`, e.g., v1.0.1 + - "v*" + +permissions: + contents: write + +jobs: + check_version: + name: Check Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.read_version.outputs.version }} + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + python-version: "3.11" + + - name: Check tag matches project version + id: read_version + run: | + version="$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")" + if [ "v$version" != "$GITHUB_REF_NAME" ]; then + echo "Tag $GITHUB_REF_NAME does not match pyproject.toml version $version" >&2 + exit 1 + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + + build_extensions: + name: Build Extensions + needs: check_version + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + python-version: "3.11" + + - name: Run tests + run: uv run pytest + + - name: Pack extensions + run: uv run pack + + - name: Check extension archives + run: | + for platform in windows-x64 macos-arm64 linux-x64; do + archive="dist/AnyImage.v${{ needs.check_version.outputs.version }}.${platform}.zip" + if [ ! -f "$archive" ]; then + echo "Missing archive: $archive" >&2 + exit 1 + fi + done + + - name: Publish release + uses: https://gitea.com/actions/gitea-release-action@v1.3.6 + with: + files: |- + dist/*.zip diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ca0a8b1..bbb0a56 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: 运行测试 +name: Test on: push: @@ -14,13 +14,13 @@ jobs: test: runs-on: ubuntu-latest steps: - - name: 检出代码 + - name: Checkout uses: actions/checkout@v6 - - name: 配置 uv + - name: Install uv uses: astral-sh/setup-uv@v7 with: python-version: "3.11" - - name: 运行测试 - run: uv run --frozen pytest + - name: Run tests + run: uv run pytest diff --git a/AGENTS.md b/AGENTS.md index 73dadbb..69e0741 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,11 +46,20 @@ uv run pytest # 构建 Windows、macOS、Linux 扩展包到 dist/ uv run pack +# 本地构建多版本静态页面(只提交到本地 gh-pages 分支,不推送) +uv run docs build + +# 本地热更新预览当前文档 +uv run docs dev + +# 用 pyproject.toml 中的 major.minor.x 发布文档并让 latest 指向它 +uv run docs deploy + # 构建并验证 Blender 节点资产 uv run node-group build # 准备、校验并同步 ONNX 模型到 R2 -uv run sync-models +uv run models sync ``` ## 命名规范 @@ -76,6 +85,8 @@ uv run sync-models - `README.md` 面向用户,不在其中堆叠内部细节。实现原理放入 `docs/internals` 对应主题 - 文档只正面描述当前主题的职责与实现,不记录无关模块、未采用路径、历史差异、边界辩解、“不做什么”的排除性信息。 - 根据内容选择最清晰的表达方式,优先简洁文字;复杂流程或关系在有助于理解时使用图表。Mermaid 流程图默认从上到下。 +- 文档版本只使用 `pyproject.toml` 中的 major.minor,写成 `x.y.x` 系列标识,不写具体 patch;`uv run docs build` 和 `uv run docs deploy` 都构建到该版本并更新 `latest` +- mike 默认只提交不推送,`--push` 不能省;内容没有变化时 mike 会跳过提交并连带跳过推送,所以 `uv run docs deploy` 额外带 `--allow-empty`;mike 默认推送到 `origin`,本仓库的远端名是 `omoolab` ## Blender 开发规范 diff --git a/mkdocs.yml b/mkdocs.yml index 72d5397..c4f221c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -36,7 +36,6 @@ nav: theme: name: material - favicon: assets/logo.png logo: assets/logo.png language: zh font: @@ -65,6 +64,7 @@ theme: toggle: icon: fontawesome/solid/moon name: Switch to system preference + features: - header.autohide - navigation.footer @@ -81,14 +81,25 @@ extra: extra_css: - stylesheets/extra.css - + extra_javascript: - javascripts/extra.js -copyright: Copyright © 2026 OmooLab +copyright: Copyright © 2020 - 2024 Omoolab plugins: - search + - mkdocstrings + - i18n: + docs_structure: suffix + languages: + - locale: zh + name: 中文 + default: true + - locale: en + name: English + nav_translations: + 快捷键: Keymap markdown_extensions: - mkdocs-click diff --git a/pyproject.toml b/pyproject.toml index c93c7ea..d087bd0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,13 +9,14 @@ dependencies = ["blendjob==0.1.17", "numpy>=1.26,<2.0", "scipy==1.15.3"] [dependency-groups] dev = [ - "bpy==4.5.3", "huggingface-hub==1.4.1", "mike>=2.1.3,<3", "mkdocs>=1.6.1,<2", "mkdocs-click==0.9.0", "mkdocs-material>=9.6.16,<10", + "mkdocs-static-i18n[material]>=1.3.0", + "mkdocstrings[python]>=1.0.6", "onnx==1.19.0", "onnxruntime==1.24.4", "pillow==12.1.1", diff --git a/tests/tools/test_docs.py b/tests/tools/test_docs.py new file mode 100644 index 0000000..39dfe40 --- /dev/null +++ b/tests/tools/test_docs.py @@ -0,0 +1,80 @@ +from tests.support.paths import PROJECT_ROOT +import tomllib +import unittest + +from tools.docs import documentation_command +from tools.docs import documentation_version + + +MKDOCS_FILE = PROJECT_ROOT / "mkdocs.yml" +DOCS_ROOT = PROJECT_ROOT / "docs" + + +def project_config(): + return tomllib.loads( + (PROJECT_ROOT / "pyproject.toml").read_text(encoding="utf-8") + ) + + +class DocumentationContentTest(unittest.TestCase): + def test_navigation_references_existing_markdown_files(self): + referenced_files = [] + for line in MKDOCS_FILE.read_text(encoding="utf-8").splitlines(): + value = line.rsplit(":", 1)[-1].strip().removeprefix("- ").strip() + if value.endswith(".md"): + referenced_files.append(value) + + self.assertTrue(referenced_files) + for relative_path in referenced_files: + with self.subTest(relative_path=relative_path): + self.assertTrue((DOCS_ROOT / relative_path).is_file()) + + +class DocumentationCommandTest(unittest.TestCase): + def test_documentation_version_omits_the_patch(self): + project_version = project_config()["project"]["version"] + major_minor = ".".join(project_version.split(".")[:2]) + + self.assertEqual( + documentation_version(), + f"{major_minor}.x", + ) + + def test_build_writes_the_multi_version_site_locally(self): + self.assertEqual( + documentation_command("build"), + ("mike", "deploy", "--update-aliases", documentation_version(), "latest"), + ) + self.assertNotIn("--push", documentation_command("build")) + + def test_dev_serves_the_current_documentation_with_hot_reload(self): + self.assertEqual( + documentation_command("dev"), + ("mkdocs", "serve"), + ) + + def test_deploy_publishes_the_version_series_as_latest(self): + self.assertEqual( + documentation_command("deploy"), + ( + "mike", + "deploy", + "--update-aliases", + "--push", + "--allow-empty", + "--remote", + "omoolab", + documentation_version(), + "latest", + ), + ) + + def test_project_exposes_the_docs_command(self): + self.assertEqual( + project_config()["project"]["scripts"]["docs"], + "tools.docs:main", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/docs.py b/tools/docs.py index 73a9bec..32019a4 100644 --- a/tools/docs.py +++ b/tools/docs.py @@ -1,3 +1,5 @@ +"""Build and preview the versioned project documentation.""" + import argparse import subprocess import tomllib @@ -5,6 +7,11 @@ from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[1] +DOCUMENTATION_COMMANDS = { + "dev": ("mkdocs", "serve"), +} +MIKE_DEPLOY_COMMAND = ("mike", "deploy", "--update-aliases") +DOCUMENTATION_REMOTE = "omoolab" def project_version(): @@ -15,33 +22,69 @@ def project_version(): return project["version"] -def documentation_command(dev=False): - """Return the Mike or MkDocs command for the requested mode.""" - if dev: - return ["mkdocs", "serve"] - return [ - "mike", +def documentation_version(): + """Return the major.minor series used for versioned documentation.""" + parts = project_version().split(".") + if len(parts) < 2: + raise ValueError("Project version must contain major and minor parts") + return f"{parts[0]}.{parts[1]}.x" + + +def documentation_command(command): + """Return the command line that runs one documentation subcommand.""" + if command == "build": + # 本地构建多版本站点,只提交到本地 gh-pages 分支,不推送。 + return (*MIKE_DEPLOY_COMMAND, documentation_version(), "latest") + if command == "deploy": + # mike 在内容没有变化时不提交也不推送,--allow-empty 保证部署动作能到达远端。 + return ( + *MIKE_DEPLOY_COMMAND, + "--push", + "--allow-empty", + "--remote", + DOCUMENTATION_REMOTE, + documentation_version(), + "latest", + ) + return DOCUMENTATION_COMMANDS[command] + + +def run(command): + """Run one external command from the project root.""" + print("+", subprocess.list2cmdline([str(part) for part in command]), flush=True) + subprocess.run(command, cwd=PROJECT_ROOT, check=True) + + +def build_parser(): + parser = argparse.ArgumentParser(description="Build or preview the documentation.") + commands = parser.add_subparsers(dest="command") + + commands.add_parser( + "build", + help="Build the current major.minor documentation locally", + ) + commands.add_parser( + "dev", + help="Serve the current documentation with hot reload", + ) + commands.add_parser( "deploy", - "--update-aliases", - project_version(), - "latest", - ] + help="Publish the current major.minor documentation and update latest", + ) + return parser def main(argv=None): - parser = argparse.ArgumentParser(description="Build or preview documentation.") - parser.add_argument( - "-dev", - "--dev", - action="store_true", - help="preview the current documentation with live reload", - ) + parser = build_parser() args = parser.parse_args(argv) - subprocess.run( - documentation_command(args.dev), - cwd=PROJECT_ROOT, - check=True, - ) + if args.command is None: + parser.print_help() + return + try: + run(documentation_command(args.command)) + except KeyboardInterrupt: + # 本地预览用 Ctrl+C 结束,不需要输出回溯。 + pass if __name__ == "__main__": diff --git a/uv.lock b/uv.lock index c942ab2..b4af88b 100644 --- a/uv.lock +++ b/uv.lock @@ -29,6 +29,8 @@ dev = [ { name = "mkdocs" }, { name = "mkdocs-click" }, { name = "mkdocs-material" }, + { name = "mkdocs-static-i18n", extra = ["material"] }, + { name = "mkdocstrings", extra = ["python"] }, { name = "onnx" }, { name = "onnxruntime" }, { name = "pillow" }, @@ -53,6 +55,8 @@ dev = [ { name = "mkdocs", specifier = ">=1.6.1,<2" }, { name = "mkdocs-click", specifier = "==0.9.0" }, { name = "mkdocs-material", specifier = ">=9.6.16,<10" }, + { name = "mkdocs-static-i18n", extras = ["material"], specifier = ">=1.3.0" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=1.0.6" }, { name = "onnx", specifier = "==1.19.0" }, { name = "onnxruntime", specifier = "==1.24.4" }, { name = "pillow", specifier = "==12.1.1" }, @@ -321,6 +325,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, ] +[[package]] +name = "griffelib" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/af/018c10bc9edd42b6ef6db2e96b09542050d5253f9b195e74bc910b2d13ab/griffelib-2.3.0.tar.gz", hash = "sha256:7b0952caf5bca6afa4bb5ee8c6a2d183fe3f21b62efc5f6c7243cb2b26d2d115", size = 234534, upload-time = "2026-09-04T15:08:17.472Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/63/e876e789525063c840ccfa8857febdabd6523bcef9ce7eb979b9305ea895/griffelib-2.3.0-py3-none-any.whl", hash = "sha256:1b8f9cd525681c26b1d6d574faa1371651e8459ca51d209684f50b8096ae06e0", size = 169423, upload-time = "2026-09-04T15:08:12.956Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -524,6 +537,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, ] +[[package]] +name = "mkdocs-autorefs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, +] + [[package]] name = "mkdocs-click" version = "0.9.0" @@ -582,6 +609,59 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, ] +[[package]] +name = "mkdocs-static-i18n" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/2b/59652a2550465fde25ae6a009cb6d74d0f7e724d272fc952685807b29ca1/mkdocs_static_i18n-1.3.0.tar.gz", hash = "sha256:65731e1e4ec6d719693e24fee9340f5516460b2b7244d2a89bed4ce3cfa6a173", size = 1370450, upload-time = "2025-01-24T09:03:24.389Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/f7/ef222a7a2f96ecf79c7c00bfc9dde3b22cd2cc1bd2b7472c7b204fc64225/mkdocs_static_i18n-1.3.0-py3-none-any.whl", hash = "sha256:7905d52fff71d2c108b6c344fd223e848ca7e39ddf319b70864dfa47dba85d6b", size = 21660, upload-time = "2025-01-24T09:03:22.461Z" }, +] + +[package.optional-dependencies] +material = [ + { name = "mkdocs-material" }, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/71/f85bdf13355073ae15a7375f09879375a830553552e58c1c4b7e0bbc5c8b/mkdocstrings-1.0.6.tar.gz", hash = "sha256:a0b8c2bdd29a6416c80d717aa369bbf7831946bd9f23c2a66db1b1dbe7693dbd", size = 100649, upload-time = "2026-07-11T19:38:05.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/5b/4c1902e8bdd5c4db63284e9d101dece4038d4025d6d88850ffe0a1578980/mkdocstrings-1.0.6-py3-none-any.whl", hash = "sha256:2703708697487d1b6d6d7b412e176fa436edf120c1bf81dc9e126b12d00893c7", size = 35787, upload-time = "2026-07-11T19:38:04.417Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/65/0db919b7c91bf033b42d04f15ff16a56f4edd6afb26e84d448359660ece3/mkdocstrings_python-2.0.8.tar.gz", hash = "sha256:34545b54681cf0b212aab95394d37b0f5326a93b102f6ad1df3d965c79384c9d", size = 201771, upload-time = "2026-08-31T20:16:06.698Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/b7/a5a854d4e44853f26decc0c5e0d68df052399f3da344a313da4178f00816/mkdocstrings_python-2.0.8-py3-none-any.whl", hash = "sha256:e555d9ef53b0febdeae4dd603144d4f86ec90fef672276645ea3bb61232c3d24", size = 105443, upload-time = "2026-08-31T20:16:05.25Z" }, +] + [[package]] name = "ml-dtypes" version = "0.5.4"