"""检查真寻、NoneBot 与 AstrBot 工作流的 YAML 和发布边界。""" import argparse from pathlib import Path import sys def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--dependency-path", type=Path) args = parser.parse_args() if args.dependency_path: sys.path.insert(0, str(args.dependency_path.resolve(strict=True))) import yaml root = Path(__file__).resolve().parents[1] source = yaml.load(( root / ".github/workflows/zhenxun-preview.yml" ).read_text(encoding="utf-8"), Loader=yaml.BaseLoader) generated = yaml.load(( root / "release/zhenxun/verify.yml" ).read_text(encoding="utf-8"), Loader=yaml.BaseLoader) if source["permissions"] != {"contents":"read"}: raise ValueError("Source workflow permissions differ") trigger = source["on"] if set(trigger) != {"push", "pull_request", "workflow_dispatch"}: raise ValueError("Unsafe or missing workflow trigger") inputs = trigger["workflow_dispatch"]["inputs"] if set(inputs) != {"publish"}: raise ValueError("Version must come from the root version.txt file") if inputs["publish"]["default"] != "false": raise ValueError("Publish must be opt-in") sync = source["jobs"]["sync-preview"] expected_condition = ( "(github.event_name == 'push' || " "(github.event_name == 'workflow_dispatch' && inputs.publish)) && " "github.ref == 'refs/heads/main' && " "github.repository == 'Shu-Ying/main_plugin_farm'" ) if (sync["environment"] != "zhenxun-preview" or " ".join(sync["if"].split()) != expected_condition or trigger["push"]["branches"] != ["main"] or source["jobs"]["build"]["needs"] != "test" or sync["needs"] != ["test", "build"]): raise ValueError("Publish boundary differs") if generated["permissions"] != {"contents":"read"}: raise ValueError("Generated workflow permissions differ") if generated["on"]["push"]["branches"] != ["main"]: raise ValueError("Generated branch differs") nonebot = yaml.load(( root / ".github/workflows/nonebot-preview.yml" ).read_text(encoding="utf-8"), Loader=yaml.BaseLoader) astrbot = yaml.load(( root / ".github/workflows/astrbot-preview.yml" ).read_text(encoding="utf-8"), Loader=yaml.BaseLoader) for workflow in (source, nonebot, astrbot): install_commands = [ step.get("run", "").split() for step in workflow["jobs"]["test"]["steps"] ] if not any( any(tokens[index:index + 2] == [ "-r", "integrations/astrbot/requirements.txt", ] for index in range(len(tokens) - 1)) for tokens in install_commands ): raise ValueError("Full test suite requires AstrBot dependencies") for platform, workflow in ( ('zhenxun', source), ('nonebot', nonebot), ('astrbot', astrbot), ): steps = workflow['jobs']['build']['steps'] generation = next((i for i, step in enumerate(steps) if 'scripts/preview_help.py' in step.get('run', '')), None) build = next((i for i, step in enumerate(steps) if f'scripts/build_{platform}.py' in step.get('run', '')), None) if generation is None or build is None or generation >= build: raise ValueError('Help menu must be generated before packaging') if (f'--platform {platform}' not in steps[generation]['run'] or f'help-{platform}.png' not in steps[build]['run']): raise ValueError('Workflow help image platform mismatch') if nonebot["permissions"] != {"contents": "read"}: raise ValueError("NoneBot source permissions differ") trigger = nonebot["on"] if (set(trigger) != {"push", "pull_request", "workflow_dispatch"} or trigger["push"]["branches"] != ["main"] or set(trigger["workflow_dispatch"]["inputs"]) != {"publish"} or trigger["workflow_dispatch"]["inputs"]["publish"]["default"] != "false"): raise ValueError("NoneBot source triggers differ") sync = nonebot["jobs"]["sync-preview"] if (sync["environment"] != "zhenxun-preview" or " ".join(sync["if"].split()) != expected_condition or nonebot["jobs"]["build"]["needs"] != "test" or sync["needs"] != ["test", "build"]): raise ValueError("NoneBot sync boundary differs") token = sync["steps"][-1]["env"]["FARM_RELEASE_TOKEN"] if token != "${{ secrets.FARM_RELEASE_TOKEN }}": raise ValueError("NoneBot must reuse FARM_RELEASE_TOKEN") if '--release-tag' not in sync['steps'][-1]['run'].split(): raise ValueError('NoneBot sync must create its version release tag') generated = yaml.load(( root / "release/nonebot/verify.yml" ).read_text(encoding="utf-8"), Loader=yaml.BaseLoader) if (generated["permissions"] != {"contents": "read"} or generated["on"]["push"]["branches"] != ["main"]): raise ValueError("NoneBot generated verification boundary differs") publishing = yaml.load(( root / "release/nonebot/publish.yaml" ).read_text(encoding="utf-8"), Loader=yaml.BaseLoader) if (publishing["permissions"] != {"contents": "read"} or publishing["on"] != {"push": {"tags": ["v*"]}} or publishing["jobs"]["publish"]["needs"] != "build" or publishing["jobs"]["publish"]["if"] != ( "github.repository == 'Shu-Ying/nonebot_plugin_farm'" )): raise ValueError("NoneBot PyPI boundary differs") for workflow, job in ((generated, 'verify'), (publishing, 'build')): steps = workflow['jobs'][job]['steps'] if not any(step.get('run') == ( 'python -m build --sdist --wheel --outdir dist' ) for step in steps): raise ValueError('NoneBot distributions must build inside dist') upload = next(step for step in steps if step.get('uses', '').startswith( 'actions/upload-artifact@')) if (upload['with']['path'] != 'dist/*' or upload['with'].get('if-no-files-found') != 'error'): raise ValueError('NoneBot upload path must be dist/*') if not any(step.get('run') == 'python -m twine check dist/*' for step in publishing['jobs']['build']['steps']): raise ValueError('NoneBot distribution check must use dist/*') steps = publishing["jobs"]["publish"]["steps"] if steps[-1]["with"]["password"] != "${{ secrets.PYPI_TOKEN }}": raise ValueError("Keep the existing PyPI secret") trigger = astrbot["on"] sync = astrbot["jobs"]["sync-preview"] if (astrbot["permissions"] != {"contents": "read"} or set(trigger) != set(nonebot["on"]) or trigger["push"]["branches"] != ["main"] or set(trigger["workflow_dispatch"]["inputs"]) != {"publish"} or trigger["workflow_dispatch"]["inputs"]["publish"]["default"] != "false" or sync["environment"] != "zhenxun-preview" or " ".join(sync["if"].split()) != expected_condition or astrbot["jobs"]["build"]["needs"] != "test" or sync["needs"] != ["test", "build"] or sync["steps"][-1]["env"]["FARM_RELEASE_TOKEN"] != "${{ secrets.FARM_RELEASE_TOKEN }}"): raise ValueError("AstrBot workflow publication boundary differs") print("Workflow YAML and publication guards checked") if __name__ == "__main__": main()