diff --git a/.gitea/workflows/mcp-auto-release.yml b/.gitea/workflows/mcp-auto-release.yml new file mode 100644 index 0000000..b2b0b6e --- /dev/null +++ b/.gitea/workflows/mcp-auto-release.yml @@ -0,0 +1,278 @@ +# MCP Server Auto-Release +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# +# MCP-specific release pipeline that builds TypeScript, runs validation, +# attaches the compiled dist/ as a release artifact, and creates a GitHub +# Release with tool inventory in the release notes. +# +# This replaces the generic auto-release.yml for MCP server repos. + +name: "MCP: Build & Release" + +on: + push: + branches: + - main + paths: + - 'src/**' + - 'package.json' + - 'tsconfig.json' + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +permissions: + contents: write + issues: write + +jobs: + build-and-release: + name: Build, Validate & Release + runs-on: ubuntu-latest + if: >- + !contains(github.event.head_commit.message, '[skip ci]') && + github.actor != 'github-actions[bot]' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GH_TOKEN || github.token }} + fetch-depth: 0 + + # ── Build ──────────────────────────────────────────────────────── + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm ci + + - name: TypeScript compile check + run: npx tsc --noEmit + + - name: Build + run: npm run build + + - name: Verify dist output + run: | + for f in index.js client.js config.js types.js; do + test -f "dist/${f}" || (echo "ERROR: dist/${f} not found" && exit 1) + done + echo "✓ All dist files present" + + # ── Tool Inventory ─────────────────────────────────────────────── + - name: Generate tool inventory + id: tools + run: | + TOOL_COUNT=$(grep -c "server\.tool(" src/index.ts || echo "0") + echo "count=${TOOL_COUNT}" >> "$GITHUB_OUTPUT" + + # Extract tool names + TOOL_LIST=$(grep -oE "'[a-z_]+'" src/index.ts | head -100 | tr -d "'" | sort -u) + echo "Tools registered: ${TOOL_COUNT}" + + # Generate inventory for release notes + echo "## Tool Inventory (${TOOL_COUNT} tools)" > /tmp/tool-inventory.md + echo "" >> /tmp/tool-inventory.md + grep -B0 -A1 "server\.tool(" src/index.ts | grep -oE "'[^']+'" | while IFS= read -r name; do + read -r desc 2>/dev/null || true + CLEAN_NAME=$(echo "$name" | tr -d "'") + CLEAN_DESC=$(echo "$desc" | tr -d "'" | sed 's/,$//') + if [ -n "$CLEAN_NAME" ] && [ -n "$CLEAN_DESC" ]; then + echo "- \`${CLEAN_NAME}\` — ${CLEAN_DESC}" >> /tmp/tool-inventory.md + fi + done + + # ── Version ────────────────────────────────────────────────────── + - name: Setup MokoStandards tools + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards + cd /tmp/mokostandards + composer install --no-dev --no-interaction --quiet + + - name: Read version from README.md + id: version + run: | + VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null) + if [ -z "$VERSION" ]; then + echo "No VERSION in README.md — skipping release" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') + MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}') + PATCH=$(echo "$VERSION" | awk -F. '{print $3}') + + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT" + echo "major=$MAJOR" >> "$GITHUB_OUTPUT" + echo "minor=$MINOR" >> "$GITHUB_OUTPUT" + echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT" + + if [ "$PATCH" = "00" ]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + if [ "$PATCH" = "01" ]; then + echo "is_first=true" >> "$GITHUB_OUTPUT" + else + echo "is_first=false" >> "$GITHUB_OUTPUT" + fi + fi + + - name: Check if already released + if: steps.version.outputs.skip != 'true' + id: check + run: | + TAG="${{ steps.version.outputs.release_tag }}" + TAG_EXISTS=false + git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true + echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" + + # ── Release Artifact ───────────────────────────────────────────── + - name: Package dist + if: steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.version.outputs.version }}" + REPO_NAME="${{ github.event.repository.name }}" + tar -czf "/tmp/${REPO_NAME}-${VERSION}.tar.gz" -C dist . + echo "artifact=/tmp/${REPO_NAME}-${VERSION}.tar.gz" >> "$GITHUB_OUTPUT" + + # ── Version Updates ────────────────────────────────────────────── + - name: Set platform version + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' + run: | + VERSION="${{ steps.version.outputs.version }}" + php /tmp/mokostandards/api/cli/version_set_platform.php \ + --path . --version "$VERSION" --branch main + + - name: Update version badges + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' + run: | + VERSION="${{ steps.version.outputs.version }}" + find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" | while read -r f; do + if grep -q '\[VERSION:' "$f" 2>/dev/null; then + sed -i "s/\[VERSION:[[:space:]]*[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\]/[VERSION: ${VERSION}]/" "$f" + fi + done + + - name: Commit release changes + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' + run: | + if git diff --quiet && git diff --cached --quiet; then + echo "No changes to commit" + exit 0 + fi + VERSION="${{ steps.version.outputs.version }}" + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add -A + git commit -m "chore(release): build ${VERSION} [skip ci]" \ + --author="github-actions[bot] " + git push + + # ── Version Branch ─────────────────────────────────────────────── + - name: Archive version branch + if: steps.check.outputs.tag_exists != 'true' + run: | + BRANCH="${{ steps.version.outputs.branch }}" + git push origin HEAD:"$BRANCH" --force + echo "Updated archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY + + # ── Tag & Release ──────────────────────────────────────────────── + - name: Create git tag + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' && + steps.version.outputs.is_first == 'true' + run: | + TAG="${{ steps.version.outputs.release_tag }}" + if ! git rev-parse "$TAG" >/dev/null 2>&1; then + git tag "$TAG" + git push origin "$TAG" + fi + + - name: GitHub Release + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + VERSION="${{ steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + MAJOR="${{ steps.version.outputs.major }}" + BRANCH="${{ steps.version.outputs.branch }}" + TOOL_COUNT="${{ steps.tools.outputs.count }}" + REPO_NAME="${{ github.event.repository.name }}" + + # Build release notes + NOTES=$(php /tmp/mokostandards/api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null) + [ -z "$NOTES" ] && NOTES="Release ${VERSION}" + + { + echo "$NOTES" + echo "" + echo "---" + echo "" + echo "### MCP Server Info" + echo "- **Tools registered**: ${TOOL_COUNT}" + echo "- **Node.js**: 20+" + echo "- **MCP SDK**: $(node -p \"require('./package.json').dependencies['@modelcontextprotocol/sdk']\" 2>/dev/null || echo 'unknown')" + echo "" + cat /tmp/tool-inventory.md 2>/dev/null || true + } > /tmp/release_notes.md + + EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true) + + ARTIFACT="/tmp/${REPO_NAME}-${VERSION}.tar.gz" + + if [ -z "$EXISTING" ]; then + gh release create "$RELEASE_TAG" \ + --title "v${MAJOR} (latest: ${VERSION})" \ + --notes-file /tmp/release_notes.md \ + --target "$BRANCH" \ + "$ARTIFACT" + echo "Release created: ${RELEASE_TAG} (${VERSION})" >> $GITHUB_STEP_SUMMARY + else + gh release edit "$RELEASE_TAG" \ + --title "v${MAJOR} (latest: ${VERSION})" \ + --notes-file /tmp/release_notes.md + gh release upload "$RELEASE_TAG" "$ARTIFACT" --clobber 2>/dev/null || true + echo "Release updated: ${RELEASE_TAG} -> ${VERSION}" >> $GITHUB_STEP_SUMMARY + fi + + # ── Summary ────────────────────────────────────────────────────── + - name: Pipeline Summary + if: always() + run: | + VERSION="${{ steps.version.outputs.version }}" + TOOL_COUNT="${{ steps.tools.outputs.count }}" + if [ "${{ steps.version.outputs.skip }}" = "true" ]; then + echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "## MCP Release Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Detail | Value |" >> $GITHUB_STEP_SUMMARY + echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tools | ${TOOL_COUNT} |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${{ steps.version.outputs.release_tag }}\` |" >> $GITHUB_STEP_SUMMARY + fi