ci: source-dir resolver from metadata entry_point (Refs #33) #36

Merged
jmiller merged 3 commits from fix/ci-resolve-source-dir into dev 2026-07-18 18:22:02 +00:00
4 changed files with 205 additions and 10 deletions
@@ -0,0 +1,108 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# 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"
+22 -5
View File
@@ -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
+51
View File
@@ -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 }}
+24 -5
View File
@@ -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