diff --git a/.mokogit/actions/resolve-source-dir/action.yml b/.mokogit/actions/resolve-source-dir/action.yml new file mode 100644 index 0000000..444e829 --- /dev/null +++ b/.mokogit/actions/resolve-source-dir/action.yml @@ -0,0 +1,108 @@ +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later + +name: "Resolve source dir" +description: >- + Resolve the repository source directory from the MokoGIT metadata API + (entry_point) and cross-check the Makefile SRC_DIR, erroring on conflict. + Falls back to source/ when neither is available. + +inputs: + mokogit_token: + description: "MokoGIT API token used for the metadata lookup (secrets are not accessible inside composite actions, so pass it in)." + required: false + default: "" + strict: + description: >- + When 'true', an unreachable/invalid metadata API is a hard error + (refuse to guess). When 'false', warn and treat entry_point as absent. + A genuine metadata-vs-Makefile conflict always errors regardless. + required: false + default: "true" + +outputs: + source_dir: + description: "Resolved source directory with trailing slash (e.g. source/)" + value: ${{ steps.resolve.outputs.source_dir }} + source_glob: + description: "Recursive glob for the source directory (e.g. source/**)" + value: ${{ steps.resolve.outputs.source_glob }} + +runs: + using: "composite" + steps: + - id: resolve + shell: bash + run: | + set -euo pipefail + + STRICT='${{ inputs.strict }}' + + normalize() { + # Trim whitespace, strip surrounding quotes, ensure single trailing slash. + # A resolved value of "." / "./" (repo root) is treated as invalid → empty. + local v="$1" + v="$(printf '%s' "$v" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + v="${v%\"}"; v="${v#\"}" + v="${v%\'}"; v="${v#\'}" + v="$(printf '%s' "$v" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + [ -z "$v" ] && { printf ''; return; } + v="${v%/}/" + if [ "$v" = "./" ] || [ "$v" = "." ]; then + printf '' + return + fi + printf '%s' "$v" + } + + # --- Metadata entry_point -------------------------------------------- + ENTRY_POINT="" + META_DIR="" + if [ -n "${MOKOGIT_URL:-}" ] && [ -n "${GIT_ORG:-}" ] && [ -n "${GIT_REPO:-}" ]; then + URL="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}/metadata" + CURL_RC=0 + META=$(curl -sf -H "Authorization: token ${{ inputs.mokogit_token }}" "$URL") || CURL_RC=$? + if [ "$CURL_RC" -ne 0 ]; then + if [ "$STRICT" = "true" ]; then + echo "::error::metadata API unreachable (curl $CURL_RC); refusing to guess source dir" + exit 1 + else + echo "::warning::metadata unreachable; treating entry_point as absent" + META_DIR="" + fi + else + ENTRY_POINT=$(printf '%s' "$META" | python3 -c "import json,sys;print(json.load(sys.stdin).get('entry_point','') or '')") || { echo "::error::metadata not valid JSON"; exit 1; } + META_DIR="$(normalize "$ENTRY_POINT")" + fi + fi + + # --- Makefile SRC_DIR ------------------------------------------------- + MAKE_DIR="" + if [ -f Makefile ]; then + RAW=$(awk -F':= *' '/^SRC_DIR[[:space:]]*:=/{print $2}' Makefile | head -n1) + if [ -z "$RAW" ]; then + RAW=$(awk -F'\\?= *' '/^SRC_DIR[[:space:]]*\?=/{print $2}' Makefile | head -n1) + fi + MAKE_DIR="$(normalize "$RAW")" + fi + + # --- Reconcile -------------------------------------------------------- + # A genuine conflict ALWAYS errors, regardless of strict. + SOURCE_DIR="" + if [ -n "$META_DIR" ] && [ -n "$MAKE_DIR" ]; then + if [ "$META_DIR" != "$MAKE_DIR" ]; then + echo "::error::source dir conflict: metadata entry_point='${META_DIR}' vs Makefile SRC_DIR='${MAKE_DIR}'" + exit 1 + fi + SOURCE_DIR="$META_DIR" + elif [ -n "$META_DIR" ]; then + SOURCE_DIR="$META_DIR" + elif [ -n "$MAKE_DIR" ]; then + SOURCE_DIR="$MAKE_DIR" + else + SOURCE_DIR="source/" + fi + + echo "Resolved source_dir: ${SOURCE_DIR}" + echo "source_dir=${SOURCE_DIR}" >> "$GITHUB_OUTPUT" + echo "source_glob=${SOURCE_DIR}**" >> "$GITHUB_OUTPUT" diff --git a/.mokogit/workflows/npm-build-test.yml b/.mokogit/workflows/npm-build-test.yml index e2cebd2..8932ee3 100644 --- a/.mokogit/workflows/npm-build-test.yml +++ b/.mokogit/workflows/npm-build-test.yml @@ -10,15 +10,20 @@ name: "MCP: Build & Validate" on: push: branches: [main, dev/**] - paths: ['src/**', 'package.json', 'tsconfig.json'] + paths: ['source/**', 'src/**', 'package.json', 'tsconfig.json'] pull_request: branches: [main] - paths: ['src/**', 'package.json', 'tsconfig.json'] + paths: ['source/**', 'src/**', 'package.json', 'tsconfig.json'] permissions: contents: read +env: + MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }} + GIT_ORG: ${{ vars.MOKOGIT_ORG || github.repository_owner }} + GIT_REPO: ${{ vars.MOKOGIT_REPO || github.event.repository.name }} + jobs: build: runs-on: ubuntu-latest @@ -30,6 +35,13 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Resolve source dir + id: srcdir + uses: ./.mokogit/actions/resolve-source-dir + with: + mokogit_token: ${{ secrets.MOKOGIT_TOKEN }} + strict: 'false' + - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: @@ -58,9 +70,14 @@ jobs: - name: Count registered tools run: | - TOOL_COUNT=$(grep -c "server\.tool(" src/index.ts || true) - echo "Registered tools: ${TOOL_COUNT}" + INDEX_FILE="${{ steps.srcdir.outputs.source_dir }}index.ts" + if [ ! -f "${INDEX_FILE}" ]; then + echo "::warning::resolved entrypoint ${INDEX_FILE} missing; falling back to src/index.ts" + INDEX_FILE="src/index.ts" + fi + TOOL_COUNT=$(grep -c "server\.tool(" "${INDEX_FILE}" || true) + echo "Registered tools in ${INDEX_FILE}: ${TOOL_COUNT}" if [ "${TOOL_COUNT}" -eq 0 ]; then - echo "ERROR: No tools registered in src/index.ts" + echo "ERROR: No tools registered in ${INDEX_FILE}" exit 1 fi diff --git a/.mokogit/workflows/npm-publish.yml b/.mokogit/workflows/npm-publish.yml index 79ad04a..7c415f1 100644 --- a/.mokogit/workflows/npm-publish.yml +++ b/.mokogit/workflows/npm-publish.yml @@ -24,20 +24,70 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Resolve source dir + id: srcdir + uses: ./.mokogit/actions/resolve-source-dir + with: + mokogit_token: ${{ secrets.MOKOGIT_TOKEN }} + strict: 'true' + + - name: Detect publishable changes + id: detect + run: | + set -euo pipefail + + # workflow_dispatch has no diff base — always treat as changed. + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "::notice::workflow_dispatch — forcing changed=true" + echo "changed=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Build the source-dir alternative from the resolved dir (escape ERE metachars). + # Class lists ] first and [ last to avoid a [. collating-symbol parse; the + # \\\\& replacement reaches sed as \\& = literal backslash + matched char. + SRC_DIR='${{ steps.srcdir.outputs.source_dir }}' + SRC_RE=$(printf '%s' "$SRC_DIR" | sed 's/[].^$*+?(){}|[]/\\\\&/g') + PATTERN="^(${SRC_RE}|package\.json$|package-lock\.json$|tsconfig\.json$|README\.md$)" + echo "Detect pattern: ${PATTERN}" + + BEFORE="${{ github.event.before }}" + if [ -z "${BEFORE}" ] || ! git cat-file -e "${BEFORE}^{commit}" 2>/dev/null; then + BEFORE=$(git rev-parse HEAD^ 2>/dev/null || echo "") + fi + if [ -n "${BEFORE}" ]; then + FILES=$(git diff --name-only "${BEFORE}" HEAD) + else + FILES=$(git show --name-only --pretty=format: HEAD) + fi + + if printf '%s\n' "${FILES}" | grep -Eq "${PATTERN}"; then + echo "changed=true" >> "$GITHUB_OUTPUT" + else + echo "::notice::No publishable changes under '${SRC_DIR}'. Files: ${FILES}" + echo "changed=false" >> "$GITHUB_OUTPUT" + fi - name: Setup Node.js + if: steps.detect.outputs.changed == 'true' uses: actions/setup-node@v4 with: node-version: '20' registry-url: 'https://registry.npmjs.org' - name: Install dependencies + if: steps.detect.outputs.changed == 'true' run: npm install - name: Build + if: steps.detect.outputs.changed == 'true' run: npm run build - name: Auto-bump patch version + if: steps.detect.outputs.changed == 'true' run: | API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" TOKEN="${{ secrets.MOKOGIT_TOKEN }}" @@ -109,6 +159,7 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Publish + if: steps.detect.outputs.changed == 'true' run: npm publish --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.mokogit/workflows/npm-tool-inventory.yml b/.mokogit/workflows/npm-tool-inventory.yml index c46a5f7..8bd4313 100644 --- a/.mokogit/workflows/npm-tool-inventory.yml +++ b/.mokogit/workflows/npm-tool-inventory.yml @@ -10,13 +10,18 @@ name: "MCP: Tool Inventory" on: push: branches: [main] - paths: ['src/index.ts'] + paths: ['source/index.ts', 'src/index.ts'] workflow_dispatch: permissions: contents: read +env: + MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }} + GIT_ORG: ${{ vars.MOKOGIT_ORG || github.repository_owner }} + GIT_REPO: ${{ vars.MOKOGIT_REPO || github.event.repository.name }} + jobs: inventory: runs-on: ubuntu-latest @@ -24,15 +29,29 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Resolve source dir + id: srcdir + uses: ./.mokogit/actions/resolve-source-dir + with: + mokogit_token: ${{ secrets.MOKOGIT_TOKEN }} + strict: 'false' + - name: Generate tool inventory run: | + INDEX_FILE="${{ steps.srcdir.outputs.source_dir }}index.ts" + if [ ! -f "${INDEX_FILE}" ]; then + echo "::warning::resolved entrypoint ${INDEX_FILE} missing; falling back to src/index.ts" + INDEX_FILE="src/index.ts" + fi + [ -r "${INDEX_FILE}" ] || { echo "::error::entry file ${INDEX_FILE} unreadable"; exit 1; } + echo "# MCP Tool Inventory" > TOOLS.md echo "" >> TOOLS.md - echo "Auto-generated from \`src/index.ts\` on $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> TOOLS.md + echo "Auto-generated from \`${INDEX_FILE}\` on $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> TOOLS.md echo "" >> TOOLS.md # Count tools - TOOL_COUNT=$(grep -c "server\.tool(" src/index.ts || true) + TOOL_COUNT=$(grep -c "server\.tool(" "${INDEX_FILE}" || true) echo "**Total tools: ${TOOL_COUNT}**" >> TOOLS.md echo "" >> TOOLS.md @@ -40,10 +59,10 @@ jobs: echo "| Tool | Description |" >> TOOLS.md echo "|------|-------------|" >> TOOLS.md - grep -A1 "server\.tool(" src/index.ts | grep -E "^\s*'" | while read -r line; do + grep -A1 "server\.tool(" "${INDEX_FILE}" | grep -E "^\s*'" | while read -r line; do TOOL_NAME=$(echo "$line" | sed "s/.*'\([^']*\)'.*/\1/") # Get next line for description - DESC=$(grep -A2 "'${TOOL_NAME}'" src/index.ts | grep -E "^\s*'" | tail -1 | sed "s/.*'\([^']*\)'.*/\1/" || echo "") + DESC=$(grep -A2 "'${TOOL_NAME}'" "${INDEX_FILE}" | grep -E "^\s*'" | tail -1 | sed "s/.*'\([^']*\)'.*/\1/" || echo "") echo "| \`${TOOL_NAME}\` | ${DESC} |" >> TOOLS.md done