Repository navigation
Back-merge main into staging after 0.7.1 #339
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: PR validation | |
| on: | |
| push: | |
| branches: [ "main", "staging" ] | |
| pull_request: | |
| branches: [ "main", "staging" ] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| powershell-tests: | |
| name: PowerShell workflow regressions | |
| runs-on: windows-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Test session build classification | |
| shell: pwsh | |
| run: ./scripts/tests/Test-SessionBuildClassification.ps1 | |
| - name: Test Sandbox UI script contracts | |
| shell: pwsh | |
| run: ./scripts/tests/Test-WinuiUiTestingSandbox.ps1 | |
| - name: Test setup version detection | |
| shell: pwsh | |
| run: ./scripts/tests/Test-SetupVersionDetection.ps1 | |
| vally-lint: | |
| name: Marketplace skill lint (vally) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Setup Node | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: 22 | |
| # Same linter and version github/awesome-copilot runs on external plugins. | |
| - name: Lint skills | |
| run: | | |
| npm ci --prefix scripts/vally --no-audit --no-fund | |
| node scripts/vally/lint-skills.mjs | |
| validate-plugin-manifest: | |
| name: Validate Agent Plugins package | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Validate Agent Plugins 1.0 structure | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| python3 - <<'PY' | |
| import json | |
| import re | |
| import sys | |
| from pathlib import Path | |
| compatibility_root = Path("plugins/winui") | |
| plugin_root = compatibility_root / "agent-plugin" | |
| manifest_path = plugin_root / "plugin.json" | |
| errors = [] | |
| if not manifest_path.is_file(): | |
| errors.append(f"Plugin manifest not found at {manifest_path}") | |
| else: | |
| try: | |
| manifest = json.loads(manifest_path.read_text(encoding="utf-8")) | |
| except (json.JSONDecodeError, UnicodeDecodeError) as error: | |
| errors.append(f"{manifest_path} is not valid UTF-8 JSON: {error}") | |
| manifest = {} | |
| schema = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json" | |
| allowed = { | |
| "$schema", "name", "version", "description", "author", | |
| "homepage", "repository", "license", "keywords", "extensions", | |
| } | |
| if not isinstance(manifest, dict): | |
| errors.append("plugin.json must contain a top-level object") | |
| manifest = {} | |
| else: | |
| if manifest.get("$schema") != schema: | |
| errors.append(f"plugin.json must declare $schema as {schema}") | |
| name = manifest.get("name") | |
| if not isinstance(name, str) or not re.fullmatch( | |
| r"[a-z0-9](?:[a-z0-9.-]{0,62}[a-z0-9])?", name or "" | |
| ) or "--" in name or ".." in name: | |
| errors.append("plugin.json name does not meet Agent Plugins 1.0 constraints") | |
| unknown = sorted(set(manifest) - allowed) | |
| if unknown: | |
| errors.append( | |
| "plugin.json contains non-portable top-level fields: " | |
| + ", ".join(unknown) | |
| ) | |
| for field in ( | |
| "version", "description", "homepage", "repository", "license" | |
| ): | |
| if field in manifest and not isinstance(manifest[field], str): | |
| errors.append(f"plugin.json {field} must be a string") | |
| author = manifest.get("author") | |
| if author is not None: | |
| if not isinstance(author, dict): | |
| errors.append("plugin.json author must be an object") | |
| elif set(author) - {"name", "email", "url"}: | |
| errors.append("plugin.json author contains unsupported fields") | |
| elif any(not isinstance(value, str) for value in author.values()): | |
| errors.append("plugin.json author values must be strings") | |
| keywords = manifest.get("keywords") | |
| if keywords is not None and ( | |
| not isinstance(keywords, list) | |
| or any(not isinstance(keyword, str) for keyword in keywords) | |
| ): | |
| errors.append("plugin.json keywords must be an array of strings") | |
| extensions = manifest.get("extensions") | |
| if extensions is not None and ( | |
| not isinstance(extensions, dict) | |
| or any(not isinstance(value, dict) for value in extensions.values()) | |
| ): | |
| errors.append("plugin.json extensions must map namespaces to objects") | |
| skills_root = plugin_root / "skills" | |
| retired_paths = ( | |
| skills_root / "winui-dev-workflow/BuildAndRun.ps1", | |
| skills_root / "winui-dev-workflow/analyzer", | |
| Path("src/tools/winmd-cli"), | |
| Path("src/tools/winui-analyzer"), | |
| Path("scripts/build-tools.ps1"), | |
| ) | |
| for retired_path in retired_paths: | |
| if retired_path.exists(): | |
| errors.append( | |
| f"Retired local tooling must not be redistributed: {retired_path}" | |
| ) | |
| if not skills_root.is_dir(): | |
| errors.append(f"Portable skills directory not found at {skills_root}") | |
| else: | |
| skill_dirs = sorted(path for path in skills_root.iterdir() if path.is_dir()) | |
| if not skill_dirs: | |
| errors.append(f"No immediate skill directories found under {skills_root}") | |
| for skill_dir in skill_dirs: | |
| if not (skill_dir / "SKILL.md").is_file(): | |
| errors.append(f"Immediate skill directory lacks SKILL.md: {skill_dir}") | |
| forbidden_portable_paths = ( | |
| ".claude-plugin", | |
| ".codex-plugin", | |
| "agents", | |
| "index.js", | |
| "openclaw.plugin.json", | |
| "package.json", | |
| ) | |
| for relative_path in forbidden_portable_paths: | |
| if (plugin_root / relative_path).exists(): | |
| errors.append( | |
| f"Client-specific path must remain outside the portable package: " | |
| f"{plugin_root / relative_path}" | |
| ) | |
| legacy_agent = compatibility_root / "agents/winui-dev.agent.md" | |
| copilot_agent = plugin_root / "com.github.copilot/agents/winui-dev.agent.md" | |
| if not copilot_agent.is_file(): | |
| errors.append(f"Copilot namespaced agent not found at {copilot_agent}") | |
| elif not legacy_agent.is_file(): | |
| errors.append(f"Claude compatibility agent not found at {legacy_agent}") | |
| elif copilot_agent.read_text(encoding="utf-8").rstrip("\r\n") != legacy_agent.read_text(encoding="utf-8").rstrip("\r\n"): | |
| errors.append( | |
| "Copilot and Claude compatibility copies of winui-dev.agent.md have drifted" | |
| ) | |
| def read_json(path): | |
| try: | |
| return json.loads(path.read_text(encoding="utf-8")) | |
| except (OSError, json.JSONDecodeError, UnicodeDecodeError) as error: | |
| errors.append(f"{path} is not valid UTF-8 JSON: {error}") | |
| return {} | |
| claude_manifest = read_json( | |
| compatibility_root / ".claude-plugin/plugin.json" | |
| ) | |
| codex_manifest = read_json( | |
| compatibility_root / ".codex-plugin/plugin.json" | |
| ) | |
| openclaw_manifest = read_json( | |
| compatibility_root / "openclaw.plugin.json" | |
| ) | |
| npm_package = read_json(compatibility_root / "package.json") | |
| github_marketplace = read_json(Path(".github/plugin/marketplace.json")) | |
| claude_marketplace = read_json(Path(".claude-plugin/marketplace.json")) | |
| codex_marketplace = read_json(Path(".agents/plugins/marketplace.json")) | |
| def first_plugin(manifest, path): | |
| plugins = manifest.get("plugins") | |
| if ( | |
| not isinstance(plugins, list) | |
| or not plugins | |
| or not isinstance(plugins[0], dict) | |
| ): | |
| errors.append(f"{path} must contain a non-empty plugins array") | |
| return {} | |
| return plugins[0] | |
| github_entry = first_plugin( | |
| github_marketplace, ".github/plugin/marketplace.json" | |
| ) | |
| claude_entry = first_plugin( | |
| claude_marketplace, ".claude-plugin/marketplace.json" | |
| ) | |
| codex_entry = first_plugin( | |
| codex_marketplace, ".agents/plugins/marketplace.json" | |
| ) | |
| codex_source = codex_entry.get("source") | |
| if not isinstance(codex_source, dict): | |
| errors.append( | |
| ".agents/plugins/marketplace.json plugin source must be an object" | |
| ) | |
| codex_source = {} | |
| adapter_skills = "./agent-plugin/skills/" | |
| if claude_manifest.get("skills") != adapter_skills: | |
| errors.append( | |
| "Claude compatibility manifest must reference canonical portable skills " | |
| f"at {adapter_skills}" | |
| ) | |
| if codex_manifest.get("skills") != adapter_skills: | |
| errors.append( | |
| "Codex compatibility manifest must reference canonical portable skills " | |
| f"at {adapter_skills}" | |
| ) | |
| if openclaw_manifest.get("skills") != ["agent-plugin/skills"]: | |
| errors.append( | |
| "OpenClaw compatibility manifest must reference canonical portable " | |
| "skills at agent-plugin/skills" | |
| ) | |
| package_files = npm_package.get("files", []) | |
| if not isinstance(package_files, list) or "agent-plugin" not in package_files: | |
| errors.append( | |
| "OpenClaw package.json must include the nested agent-plugin package" | |
| ) | |
| expected_sources = ( | |
| ( | |
| ".github/plugin/marketplace.json", | |
| github_entry.get("source"), | |
| "./plugins/winui/agent-plugin", | |
| ), | |
| ( | |
| ".claude-plugin/marketplace.json", | |
| claude_entry.get("source"), | |
| "./plugins/winui", | |
| ), | |
| ( | |
| ".agents/plugins/marketplace.json", | |
| codex_source.get("path"), | |
| "./plugins/winui", | |
| ), | |
| ) | |
| for marketplace, actual, expected in expected_sources: | |
| if actual != expected: | |
| errors.append( | |
| f"{marketplace} must install from {expected}, got {actual!r}" | |
| ) | |
| codex_interface = codex_manifest.get("interface", {}) | |
| expected_artwork = { | |
| "composerIcon": "./agent-plugin/assets/logo.svg", | |
| "logo": "./agent-plugin/assets/logo-512.png", | |
| } | |
| for field, relative_path in expected_artwork.items(): | |
| if codex_interface.get(field) != relative_path: | |
| errors.append( | |
| f"Codex interface.{field} must reference {relative_path}" | |
| ) | |
| elif not (compatibility_root / relative_path.removeprefix("./")).is_file(): | |
| errors.append( | |
| f"Codex interface.{field} asset not found: {relative_path}" | |
| ) | |
| if errors: | |
| for error in errors: | |
| print(f"::error::{error}") | |
| sys.exit(1) | |
| print("Portable Agent Plugins package and legacy client adapters are valid.") | |
| PY | |
| validate-skill-frontmatter: | |
| name: Validate Agent Skills | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Install Agent Skills reference validator | |
| run: >- | |
| python3 -m pip install | |
| "git+https://github.com/agentskills/agentskills.git@69ef37e9424c0a7ea9dd2293b559e43ec8176379#subdirectory=skills-ref" | |
| - name: Validate every portable skill | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| while IFS= read -r -d '' skill; do | |
| skills-ref validate "$(dirname "$skill")" | |
| done < <(find plugins/winui/agent-plugin/skills -mindepth 2 -maxdepth 2 -type f -name SKILL.md -print0) |