Skip to content

Release 0.6.0

Release 0.6.0 #259

Workflow file for this run

name: PR validation
on:
push:
branches: [ "main", "staging" ]
pull_request:
branches: [ "main", "staging" ]
workflow_dispatch:
permissions:
contents: read
jobs:
build-tools:
name: Build C# tools
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: |
8.0.x
10.0.x
- name: Build Microsoft.WindowsAppSDK.Analyzers (Roslyn analyzer)
# No -warnaserror flag here: the analyzer subtree's Directory.Build.props
# already turns it on for production code, and the test csproj opts out.
# The CLI flag would force it on for tests too, blocking xUnit naming
# conventions like Suppress_Wui4101 (CA1707).
run: dotnet build src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.slnx -c Release
- name: Test Microsoft.WindowsAppSDK.Analyzers
run: dotnet test src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers.Tests/Microsoft.WindowsAppSDK.Analyzers.Tests.csproj -c Release --no-build --logger "console;verbosity=normal"
- name: Build winmd-cli
run: dotnet build src/tools/winmd-cli/winmd.csproj -c Release
- name: Upload analyzer DLL artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Microsoft.WindowsAppSDK.Analyzers-built
path: src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers/bin/Release/netstandard2.0/Microsoft.WindowsAppSDK.Analyzers.dll
if-no-files-found: error
analyzer-provenance:
name: Analyzer DLL provenance
needs: build-tools
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download CI-built analyzer DLL
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: Microsoft.WindowsAppSDK.Analyzers-built
path: ci-built/
- name: Compare CI-built vs committed analyzer DLL
shell: bash
run: |
set -euo pipefail
COMMITTED="plugins/winui/agent-plugin/skills/winui-dev-workflow/analyzer/Microsoft.WindowsAppSDK.Analyzers.dll"
CI_BUILT="ci-built/Microsoft.WindowsAppSDK.Analyzers.dll"
if [[ ! -f "$COMMITTED" ]]; then
echo "::error::Committed analyzer DLL not found at $COMMITTED"
exit 1
fi
COMMITTED_HASH=$(sha256sum "$COMMITTED" | awk '{print $1}')
CI_HASH=$(sha256sum "$CI_BUILT" | awk '{print $1}')
COMMITTED_SIZE=$(stat -c %s "$COMMITTED")
CI_SIZE=$(stat -c %s "$CI_BUILT")
echo "Committed: $COMMITTED ($COMMITTED_SIZE bytes, sha256=$COMMITTED_HASH)"
echo "CI-built: $CI_BUILT ($CI_SIZE bytes, sha256=$CI_HASH)"
if [[ "$COMMITTED_HASH" == "$CI_HASH" ]]; then
echo "::notice::Analyzer DLL hash matches — provenance verified."
exit 0
fi
# Hashes differ. Distinguish "source changed without rebuild" (real
# problem) from "deterministic-build drift" (toolchain / SDK delta;
# less alarming) by comparing sizes as a coarse proxy.
SIZE_DELTA=$(( CI_SIZE - COMMITTED_SIZE ))
ABS_DELTA=${SIZE_DELTA#-}
if [[ "$ABS_DELTA" -gt 256 ]]; then
echo "::error::Analyzer DLL hash mismatch and size differs by $SIZE_DELTA bytes."
echo "::error::This usually means src/tools/winui-analyzer/ was changed without rebuilding and recommitting Microsoft.WindowsAppSDK.Analyzers.dll."
echo "::error::Run: dotnet build src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers/Microsoft.WindowsAppSDK.Analyzers.csproj -c Release"
echo "::error::Then copy bin/Release/netstandard2.0/Microsoft.WindowsAppSDK.Analyzers.dll into $COMMITTED and commit."
exit 1
else
echo "::warning::Analyzer DLL hash differs but size delta is small ($SIZE_DELTA bytes) — likely deterministic-build drift across SDK versions, not a source change. Investigate before merging."
fi
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"
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)
analyzer-targets-sync:
name: Analyzer .targets in sync
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Compare duplicated analyzer .targets files
shell: bash
run: |
set -euo pipefail
# The analyzer .targets file is currently committed in two places:
# - source-of-truth in the source tree
# - distribution copy under the skill payload
# They MUST be byte-identical until the analyzer is published as a
# NuGet package and the skill payload copy goes away
# (see launch tracker §12.3 / P1-NEW-D).
SRC="src/tools/winui-analyzer/Microsoft.WindowsAppSDK.Analyzers/Microsoft.WindowsAppSDK.Analyzers.targets"
DIST="plugins/winui/agent-plugin/skills/winui-dev-workflow/analyzer/Microsoft.WindowsAppSDK.Analyzers.targets"
for f in "$SRC" "$DIST"; do
if [[ ! -f "$f" ]]; then
echo "::error::Expected analyzer .targets at $f"
exit 1
fi
done
if ! diff -q "$SRC" "$DIST" >/dev/null; then
echo "::error::$SRC and $DIST have drifted."
echo "::error::These files must be byte-identical. Update both copies in the same commit."
diff -u "$SRC" "$DIST" || true
exit 1
fi
echo "Analyzer .targets files are in sync."