Template
ci: source-dir resolver from metadata entry_point (Refs #33) #36
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user