chore(deps): bump graphql from 16.14.2 to 17.0.0 #1786
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # CI/CD Pipeline — Quality Checks & Docker Build | |
| # | |
| # This workflow: | |
| # 1. Runs linting and tests | |
| # 2. Builds the project | |
| # 3. Builds Docker image | |
| # | |
| # Release and publishing are handled by release-please.yml | |
| # | |
| # Triggers on push to main and pull requests | |
| name: CI/CD Pipeline | |
| permissions: | |
| contents: read | |
| on: | |
| push: | |
| branches: | |
| - main | |
| - develop | |
| pull_request: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| env: | |
| GHCR_REGISTRY: ghcr.io | |
| IMAGE_NAME: structured-world/gitlab-mcp | |
| jobs: | |
| # Job 1: Quality checks (lint, test, build) | |
| quality-checks: | |
| name: Quality Checks | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '22' | |
| - name: Enable Corepack | |
| run: corepack enable | |
| - name: Install Yarn | |
| run: corepack prepare yarn@4.12.0 --activate | |
| - name: Install dependencies | |
| run: yarn install --frozen-lockfile | |
| - name: Run linting | |
| run: yarn lint | |
| working-directory: packages/gitlab-mcp | |
| - name: Run tests with coverage | |
| run: yarn test:cov | |
| working-directory: packages/gitlab-mcp | |
| env: | |
| CI: true | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v7 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: ./packages/gitlab-mcp/coverage/lcov.info | |
| fail_ci_if_error: false | |
| - name: Build core project | |
| run: yarn build | |
| working-directory: packages/gitlab-mcp | |
| - name: Build optional db package | |
| run: yarn workspace @structured-world/gitlab-mcp-db build | |
| - name: Run optional db package tests | |
| run: yarn workspace @structured-world/gitlab-mcp-db test | |
| - name: Validate TOOLS.md generation | |
| run: yarn list-tools --export --toc > /dev/null | |
| working-directory: packages/gitlab-mcp | |
| - name: Validate MCPB manifest template | |
| run: | | |
| MANIFEST=$(sed 's/{{VERSION}}/0.0.0/g' mcpb/manifest.json.template) | |
| echo "$MANIFEST" | jq . > /dev/null | |
| # Validate required MCPB fields | |
| echo "$MANIFEST" | jq -e '.manifest_version and .name and .version and .server.entry_point and .server.mcp_config.command' > /dev/null | |
| echo "Manifest template is valid (JSON + required fields)" | |
| working-directory: packages/gitlab-mcp | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: build-artifacts | |
| path: packages/gitlab-mcp/dist/ | |
| retention-days: 1 | |
| # Job: keep generated metadata (README counts, server.json description, | |
| # package.json MCP manifest) in sync on main. README.md is a committed file | |
| # GitHub renders from the repo, so it must physically hold current counts — | |
| # unlike the docs site, which regenerates an ephemeral Pages artifact on every | |
| # deploy. Counts come from the built registry via prepare-release.sh. | |
| # | |
| # Runs ONLY on push to main (never on PRs). Reconciles against the actual | |
| # post-merge state, so it self-heals regardless of merge order. Pushes ONLY | |
| # when there is real drift (git diff guard) — empty/no-op pushes never happen. | |
| # The commit carries [skip ci], so the sync push never re-triggers this | |
| # workflow or release-please: no loop. | |
| sync-metadata: | |
| name: Sync generated metadata | |
| runs-on: ubuntu-latest | |
| needs: [quality-checks] | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| permissions: | |
| contents: write | |
| steps: | |
| # App token (not GITHUB_TOKEN) so the push can bypass branch protection on | |
| # main. Same releaser app used by release-please.yml. | |
| - name: Generate release token | |
| uses: actions/create-github-app-token@v3 | |
| id: app-token | |
| with: | |
| app-id: ${{ secrets.RELEASER_APP_ID }} | |
| private-key: ${{ secrets.RELEASER_APP_PRIVATE_KEY }} | |
| owner: ${{ github.repository_owner }} | |
| repositories: ${{ github.event.repository.name }} | |
| - name: Checkout main | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: main | |
| token: ${{ steps.app-token.outputs.token }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '22' | |
| - name: Enable Corepack | |
| run: corepack enable | |
| - name: Install Yarn | |
| run: corepack prepare yarn@4.12.0 --activate | |
| - name: Install dependencies | |
| run: yarn install --frozen-lockfile | |
| - name: Build core | |
| run: yarn build | |
| working-directory: packages/gitlab-mcp | |
| # Use the currently released version from package.json (release-please bumps | |
| # it only inside the release PR), so server.json version does not spuriously | |
| # drift here — only the tool/action/entity counts can change. | |
| - name: Regenerate metadata | |
| run: ./scripts/prepare-release.sh "$(jq -r .version package.json)" | |
| working-directory: packages/gitlab-mcp | |
| # Push ONLY on real drift. .semantic-release-version is release-time-only | |
| # (gitignored) and never staged. | |
| - name: Commit and push on drift | |
| working-directory: packages/gitlab-mcp | |
| run: | | |
| git config user.name "structured-world-releaser[bot]" | |
| git config user.email "structured-world-releaser[bot]@users.noreply.github.com" | |
| git add README.md server.json package.json | |
| if git diff --cached --quiet; then | |
| echo "No metadata drift; nothing to push." | |
| exit 0 | |
| fi | |
| git commit -m "chore: sync generated metadata [skip ci]" | |
| git push origin HEAD:main | |
| # Legacy required status check - reports quality-checks result for branch protection | |
| # Added to support migration from old "Lint, Test & Build" ruleset requirement | |
| expected-status: | |
| name: Lint, Test & Build | |
| runs-on: ubuntu-latest | |
| needs: [quality-checks] | |
| if: always() | |
| steps: | |
| - name: Report status | |
| run: | | |
| if [ "${{ needs.quality-checks.result }}" = "success" ]; then | |
| echo "Lint/Test/Build passed." | |
| exit 0 | |
| fi | |
| echo "Lint/Test/Build failed." | |
| exit 1 | |
| # Job 2: Build and test Docker image | |
| docker-build: | |
| name: Build Docker Image | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| needs: [quality-checks] | |
| permissions: | |
| contents: read | |
| pull-requests: read # For review thread check on PRs | |
| security-events: read # For checking CodeQL alert states | |
| # Local registry so the db image can be built `FROM` the just-built | |
| # core image. The buildx `docker-container` builder is isolated from | |
| # the host docker daemon, so a `--load`ed core image is NOT visible to | |
| # the db build (it would try to pull `structured-world/gitlab-mcp:test` | |
| # from docker.io and fail). Pushing core to this in-job registry and | |
| # referencing it via `localhost:5000` is the canonical chained-image | |
| # pattern; the builder reaches it via `driver-opts: network=host`. | |
| services: | |
| registry: | |
| image: registry:2 | |
| ports: | |
| - 5000:5000 | |
| steps: | |
| - name: Check for unresolved review threads | |
| if: github.event_name == 'pull_request' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| if ! command -v gh >/dev/null 2>&1; then | |
| echo "gh CLI not available; skipping thread check (fail-open)." >> "$GITHUB_STEP_SUMMARY" | |
| exit 0 | |
| fi | |
| if [ -z "$PR_NUMBER" ]; then | |
| echo "PR_NUMBER is empty; skipping thread check (fail-open)." >> "$GITHUB_STEP_SUMMARY" | |
| exit 0 | |
| fi | |
| OWNER="${REPO%/*}" | |
| NAME="${REPO#*/}" | |
| # Collect all unresolved, non-outdated threads with author info | |
| ALL_THREADS="[]" | |
| AFTER="" | |
| MAX_PAGES=100 | |
| PAGE_COUNT=0 | |
| while [ "$PAGE_COUNT" -lt "$MAX_PAGES" ]; do | |
| PAGE_COUNT=$((PAGE_COUNT + 1)) | |
| if [ -z "$AFTER" ]; then | |
| # First comment identifies thread origin (CodeQL, Copilot, human) for categorization | |
| QUERY='query($owner:String!, $name:String!, $number:Int!) { | |
| repository(owner:$owner, name:$name) { | |
| pullRequest(number:$number) { | |
| reviewThreads(first:100) { | |
| nodes { | |
| isResolved | |
| isOutdated | |
| comments(first:1) { | |
| nodes { | |
| author { login } | |
| body | |
| } | |
| } | |
| } | |
| pageInfo { hasNextPage, endCursor } | |
| } | |
| } | |
| } | |
| }' | |
| RESP=$(gh api graphql \ | |
| -f query="$QUERY" \ | |
| -f owner="$OWNER" \ | |
| -f name="$NAME" \ | |
| -F number="$PR_NUMBER") || { | |
| echo "GraphQL query failed; skipping thread check (fail-open)." >> "$GITHUB_STEP_SUMMARY" | |
| exit 0 | |
| } | |
| else | |
| # First comment identifies thread origin (CodeQL, Copilot, human) for categorization | |
| QUERY_AFTER='query($owner:String!, $name:String!, $number:Int!, $after:String) { | |
| repository(owner:$owner, name:$name) { | |
| pullRequest(number:$number) { | |
| reviewThreads(first:100, after:$after) { | |
| nodes { | |
| isResolved | |
| isOutdated | |
| comments(first:1) { | |
| nodes { | |
| author { login } | |
| body | |
| } | |
| } | |
| } | |
| pageInfo { hasNextPage, endCursor } | |
| } | |
| } | |
| } | |
| }' | |
| RESP=$(gh api graphql \ | |
| -f query="$QUERY_AFTER" \ | |
| -f owner="$OWNER" \ | |
| -f name="$NAME" \ | |
| -F number="$PR_NUMBER" \ | |
| -f after="$AFTER") || { | |
| echo "GraphQL query failed; skipping thread check (fail-open)." >> "$GITHUB_STEP_SUMMARY" | |
| exit 0 | |
| } | |
| fi | |
| # Extract unresolved, non-outdated threads from this page | |
| PAGE_THREADS=$(echo "$RESP" | jq '[.data.repository.pullRequest.reviewThreads.nodes // [] | .[] | select(.isResolved == false and .isOutdated == false)]') | |
| PAGE_NODE_COUNT=$(echo "$RESP" | jq '(.data.repository.pullRequest.reviewThreads.nodes // []) | length') | |
| if ! [[ "$PAGE_NODE_COUNT" =~ ^[0-9]+$ ]]; then PAGE_NODE_COUNT=0; fi | |
| # Merge into ALL_THREADS | |
| ALL_THREADS=$(echo "$ALL_THREADS" "$PAGE_THREADS" | jq -s '.[0] + .[1]') | |
| HAS_NEXT=$(echo "$RESP" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage') | |
| if [ "$HAS_NEXT" != "true" ]; then break; fi | |
| AFTER=$(echo "$RESP" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.endCursor') | |
| if [ "$AFTER" = "null" ]; then AFTER=""; fi | |
| if [ "$PAGE_NODE_COUNT" -eq 0 ] && [ "$HAS_NEXT" = "true" ]; then | |
| echo "::error::Thread scan incomplete: empty page with hasNextPage=true. Blocking build for safety." | |
| exit 1 | |
| fi | |
| if [ "$HAS_NEXT" = "true" ] && [ -z "$AFTER" ]; then | |
| echo "::error::Thread scan incomplete: missing endCursor with hasNextPage=true. Blocking build for safety." | |
| exit 1 | |
| fi | |
| done | |
| if [ "$PAGE_COUNT" -ge "$MAX_PAGES" ] && [ "$HAS_NEXT" = "true" ]; then | |
| echo "::error::Thread scan incomplete (exceeded $MAX_PAGES pages). Blocking build for safety." | |
| exit 1 | |
| fi | |
| # Categorize threads and check CodeQL alert states | |
| COPILOT_COUNT=0 | |
| CODEQL_ACTIVE=0 | |
| CODEQL_DISMISSED=0 | |
| OTHER_COUNT=0 | |
| THREAD_COUNT=$(echo "$ALL_THREADS" | jq 'length') | |
| # Validate THREAD_COUNT is numeric (jq always returns number for length, but be defensive) | |
| if ! [[ "$THREAD_COUNT" =~ ^[0-9]+$ ]]; then THREAD_COUNT=0; fi | |
| # Skip loop if no threads (seq behavior varies: GNU outputs nothing, BSD may output values) | |
| if [ "$THREAD_COUNT" -gt 0 ]; then | |
| for i in $(seq 0 $((THREAD_COUNT - 1))); do | |
| THREAD=$(echo "$ALL_THREADS" | jq ".[$i]") | |
| # Review threads always have at least one comment by definition; fallback handles edge cases | |
| AUTHOR=$(echo "$THREAD" | jq -r '.comments.nodes[0].author.login // ""') | |
| BODY=$(echo "$THREAD" | jq -r '.comments.nodes[0].body // ""') | |
| if [ "$AUTHOR" = "github-advanced-security" ]; then | |
| # CodeQL thread - extract alert number and check state | |
| # Note: Each CodeQL thread body contains exactly one alert URL, so head -1 is correct. | |
| # The URL format is /security/code-scanning/NNN where NNN is the alert number. | |
| ALERT_NUM=$(echo "$BODY" | grep -oE '/security/code-scanning/[0-9]+' | grep -oE '[0-9]+$' | head -1) | |
| if [ -n "$ALERT_NUM" ]; then | |
| # API calls are made sequentially per thread. Typical PRs have 0-5 CodeQL threads, | |
| # so batching/parallelization is unnecessary. Rate limits are 5000 req/hour for GitHub Actions. | |
| # If API fails, "unknown" state is treated as active (fail-safe: block if state is uncertain). | |
| ALERT_STATE=$(gh api "repos/$REPO/code-scanning/alerts/$ALERT_NUM" --jq '.state' 2>/dev/null || echo "unknown") | |
| if [ "$ALERT_STATE" = "dismissed" ] || [ "$ALERT_STATE" = "fixed" ]; then | |
| CODEQL_DISMISSED=$((CODEQL_DISMISSED + 1)) | |
| else | |
| # Includes "open", "unknown" (API failure), and any unexpected values | |
| CODEQL_ACTIVE=$((CODEQL_ACTIVE + 1)) | |
| fi | |
| else | |
| # No alert number found in body, count as active (fail-safe) | |
| CODEQL_ACTIVE=$((CODEQL_ACTIVE + 1)) | |
| fi | |
| elif [ "$AUTHOR" = "copilot-pull-request-reviewer" ]; then | |
| COPILOT_COUNT=$((COPILOT_COUNT + 1)) | |
| else | |
| OTHER_COUNT=$((OTHER_COUNT + 1)) | |
| fi | |
| done | |
| fi | |
| # Calculate effective blocking threads (CodeQL dismissed don't block) | |
| BLOCKING=$((COPILOT_COUNT + CODEQL_ACTIVE + OTHER_COUNT)) | |
| # Write summary | |
| echo "## Review Thread Check" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| if [ "$THREAD_COUNT" -eq 0 ]; then | |
| echo "No unresolved threads found." >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "| Category | Count |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "|----------|-------|" >> "$GITHUB_STEP_SUMMARY" | |
| [ "$COPILOT_COUNT" -gt 0 ] && echo "| Copilot | $COPILOT_COUNT |" >> "$GITHUB_STEP_SUMMARY" | |
| [ "$CODEQL_ACTIVE" -gt 0 ] && echo "| CodeQL (active) | $CODEQL_ACTIVE |" >> "$GITHUB_STEP_SUMMARY" | |
| [ "$CODEQL_DISMISSED" -gt 0 ] && echo "| CodeQL (dismissed) | $CODEQL_DISMISSED |" >> "$GITHUB_STEP_SUMMARY" | |
| [ "$OTHER_COUNT" -gt 0 ] && echo "| Other | $OTHER_COUNT |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| **Blocking** | **$BLOCKING** |" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| # Intentionally fail (not skip) — PRs with blocking threads should not show green CI | |
| if [ "$BLOCKING" -gt 0 ]; then | |
| echo "::error::$BLOCKING unresolved review thread(s). Resolve them before building. (CodeQL dismissed: $CODEQL_DISMISSED, Copilot: $COPILOT_COUNT, CodeQL active: $CODEQL_ACTIVE, Other: $OTHER_COUNT)" | |
| exit 1 | |
| fi | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "✅ No blocking review threads." >> "$GITHUB_STEP_SUMMARY" | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| with: | |
| # Host network so the builder can reach the in-job registry at | |
| # localhost:5000 (to push core and pull it for the db FROM). | |
| driver-opts: network=host | |
| # PR test build (single-platform, no docker.io push). Core is pushed | |
| # to the in-job registry so the db image can resolve it via FROM; the | |
| # release workflow does the multi-platform docker.io push. | |
| - name: Build core Docker image (test only) | |
| id: build | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ./packages/gitlab-mcp/Dockerfile | |
| platforms: linux/amd64 | |
| push: true | |
| tags: localhost:5000/${{ env.IMAGE_NAME }}:test | |
| cache-from: type=gha,scope=core | |
| cache-to: type=gha,mode=max,scope=core | |
| - name: Build db Docker image layered on core (test only) | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ./packages/gitlab-mcp-db/Dockerfile | |
| build-args: | | |
| CORE_IMAGE=localhost:5000/${{ env.IMAGE_NAME }}:test | |
| platforms: linux/amd64 | |
| # Load the layered image into the host daemon for the run-test below. | |
| load: true | |
| tags: ${{ env.IMAGE_NAME }}-db:test | |
| cache-from: type=gha,scope=db | |
| cache-to: type=gha,mode=max,scope=db | |
| - name: Test Docker images | |
| run: | | |
| # Use --entrypoint node + a self-exiting script: the default entrypoint | |
| # starts a long-lived server (it ignores --version and never exits, so | |
| # `docker run ... --version` would hang). Core lives in the in-job | |
| # registry (pulled on run); the db image was loaded to the host daemon. | |
| docker run --rm --entrypoint node localhost:5000/${{ env.IMAGE_NAME }}:test \ | |
| -e "const fs=require('fs'); if (!fs.existsSync('/app/dist/src/main.js')) process.exit(1); console.log('core image ok')" | |
| # The layered image must resolve the optional db backend at runtime. | |
| docker run --rm --entrypoint node ${{ env.IMAGE_NAME }}-db:test \ | |
| -e "if (typeof require('@structured-world/gitlab-mcp-db').PostgreSQLStorageBackend !== 'function') process.exit(1); console.log('db backend resolves')" | |
| # Job 3: Summary | |
| summary: | |
| name: Pipeline Summary | |
| runs-on: ubuntu-latest | |
| permissions: {} | |
| needs: [quality-checks, docker-build] | |
| if: always() | |
| steps: | |
| - name: Summary | |
| run: | | |
| echo "## Pipeline Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [[ "${{ needs.quality-checks.result }}" == "success" ]]; then | |
| echo "✅ Quality checks: Passed" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "❌ Quality checks: Failed" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [[ "${{ needs.docker-build.result }}" == "success" ]]; then | |
| echo "✅ Docker build: Passed" >> $GITHUB_STEP_SUMMARY | |
| elif [[ "${{ needs.docker-build.result }}" == "skipped" ]]; then | |
| echo "⏭️ Docker build: Skipped" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "❌ Docker build: Failed" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "ℹ️ Release and publishing handled by [Release workflow](${{ github.server_url }}/${{ github.repository }}/actions/workflows/release-please.yml)" >> $GITHUB_STEP_SUMMARY |