fix: require reuse review for custom code #31
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: Validate Skills Marketplace | |
| on: | |
| push: | |
| pull_request: | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: validate-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| validate: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v6 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| - name: Install Claude Code validator | |
| run: npm install --global @anthropic-ai/claude-code@2.1.207 | |
| - name: Validate Claude marketplace | |
| run: claude plugin validate . --strict | |
| - name: Validate shared plugin structure | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $claude = Get-Content .claude-plugin/marketplace.json -Raw | ConvertFrom-Json | |
| $codex = Get-Content .agents/plugins/marketplace.json -Raw | ConvertFrom-Json | |
| $claudeNames = @($claude.plugins.name) | |
| $codexNames = @($codex.plugins.name) | |
| if (($claudeNames -join "|") -ne ($codexNames -join "|")) { | |
| throw "Claude and Codex plugin names or order differ." | |
| } | |
| $pluginDirectories = @(Get-ChildItem plugins -Directory) | |
| if ($pluginDirectories.Count -ne $claudeNames.Count) { | |
| throw "Catalog and plugin-directory counts differ." | |
| } | |
| $skillNames = [Collections.Generic.HashSet[string]]::new() | |
| foreach ($entry in $claude.plugins) { | |
| $pluginRoot = Join-Path $PWD ($entry.source -replace "^\./", "") | |
| $manifestPath = Join-Path $pluginRoot ".codex-plugin/plugin.json" | |
| if (-not (Test-Path -LiteralPath $manifestPath)) { | |
| throw "Missing manifest for $($entry.name)." | |
| } | |
| $manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json | |
| if ($manifest.name -ne $entry.name) { | |
| throw "Manifest name differs for $($entry.name)." | |
| } | |
| if ($manifest.description -ne $entry.description) { | |
| throw "Manifest description differs for $($entry.name)." | |
| } | |
| if ($manifest.version -notmatch "^\d+\.\d+\.\d+$") { | |
| throw "Manifest version is not SemVer for $($entry.name)." | |
| } | |
| foreach ($field in @("license", "homepage", "repository")) { | |
| if ([string]::IsNullOrWhiteSpace($manifest.$field)) { | |
| throw "Missing $field for $($entry.name)." | |
| } | |
| } | |
| $skillsRoot = Join-Path $pluginRoot "skills" | |
| foreach ($skillDirectory in Get-ChildItem $skillsRoot -Directory) { | |
| $skillPath = Join-Path $skillDirectory.FullName "SKILL.md" | |
| if (-not (Test-Path -LiteralPath $skillPath)) { | |
| throw "Missing SKILL.md in $($skillDirectory.Name)." | |
| } | |
| $lines = [IO.File]::ReadAllLines($skillPath) | |
| if ($lines.Count -lt 100 -or $lines.Count -gt 200) { | |
| throw "$($skillDirectory.Name) has $($lines.Count) lines; expected 100-200." | |
| } | |
| if ($lines[0] -ne "---" -or $lines[3] -ne "---") { | |
| throw "$($skillDirectory.Name) frontmatter must contain only name and description." | |
| } | |
| $name = ($lines[1] -replace "^name:\s*", "").Trim('"') | |
| $description = ($lines[2] -replace "^description:\s*", "").Trim('"') | |
| if ($name -ne $skillDirectory.Name) { | |
| throw "Folder and frontmatter names differ for $($skillDirectory.Name)." | |
| } | |
| if ($description.Length -gt 200) { | |
| throw "$name description exceeds 200 characters." | |
| } | |
| if (-not $skillNames.Add($name)) { | |
| throw "Duplicate skill name: $name." | |
| } | |
| } | |
| } | |
| if (Test-Path -LiteralPath mcp) { | |
| throw "The retired MCP source tree must not be restored." | |
| } | |
| Write-Host "Validated $($claudeNames.Count) plugins and $($skillNames.Count) standalone skills." | |
| - name: Validate static site | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $html = Get-Content site/index.html -Raw | |
| $ids = @([regex]::Matches($html, 'id="([^"]+)"') | ForEach-Object { $_.Groups[1].Value }) | |
| $hrefs = @([regex]::Matches($html, 'href="([^"]+)"') | ForEach-Object { $_.Groups[1].Value }) | |
| foreach ($href in $hrefs) { | |
| if ($href.StartsWith("#") -and $ids -notcontains $href.Substring(1)) { | |
| throw "Missing site anchor: $href." | |
| } | |
| if (-not $href.StartsWith("#") -and -not $href.StartsWith("http") -and -not $href.StartsWith("data:")) { | |
| $path = ($href -split "#")[0] | |
| if ($path -and -not (Test-Path (Join-Path site $path))) { | |
| throw "Missing site asset: $href." | |
| } | |
| } | |
| } | |
| if (Test-Path -LiteralPath site/mcp) { | |
| throw "Retired MCP compatibility pages must not be restored." | |
| } | |
| Write-Host "Validated static-site anchors and local assets." |