diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index f0d016d..4fa42bb 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -18,6 +18,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + with: + persist-credentials: false - name: Setup Node uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 @@ -42,6 +44,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + with: + persist-credentials: false - name: Setup Node uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 @@ -65,5 +69,5 @@ jobs: - name: Generate data from the public sources run: npm run build:data - - name: Package contents - run: npm pack --dry-run + - name: Verify package contents + run: npm run verify:package diff --git a/.github/workflows/publish-docs-mcp.yml b/.github/workflows/publish-docs-mcp.yml new file mode 100644 index 0000000..b14a9ce --- /dev/null +++ b/.github/workflows/publish-docs-mcp.yml @@ -0,0 +1,97 @@ +name: Publish docs MCP + +on: + push: + branches: [main] + paths: + - 'docs-mcp/**' + - '.github/workflows/publish-docs-mcp.yml' + workflow_dispatch: + +permissions: + contents: read + id-token: write + +concurrency: + group: publish-docs-mcp + cancel-in-progress: false + +jobs: + publish: + name: Publish @fiscalapi/docs-mcp to npm + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + defaults: + run: + working-directory: docs-mcp + steps: + - name: Checkout + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + with: + persist-credentials: false + + - name: Setup Node + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 24 + registry-url: 'https://registry.npmjs.org/' + cache: npm + cache-dependency-path: docs-mcp/package-lock.json + + - name: Check whether the version is already published + id: version + run: | + name=$(node -p "require('./package.json').name") + version=$(node -p "require('./package.json').version") + echo "package=$name@$version" >> "$GITHUB_OUTPUT" + if npm view "$name@$version" version > /dev/null 2>&1; then + echo "published=true" >> "$GITHUB_OUTPUT" + echo "::notice::$name@$version is already on npm. Bump the version in docs-mcp/package.json to publish." + else + echo "published=false" >> "$GITHUB_OUTPUT" + fi + + - name: Verify npm credentials + if: steps.version.outputs.published == 'false' + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + if [ -z "$NODE_AUTH_TOKEN" ]; then + echo "::error::The NPM_TOKEN secret is missing or empty." + exit 1 + fi + if ! npm whoami; then + echo "::error::NPM_TOKEN is invalid or expired. Create an npm granular access token with read and write access to @fiscalapi/docs-mcp and update the NPM_TOKEN secret." + exit 1 + fi + npm access list packages || echo "The token packages could not be listed." + + - name: Install + if: steps.version.outputs.published == 'false' + run: npm ci --ignore-scripts + + - name: Typecheck and test + if: steps.version.outputs.published == 'false' + run: | + npm run typecheck + npm test + + - name: Build + if: steps.version.outputs.published == 'false' + run: | + npm run build + npm run build:data + + - name: Verify package contents + if: steps.version.outputs.published == 'false' + run: npm run verify:package + + - name: Publish + if: steps.version.outputs.published == 'false' + run: npm publish --access public --provenance --ignore-scripts + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Summary + if: steps.version.outputs.published == 'false' + run: echo "Published ${{ steps.version.outputs.package }}." >> "$GITHUB_STEP_SUMMARY" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 419d48a..e816e7b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,6 +52,10 @@ Convenciones del código: - El código y los mensajes internos van en inglés. Los títulos, las descripciones y los resultados de las herramientas van en español, porque los lee el modelo. - Cada herramienta es una clase que implementa `McpTool` en `src/tools/` y recibe sus dependencias por interfaces. +### Publicación + +El paquete usa la misma versión que la API de FiscalAPI que documenta, fijada en `docs-mcp/package.json`. Al fusionar en `main` un cambio de `docs-mcp/`, el workflow `Publish docs MCP` compila, regenera los datos, verifica el contenido del paquete y lo publica en npm. Si esa versión ya está publicada, no publica: para publicar, sube la versión en `package.json`. + ## Pull requests El workflow `Check` debe pasar: valida las skills, los manifiestos y el servidor MCP (typecheck, build, pruebas y contenido del paquete). diff --git a/docs-mcp/package-lock.json b/docs-mcp/package-lock.json index 1c3a7f9..70b36f1 100644 --- a/docs-mcp/package-lock.json +++ b/docs-mcp/package-lock.json @@ -1,12 +1,12 @@ { "name": "@fiscalapi/docs-mcp", - "version": "0.1.0", + "version": "4.0.461", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@fiscalapi/docs-mcp", - "version": "0.1.0", + "version": "4.0.461", "license": "MPL-2.0", "dependencies": { "@modelcontextprotocol/server": "2.3.1", diff --git a/docs-mcp/package.json b/docs-mcp/package.json index 5ccd16d..769da85 100644 --- a/docs-mcp/package.json +++ b/docs-mcp/package.json @@ -1,6 +1,6 @@ { "name": "@fiscalapi/docs-mcp", - "version": "0.1.0", + "version": "4.0.461", "description": "Servidor MCP de solo lectura con la documentación y la referencia de la API de FiscalAPI (CFDI 4.0) para agentes de código.", "type": "module", "bin": { @@ -17,7 +17,8 @@ "build": "npm run clean && tsc -p tsconfig.build.json", "typecheck": "tsc -p tsconfig.json", "test": "vitest run", - "prepack": "npm run build && npm run build:data" + "prepack": "npm run build && npm run build:data", + "verify:package": "node scripts/verify-package.ts" }, "keywords": [ "fiscalapi", diff --git a/docs-mcp/scripts/verify-package.ts b/docs-mcp/scripts/verify-package.ts new file mode 100644 index 0000000..6766036 --- /dev/null +++ b/docs-mcp/scripts/verify-package.ts @@ -0,0 +1,46 @@ +import { spawnSync } from 'node:child_process'; + +import * as z from 'zod/v4'; + +import { DATA_FILES } from '../src/data-files.ts'; +import { createLogger, runScript, ScriptError } from './script-runner.ts'; + +const SCRIPT_NAME = 'verify-package'; +const PACK_COMMAND = 'npm pack --dry-run --json --ignore-scripts'; +const REQUIRED_FILES = [ + 'package.json', + 'README.md', + 'LICENSE', + 'dist/index.js', + 'dist/server.js', + `data/${DATA_FILES.operations}`, + `data/${DATA_FILES.docsIndex}`, + `data/${DATA_FILES.source}`, +] as const; + +const packResultSchema = z.array( + z.object({ + name: z.string(), + version: z.string(), + files: z.array(z.object({ path: z.string() })), + }), +); + +function packedFiles(): { readonly id: string; readonly paths: ReadonlySet } { + const pack = spawnSync(PACK_COMMAND, { encoding: 'utf8', shell: true }); + if (pack.status !== 0) throw new ScriptError(`npm pack failed: ${pack.stderr}`); + const [result] = packResultSchema.parse(JSON.parse(pack.stdout)); + if (result === undefined) throw new ScriptError('npm pack returned no package.'); + return { id: `${result.name}@${result.version}`, paths: new Set(result.files.map((file) => file.path)) }; +} + +async function verifyPackage(): Promise { + const { id, paths } = packedFiles(); + const missing = REQUIRED_FILES.filter((file) => !paths.has(file)); + if (missing.length > 0) throw new ScriptError(`${id} is missing ${missing.join(', ')}. Run npm run build and npm run build:data.`); + const contracts = [...paths].filter((file) => file.startsWith(`data/${DATA_FILES.operationContracts}`)).length; + if (contracts === 0) throw new ScriptError(`${id} has no operation contracts.`); + createLogger(SCRIPT_NAME)(`${id} contains ${paths.size} files, ${contracts} operation contracts included.`); +} + +await runScript(SCRIPT_NAME, verifyPackage);