From 9b3fabb44ee0f82d3e32f6b512635535c2fa67ff Mon Sep 17 00:00:00 2001 From: Aaron Elijah Mars <61592645+aaronjmars@users.noreply.github.com> Date: Mon, 20 Jul 2026 12:55:39 -0400 Subject: [PATCH] ci: gate apps/** and catalog/skill-packs.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two previously ungated surfaces: - apps/** had zero CI despite Dependabot opening monthly grouped PRs against dashboard, mcp-server, and webhook — nothing ever verified those bumps left an app compiling. New ci-apps workflow: dashboard (typecheck + its 156 unit tests + `next build`, kept separate per the documented TS7/Next16 incompatibility), cli (typecheck, borrowing the dashboard's TypeScript since it has none of its own), mcp-server (build), webhook (syntax check + `wrangler deploy --dry-run`, no Cloudflare creds needed). Also added the missing apps/cli entry to dependabot.yml. - catalog/skill-packs.json is the one file routinely hand-edited by outside contributors (every community pack listing), with nothing validating it. New scripts/validate-skill-packs.mjs + ci-skill-packs gate: registry shape (JSON, owner/repo format, non-empty unique skills[], trust_level enum, capabilities against the locked taxonomy read out of bin/install-skill-pack, secrets_required shape) and README parity (every entry has a matching table row and vice versa, skill counts agree, --path flags agree, handles monorepos publishing multiple packs, checks the "N community skill packs" counter). Also catches a self-declared trust_level: trusted that isn't backed by trusted-sources.txt, which is what the installer actually checks. 30 test cases in scripts/tests/test_validate_skill_packs.sh, wired into ci-tests. Docs (CONTRIBUTING.md, PULL_REQUEST_TEMPLATE.md, community-skill-packs.md, apps/cli/README.md) updated to match. --- .github/CONTRIBUTING.md | 12 + .github/PULL_REQUEST_TEMPLATE.md | 2 + .github/dependabot.yml | 16 + .github/workflows/ci-apps.yml | 142 ++++++++ .github/workflows/ci-skill-packs.yml | 56 ++++ .github/workflows/ci-tests.yml | 2 + apps/cli/README.md | 12 + apps/cli/package.json | 3 + apps/dashboard/package.json | 1 + apps/mcp-server/package.json | 1 + docs/community-skill-packs.md | 31 ++ scripts/tests/test_validate_skill_packs.sh | 289 ++++++++++++++++ scripts/validate-skill-packs.mjs | 362 +++++++++++++++++++++ 13 files changed, 929 insertions(+) create mode 100644 .github/workflows/ci-apps.yml create mode 100644 .github/workflows/ci-skill-packs.yml create mode 100755 scripts/tests/test_validate_skill_packs.sh create mode 100644 scripts/validate-skill-packs.mjs diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f5e1045a51..5af9a677c0 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -184,12 +184,24 @@ protect: | `ci-packs-json` | `packs.json` matches a fresh `bin/generate-packs-json` | | `ci-skill-category` | every `SKILL.md` declares a valid `category:` | | `ci-capabilities-parity` | the capabilities taxonomy stays in sync | +| `ci-skill-packs` | `catalog/skill-packs.json` is well-formed **and** matches the README's Community Packs table | +| `ci-apps` | each app in `apps/**` typechecks, tests, and builds | +| `ci-tests` | the `scripts/tests/` suites pass | +| `ci-okf` | the knowledge bundle stays OKF-conformant | +| `ci-agents-md` | `AGENTS.md` is regenerated from `STRATEGY.md` | Run the checks locally before pushing: ```bash bash scripts/check-skill-categories.sh bash scripts/check-capabilities-parity.sh +node scripts/validate-skill-packs.mjs # listing a community pack +``` + +Touching `apps/**`? Run that app's own checks — for the dashboard: + +```bash +cd apps/dashboard && npm ci && npm run typecheck && npm test && npm run build ``` If `ci-skills-json`/`ci-packs-json` fails, you changed a generator input without diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 780323d505..4420c2050e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -38,6 +38,7 @@ - [ ] Pack has a `skills-pack.json` manifest at its root and a `SKILL.md` per skill - [ ] Write / onchain / bet skills are `default_enabled: false` - [ ] This PR adds **both** a README table row and a matching `skill-packs.json` entry +- [ ] `node scripts/validate-skill-packs.mjs` passes (registry shape + README parity, incl. the pack counter) - [ ] No monkey-patching of Aeon internals; no private or auth-walled endpoints required to run ### Core fix (dashboard / scripts / workflows / docs) @@ -45,6 +46,7 @@ - [ ] Change is focused — one concern, no unrelated refactor - [ ] Touched code follows the existing pattern in the file - [ ] Relevant CI gates pass locally (e.g. `bash scripts/check-skill-categories.sh`, `bash scripts/check-capabilities-parity.sh`) +- [ ] Touched `apps/**`? That app typechecks, tests, and builds (dashboard: `npm run typecheck && npm test && npm run build`) --- diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 569b7fe047..22ee454d8f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -67,6 +67,22 @@ updates: patterns: - "*" + # CLI — non-interactive control of an Aeon repo; shares apps/dashboard/lib. + - package-ecosystem: "npm" + directory: "/apps/cli" + schedule: + interval: "monthly" + open-pull-requests-limit: 5 + assignees: + - "aeonfun" + commit-message: + prefix: "chore(deps)" + prefix-development: "chore(deps-dev)" + groups: + cli: + patterns: + - "*" + # Cloudflare Worker that relays Telegram messages to a fork. - package-ecosystem: "npm" directory: "/apps/webhook" diff --git a/.github/workflows/ci-apps.yml b/.github/workflows/ci-apps.yml new file mode 100644 index 0000000000..9c188195bc --- /dev/null +++ b/.github/workflows/ci-apps.yml @@ -0,0 +1,142 @@ +name: ci-apps + +# apps/** was the largest untested surface in the repo: four shipped apps — the +# Next.js dashboard, the CLI, the MCP server, and the Telegram Worker — with no +# gate of any kind. The dashboard alone carries 156 committed unit tests +# (apps/dashboard/lib/**/*.test.ts) that nothing ran, plus a strict-mode +# TypeScript config nothing typechecked. +# +# That gap is not theoretical. Dependabot opens grouped monthly npm PRs against +# apps/dashboard, apps/mcp-server, and apps/webhook (.github/dependabot.yml) — +# every one of them landed with zero automated verification that the app still +# compiles. The TypeScript-7 pin comment in dependabot.yml records a breakage +# caught by hand: `next build` crashed while `tsc --noEmit` passed. That is why +# the dashboard job runs BOTH — a typecheck alone would have missed it. +# +# One job per app so a red X names the broken surface, and so a slow dashboard +# build never masks a fast Worker failure. The workflow-level path filter is +# `apps/**` rather than per-job filters: keeping it native (no third-party +# paths-filter action) is worth re-running a 30-second Worker job on a +# dashboard-only PR. +# +# No secrets, no network beyond the registry: `next build` is fully static here +# (every route is dynamic/server-rendered at request time) and `wrangler +# --dry-run` bundles the Worker without touching a Cloudflare account, so this +# workflow runs green on fork PRs. +# +# Run locally: see each app's README, or the same commands used below. + +on: + pull_request: + paths: + - 'apps/**' + - '.github/workflows/ci-apps.yml' + push: + branches: [main] + paths: + - 'apps/**' + workflow_dispatch: + +permissions: + contents: read + +# A force-push mid-review shouldn't leave a stale build burning a runner. +concurrency: + group: ci-apps-${{ github.ref }} + cancel-in-progress: true + +jobs: + dashboard: + name: dashboard — typecheck, test, build + runs-on: ubuntu-latest + defaults: + run: + working-directory: apps/dashboard + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: '22' + cache: npm + cache-dependency-path: apps/dashboard/package-lock.json + - name: Install deps + run: npm ci + - name: Typecheck (strict) + run: npm run typecheck + - name: Unit tests + run: npm test + # Kept separate from the typecheck on purpose — see the TS7 note above. + - name: Production build + run: npm run build + + cli: + name: cli — typecheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: '22' + cache: npm + cache-dependency-path: | + apps/cli/package-lock.json + apps/dashboard/package-lock.json + # The CLI deliberately shares one source of truth with the dashboard: its + # tsconfig compiles ../dashboard/lib/**/*.ts and borrows that app's + # typescript + @types (apps/cli/tsconfig.json `typeRoots`). So the CLI + # cannot be typechecked without the dashboard's node_modules — installing + # both here is what makes `npm run typecheck` resolve at all. + - name: Install dashboard deps (CLI borrows its typescript + @types) + run: npm ci + working-directory: apps/dashboard + - name: Install CLI deps + run: npm ci + working-directory: apps/cli + - name: Typecheck (strict, includes the shared dashboard lib) + run: npm run typecheck + working-directory: apps/cli + + mcp-server: + name: mcp-server — build + runs-on: ubuntu-latest + defaults: + run: + working-directory: apps/mcp-server + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: '22' + # No package-lock.json is committed for this app, so `npm ci` is not + # available and the install floats within the package.json ranges. + - name: Install deps + run: npm install --no-audit --no-fund + # `npm run build` (tsc, emitting to dist/) rather than a bare --noEmit: + # dist/index.js is this package's published `bin`, so a build that cannot + # emit is a broken install for anyone running `aeon-mcp`. + - name: Build + run: npm run build + + webhook: + name: webhook — worker bundle + runs-on: ubuntu-latest + defaults: + run: + working-directory: apps/webhook + env: + WRANGLER_SEND_METRICS: 'false' + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: '22' + - name: Syntax check the Worker + run: node --check src/worker.js + # No lockfile committed for this app either — see mcp-server above. + - name: Install deps + run: npm install --no-audit --no-fund + # --dry-run validates wrangler.toml and bundles the Worker without any + # Cloudflare credentials, so a broken config or a bad import fails here + # instead of at deploy time. + - name: Bundle (wrangler --dry-run, no credentials needed) + run: npx wrangler deploy --dry-run --outdir "$RUNNER_TEMP/worker-dist" diff --git a/.github/workflows/ci-skill-packs.yml b/.github/workflows/ci-skill-packs.yml new file mode 100644 index 0000000000..4627b22b68 --- /dev/null +++ b/.github/workflows/ci-skill-packs.yml @@ -0,0 +1,56 @@ +name: ci-skill-packs + +# catalog/skill-packs.json is the community pack registry — and the one file in +# this repo that is routinely hand-edited by people outside it. Every listing +# arrives as an outside-contributor PR that must touch TWO surfaces in one diff +# (docs/community-skill-packs.md publishing checklist): a row in the README's +# Community Packs table and a matching registry entry. Nothing checked either. +# +# What shipping a bad edit costs: `bin/install-skill-pack --list` jq's this file +# directly, and the dashboard's community-packs panel fetches it +# (apps/dashboard/lib/packs.ts) — so one trailing comma breaks pack discovery for +# every Aeon fork, not just this repo. A README row with no registry entry is +# invisible to both. A `--path` flag that disagrees with the registry hands +# browsers a copy-paste command that installs the wrong subtree. +# +# The validator also refuses a self-declared `trust_level: trusted` that is not +# in skills/security/trusted-sources.txt: `--list` prints its trust badge from +# the registry but the installer decides the real scan bypass from the trusted +# file, so an unbacked claim advertises "security scan skipped" without earning +# it. That check is why bin/install-skill-pack and trusted-sources.txt are +# trigger paths here — the gate has to re-run when either side moves. +# +# Sibling of ci-packs-json.yml (first-party packs) and ci-skills-json.yml +# (skill catalog); this covers the third, previously ungated, registry. +# +# Run locally: node scripts/validate-skill-packs.mjs + +on: + pull_request: + paths: + - 'catalog/skill-packs.json' + - '.github/README.md' + - 'bin/install-skill-pack' + - 'skills/security/trusted-sources.txt' + - 'scripts/validate-skill-packs.mjs' + - '.github/workflows/ci-skill-packs.yml' + push: + branches: [main] + paths: + - 'catalog/skill-packs.json' + - '.github/README.md' + - 'bin/install-skill-pack' + - 'skills/security/trusted-sources.txt' + - 'scripts/validate-skill-packs.mjs' + workflow_dispatch: + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Validate the community pack registry and its README parity + run: node scripts/validate-skill-packs.mjs diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 06f7fb19ab..874e1c0846 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -42,6 +42,8 @@ jobs: run: bash scripts/tests/test_run_actions_summary.sh - name: skill-pack manifest validation tests run: bash scripts/tests/test_validate_pack.sh + - name: community pack registry validation tests + run: bash scripts/tests/test_validate_skill_packs.sh - name: grok harness runner tests run: bash scripts/tests/test_run_grok.sh - name: state reducer tests diff --git a/apps/cli/README.md b/apps/cli/README.md index 1abbb3300b..3d9ca46c5d 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -95,3 +95,15 @@ Exit code is non-zero on error. (git), `auth.ts` (auth flow), `packs.ts` (pack join). Ten routes now wrap these. - `apps/dashboard/lib/gh.ts` honours `AEON_REPO_ROOT` so the shared lib is location-independent (unset = the dashboard's original cwd-relative behaviour). + +## Typecheck + +```bash +(cd ../dashboard && npm ci) # the CLI borrows the dashboard's typescript + @types +npm ci && npm run typecheck # strict; compiles src/ plus the shared dashboard lib +``` + +Because `tsconfig.json` compiles `../dashboard/lib/**/*.ts` and points +`typeRoots` at that app, the CLI has no `typescript` dependency of its own and +cannot be typechecked without the dashboard's `node_modules`. CI does exactly +the two installs above — see `.github/workflows/ci-apps.yml`. diff --git a/apps/cli/package.json b/apps/cli/package.json index f6e5fa2d33..0d719110d3 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -8,6 +8,9 @@ "bin": { "aeon": "./aeon" }, + "scripts": { + "typecheck": "../dashboard/node_modules/.bin/tsc --noEmit -p tsconfig.json" + }, "dependencies": { "yaml": "^2.9.0" }, diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 47e955f96f..0eae1269ea 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -7,6 +7,7 @@ "dev": "next dev", "build": "next build", "start": "next start", + "typecheck": "tsc --noEmit", "test": "node --import tsx --test 'lib/**/*.test.mjs' 'lib/**/*.test.ts'" }, "dependencies": { diff --git a/apps/mcp-server/package.json b/apps/mcp-server/package.json index 3d9c193fbb..106ff41d9d 100644 --- a/apps/mcp-server/package.json +++ b/apps/mcp-server/package.json @@ -10,6 +10,7 @@ }, "scripts": { "build": "tsc", + "typecheck": "tsc --noEmit", "dev": "tsc --watch", "start": "node dist/index.js" }, diff --git a/docs/community-skill-packs.md b/docs/community-skill-packs.md index 85c5d7b5ac..ef230ab96a 100644 --- a/docs/community-skill-packs.md +++ b/docs/community-skill-packs.md @@ -233,6 +233,37 @@ The operator is always the trust boundary. The install script does not auto-trus Pack maintainers update both in the same PR so the two surfaces stay in lockstep. +### CI validates both (run it before you open the PR) + +`ci-skill-packs` gates every PR that touches the registry or the README: + +```bash +node scripts/validate-skill-packs.mjs +``` + +It fails the PR on registry shape (unparseable JSON, a `repo` that isn't +`owner/repo`, an empty or duplicated `skills[]`, a `trust_level` outside +`trusted|community`, a capability outside the [locked taxonomy](CAPABILITIES.md), +a `secrets_required` entry that isn't an env var name) and on README parity (an +entry with no table row or a row with no entry, a skill count that disagrees with +`skills[]`, a `--path` flag that disagrees with the registry's `path`, and the +`N community skill packs` counter in the README's Proof of work section). + +Two things to know: + +- **`trust_level: trusted` must be earned.** `--list` prints its trust badge from + this registry, but the installer decides the real scan bypass from + `skills/security/trusted-sources.txt`. A `trusted` entry that isn't in that + file advertises "security scan skipped" without ever getting it, so the gate + rejects it. Community packs use `trust_level: community`. +- **A subdirectory pack needs its `--path` in the README row.** If your registry + entry sets `path`, the table row has to show the matching + (`` `--path ` ``) flag — otherwise the command a reader copies out of the + README installs the wrong subtree. + +Missing recommended fields (`name`, `description`, `author`) and unrecognised +fields warn rather than fail. + --- ## Listed packs diff --git a/scripts/tests/test_validate_skill_packs.sh b/scripts/tests/test_validate_skill_packs.sh new file mode 100755 index 0000000000..ff057afd6b --- /dev/null +++ b/scripts/tests/test_validate_skill_packs.sh @@ -0,0 +1,289 @@ +#!/usr/bin/env bash +# Unit test for scripts/validate-skill-packs.mjs — registry shape, README parity, +# and the trust/capability checks. No network, no GitHub auth required. Every +# case runs against throwaway fixtures under /tmp; the last case runs the real +# committed registry so a drifted main is caught here too. +set -uo pipefail +cd "$(dirname "$0")/../.." || exit 1 + +V="scripts/validate-skill-packs.mjs" +fail=0 +pass(){ echo "ok - $1"; } +bad(){ echo "FAIL - $1"; fail=1; } + +TMP="$(mktemp -d "${TMPDIR:-/tmp}/validate-skill-packs.XXXXXX")" +trap 'rm -rf "$TMP"' EXIT + +# ── Fixture builders ──────────────────────────────────────────────────────── +# A fixture dir holds the four inputs the validator reads. Callers overwrite +# whichever one the case is about. + +# write_registry +write_registry() { + cat > "$1/skill-packs.json" < +write_readme() { + cat > "$1/README.md" < "$1/install-skill-pack" <<'EOF' +#!/usr/bin/env bash +ALLOWED_CAPABILITIES=( + read_only + external_api + writes_external_host + onchain_writes + agent_messaging + sends_notifications +) +EOF +} + +write_trusted() { + cat > "$1/trusted-sources.txt" <<'EOF' +# comment line +goodowner/good-pack +EOF +} + +ONE_PACK='[{"repo":"goodowner/good-pack","name":"Good Pack","description":"A pack.","author":"goodowner","license":"MIT","homepage":"https://example.com","category":"dev","trust_level":"community","skills":["alpha","beta"]}]' +ONE_ROW='| [Good Pack](https://github.com/goodowner/good-pack) | 2 | A pack. |' + +# new_fixture → prints the dir; baseline is valid and passes. +new_fixture() { + local d="$TMP/$1" + mkdir -p "$d" + write_registry "$d" "$ONE_PACK" + write_readme "$d" 1 "$ONE_ROW" + write_installer "$d" + write_trusted "$d" + echo "$d" +} + +run() { + node "$V" --registry "$1/skill-packs.json" --readme "$1/README.md" \ + --installer "$1/install-skill-pack" --trusted "$1/trusted-sources.txt" 2>&1 +} + +# expect_ok