Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,16 @@ updates:

cooldown:
default-days: 7

# vally skill linter (pinned to match github/awesome-copilot's marketplace gate)
- package-ecosystem: "npm"
directory: "/scripts/vally"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 2
labels:
- "dependencies"
commit-message:
prefix: "ci"
include: "scope"
39 changes: 19 additions & 20 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,24 @@ jobs:
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
Expand Down Expand Up @@ -137,27 +155,8 @@ jobs:
if not skill_dirs:
errors.append(f"No immediate skill directories found under {skills_root}")
for skill_dir in skill_dirs:
skill_md = skill_dir / "SKILL.md"
if not skill_md.is_file():
if not (skill_dir / "SKILL.md").is_file():
errors.append(f"Immediate skill directory lacks SKILL.md: {skill_dir}")
continue
# Mirrors vally's valid-refs lint used by marketplaces such as
# awesome-copilot: markdown links must resolve inside the skill
# directory. Name sibling skills in plain text instead.
body = re.sub(r"(?ms)^(```|~~~).*?^\1", "", skill_md.read_text(encoding="utf-8"))
body = re.sub(r"`[^`\n]*`", "", body)
targets = re.findall(r"\]\(([^)\s]+)", body) + re.findall(r"(?m)^\s*\[[^\]]+\]:\s*(\S+)", body)
for target in targets:
if re.match(r"^[A-Za-z][A-Za-z0-9+.-]*:", target) or target.startswith("#"):
continue
relative = target.split("#", 1)[0]
if not relative:
continue
resolved = (skill_dir / relative).resolve()
if not resolved.is_relative_to(skill_dir.resolve()):
errors.append(f"{skill_md}: link '{target}' points outside the skill directory; name the other skill in plain text")
elif not resolved.exists():
errors.append(f"{skill_md}: link '{target}' does not exist")

forbidden_portable_paths = (
".claude-plugin",
Expand Down
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ check will (correctly) refuse to let the version-bump diff land on staging.
| `staging-up-to-date-with-main` | PR targets `staging` | PR head contains every commit on `main` (back-merge PRs satisfy this naturally). |
| `powershell-tests` | Any PR | Session classification, documented Sandbox test-script behavior, and setup version detection pass focused regression tests. |
| `validate-plugin-manifest` + `validate-skill-frontmatter` | Any PR | Manifests are well-formed, every `SKILL.md` has valid frontmatter. |
| `vally-lint` | Any PR | Skills pass the same [vally](https://github.com/microsoft/vally) lint marketplaces run (e.g. awesome-copilot). Links in a `SKILL.md` must stay inside that skill's folder — name other skills in plain text. |

If a check fails, the failure message tells you exactly what to fix.

Expand All @@ -152,6 +153,13 @@ pwsh -NoProfile -File .\scripts\tests\Test-WinuiUiTestingSandbox.ps1
pwsh -NoProfile -File .\scripts\tests\Test-SetupVersionDetection.ps1
```

To run the marketplace skill lint locally (Node 22+):

```powershell
npm ci --prefix scripts/vally
node scripts/vally/lint-skills.mjs
```

These checks do not replace exercising the published CLI/analyzer with a real
app before releasing changed build, packaging, AOT, or Sandbox guidance.

Expand Down
4 changes: 2 additions & 2 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ If the helper doesn't work for some reason:

Before merging:

- ✅ All status checks green (`powershell-tests`, `version-bump`,
- ✅ All status checks green (`powershell-tests`, `vally-lint`, `version-bump`,
`changelog-entry`).
- ✅ External tools the skills depend on (WinApp CLI, the analyzer NuGet
package) are published at the versions the skills require.
Expand Down Expand Up @@ -171,7 +171,7 @@ the CI workflows alone are not enough.

2. **Branch protection on `staging`** (CRITICAL — strict mode is REQUIRED, not optional):
- Require PR before merging.
- Require status checks: `powershell-tests`,
- Require status checks: `powershell-tests`, `vally-lint`,
`validate-plugin-manifest`,
`validate-skill-frontmatter`, `version-sync`,
`staging-up-to-date-with-main`.
Expand Down
25 changes: 25 additions & 0 deletions scripts/vally/lint-skills.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Runs the same vally lint that marketplaces such as github/awesome-copilot
// apply to external plugins, so link/spec problems fail our CI first.
// Mirrors awesome-copilot's eng/external-plugin-quality-gates.mjs: lint each
// skills path declared in plugin.json, or the plugin root when none is declared.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { runLint, LintConsoleReporter } from "@microsoft/vally";

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const pluginRoot = path.join(repoRoot, "plugins", "winui", "agent-plugin");
const manifest = JSON.parse(fs.readFileSync(path.join(pluginRoot, "plugin.json"), "utf8"));

const skillPaths = [].concat(manifest.skills ?? [])
.map((p) => path.resolve(pluginRoot, p))
.filter((p) => fs.existsSync(p) && fs.statSync(p).isDirectory());
const targets = skillPaths.length > 0 ? skillPaths : [pluginRoot];

let passed = true;
for (const target of targets) {
const result = await runLint({ rootPath: target });
await new LintConsoleReporter({ verbose: true, stream: process.stdout }).report(result);
passed &&= result.passed;
}
process.exit(passed ? 0 : 1);
Loading
Loading