diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index dc53687..0db5dfa 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -19,6 +19,8 @@ jobs: run: node --test scripts/validate.test.mjs - name: Validate manifests + skills run: node scripts/validate.mjs + - name: Validate install/recovery docs + run: node scripts/validate-install-docs.mjs - name: Version consistency (plugin.json == package.json) run: | PJ=$(jq -r .version .codex-plugin/plugin.json) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 55111fb..8e44249 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,8 +13,9 @@ All contributors must sign the [JFrog CLA](https://jfrog.com/cla/) before contri 3. **Validate** locally: ```bash -npm test # unit tests for the manifest/skill validator -npm run validate # lint the plugin manifests + skill frontmatter +npm test # unit tests for the manifest/skill validator +npm run validate # lint the plugin manifests + skill frontmatter +node scripts/validate-install-docs.mjs # check install/verify docs stay aligned ``` 1. **Test** by loading your clone as the plugin. The repo root is the marketplace root (`.agents/plugins/marketplace.json` registers the `jfrog` plugin): @@ -41,7 +42,8 @@ This downloads the pinned upstream tarball and replaces the contents of `skills/ ## Pre-release checklist -- [ ] `npm test` and `npm run validate` pass. +- [ ] `npm test`, `npm run validate`, and `node scripts/validate-install-docs.mjs` pass. +- [ ] README includes **Verify** and **Recovery** and does not send readers to another plugin repository for those steps. - [ ] `version` bumped and **identical** in both [`.codex-plugin/plugin.json`](.codex-plugin/plugin.json) and [`package.json`](package.json) (CI enforces the match). - [ ] No secrets, credentials, or API keys committed. - [ ] If the skill tree changed: `pin` in [`scripts/sync-skills-vendor.json`](scripts/sync-skills-vendor.json) matches the upstream tag the new tree was generated from, and the README Prerequisites link points at that tag. diff --git a/README.md b/README.md index 5bf79a9..c2531f1 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,32 @@ Restart Codex; the `jfrog` MCP server and its tools are now available (verify wi --- +## Verify + +Verification is a required install step, not a troubleshooting fallback. After +restarting Codex, confirm all four: + +1. `codex plugin list` — `jfrog@codex-plugin` is installed and enabled. +2. `/plugins` in the Codex TUI — the JFrog plugin lists its skills (`jfrog` and others). +3. `codex mcp list` — `jfrog` is connected (after `codex mcp login jfrog`). +4. `jf rt ping` — succeeds against your configured JFrog server. + +If any check fails, see [Recovery](#recovery). Setting MCP environment variables +by hand does not repair a failed initialization — re-run `jfrog-init` instead. + +--- + +## Recovery + +| Symptom | Do this | Do **not** do this | +| --- | --- | --- | +| MCP missing after install | Run `jfrog-init`, edit the plugin `.mcp.json` host if needed, run `codex mcp login jfrog`, **restart Codex**, then `codex mcp list`. | Assume exporting `JFROG_PLATFORM_URL` will register MCP. Codex reads the host from `.mcp.json`. | +| `jfrog-init` stopped at CLI/auth | Follow the skill prompt (`jf config add`, web login, or token path), then **re-run `jfrog-init`**. | Skip init and only export env vars. | +| Placeholder still in `.mcp.json` | Set the host in `/.mcp.json`, run `codex mcp login jfrog`, restart Codex. | Reinstall the plugin when only the host placeholder is wrong. | +| Plugin not listed | Re-run `codex plugin add jfrog@codex-plugin` from a regular terminal, then restart Codex. | Run install commands from inside the Codex TUI. | + +--- + ## Usage Once configured, interact with the JFrog plugin through natural language. diff --git a/package.json b/package.json index 32d168e..aa35673 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,6 @@ "scripts": { "sync-skills": "node scripts/sync-skills.mjs", "validate": "node scripts/validate.mjs", - "test": "node --test scripts/validate.test.mjs" + "test": "node --test scripts/validate.test.mjs scripts/validate-install-docs.test.mjs" } } diff --git a/scripts/validate-install-docs.mjs b/scripts/validate-install-docs.mjs new file mode 100644 index 0000000..c6c74b8 --- /dev/null +++ b/scripts/validate-install-docs.mjs @@ -0,0 +1,97 @@ +#!/usr/bin/env node +// Copyright (c) JFrog Ltd. 2026 +// Licensed under the Apache License, Version 2.0 +// Validates install/recovery documentation invariants. + +import { readFileSync, existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = process.cwd(); + +const HARNESS_OWN_REPO = { + claude: 'claude-plugin', + codex: 'codex-plugin', + cursor: 'cursor-plugin', + devin: 'devin-plugin', + opencode: 'opencode-jfrog-plugin', + vscode: 'vscode-plugin', +}; + +const ALL_PLUGIN_REPOS = Object.values(HARNESS_OWN_REPO); + +const REQUIRED_README_MARKERS = ['## Verify']; + +const FORBIDDEN_PATTERNS = [ + { + re: /setting\s+(?:the\s+)?environment\s+variables?\s+after\s+a\s+failed\s+init\s+may\s+repair/i, + message: 'must not claim env vars repair failed init', + }, + { + re: /JFROG_URL/, + message: 'must not document the legacy JFROG_URL env var; use JFROG_PLATFORM_URL', + }, + { + re: /atlassian\.net/i, + message: 'must not reference JFrog Jira (atlassian.net) in repo files', + }, + { + re: /\b(?:AX|MLD)-\d+\b/, + message: 'must not include Jira ticket keys in repo files', + }, +]; + +export function validateInstallDocs({ repoRoot: root, harness }) { + const errors = []; + const readmePath = join(root, 'README.md'); + if (!existsSync(readmePath)) { + return [`${harness}: missing README.md`]; + } + const files = [{ label: 'README.md', text: readFileSync(readmePath, 'utf8') }]; + + const readme = files[0].text; + for (const marker of REQUIRED_README_MARKERS) { + if (!readme.includes(marker)) { + errors.push(`${harness}: README.md missing required marker: ${marker}`); + } + } + + const ownRepo = HARNESS_OWN_REPO[harness]; + const otherRepos = ALL_PLUGIN_REPOS.filter((name) => name !== ownRepo); + + for (const { label, text } of files) { + for (const { re, message } of FORBIDDEN_PATTERNS) { + if (re.test(text)) errors.push(`${harness}: ${label} ${message}`); + } + for (const other of otherRepos) { + if (text.includes(`github.com/jfrog/${other}`)) { + errors.push(`${harness}: ${label} must not link to github.com/jfrog/${other}`); + } + } + } + + return errors; +} + +function main() { + const harness = process.env.JFROG_PLUGIN_HARNESS ?? inferHarness(repoRoot); + const errors = validateInstallDocs({ repoRoot, harness }); + if (errors.length) { + console.error('install-docs validation failed:'); + for (const e of errors) console.error(` - ${e}`); + process.exit(1); + } + console.log('install-docs validation passed'); +} + +function inferHarness(root) { + if (existsSync(join(root, '.codex-plugin'))) return 'codex'; + if (existsSync(join(root, '.devin-plugin'))) return 'devin'; + if (existsSync(join(root, '.claude-plugin'))) return 'claude'; + if (existsSync(join(root, 'plugins', 'jfrog', '.cursor-plugin'))) return 'cursor'; + if (existsSync(join(root, 'plugin', '.claude-plugin'))) return 'vscode'; + if (existsSync(join(root, 'package.json')) && root.endsWith('opencode-jfrog-plugin')) return 'opencode'; + return 'unknown'; +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) main(); diff --git a/scripts/validate-install-docs.test.mjs b/scripts/validate-install-docs.test.mjs new file mode 100644 index 0000000..5a2b2b4 --- /dev/null +++ b/scripts/validate-install-docs.test.mjs @@ -0,0 +1,61 @@ +// Copyright (c) JFrog Ltd. 2026 +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { validateInstallDocs } from './validate-install-docs.mjs'; + +function writeReadme(root, body) { + writeFileSync(join(root, 'README.md'), body); +} + +test('validateInstallDocs passes when README has Verify and no other-plugin links', () => { + const root = mkdtempSync(join(tmpdir(), 'codex-docs-')); + writeReadme(root, '# Codex\n\n## Verify\n\n1. list plugins\n'); + assert.deepEqual(validateInstallDocs({ repoRoot: root, harness: 'codex' }), []); +}); + +test('validateInstallDocs flags missing Verify section', () => { + const root = mkdtempSync(join(tmpdir(), 'codex-docs-')); + writeReadme(root, '# Codex\n\nInstall the plugin.\n'); + const errors = validateInstallDocs({ repoRoot: root, harness: 'codex' }); + assert.ok(errors.some((e) => e.includes('## Verify'))); +}); + +test('validateInstallDocs rejects contradictory failed-init env-var recovery claims', () => { + const root = mkdtempSync(join(tmpdir(), 'codex-docs-')); + writeReadme( + root, + '# x\n## Verify\nSetting environment variables after a failed init may repair MCP registration.' + ); + const errors = validateInstallDocs({ repoRoot: root, harness: 'codex' }); + assert.ok(errors.some((e) => e.includes('env vars repair failed init'))); +}); + +test('validateInstallDocs rejects the legacy JFROG_URL env var', () => { + const root = mkdtempSync(join(tmpdir(), 'codex-docs-')); + writeReadme(root, '# Codex\n## Verify\nSet `JFROG_URL` to your platform.\n'); + const errors = validateInstallDocs({ repoRoot: root, harness: 'codex' }); + assert.ok(errors.some((e) => e.includes('JFROG_URL'))); +}); + +test('validateInstallDocs rejects links to other plugin GitHub repos', () => { + const root = mkdtempSync(join(tmpdir(), 'codex-docs-')); + writeReadme( + root, + '# Codex\n## Verify\nSee https://github.com/jfrog/claude-plugin/blob/main/README.md\n' + ); + const errors = validateInstallDocs({ repoRoot: root, harness: 'codex' }); + assert.ok(errors.some((e) => e.includes('claude-plugin'))); +}); + +test('validateInstallDocs rejects Jira URLs and ticket keys', () => { + const root = mkdtempSync(join(tmpdir(), 'codex-docs-')); + const host = ['jfrog-int', 'atlassian', 'net'].join('.'); + const key = ['AX', '1780'].join('-'); + writeReadme(root, `# Codex\n## Verify\nSee [${key}](https://${host}/browse/${key}).\n`); + const errors = validateInstallDocs({ repoRoot: root, harness: 'codex' }); + assert.ok(errors.some((e) => e.includes('atlassian.net'))); + assert.ok(errors.some((e) => e.includes('Jira ticket keys'))); +});