Skip to content

chore(deps): bump graphql from 16.14.2 to 17.0.0 #1786

chore(deps): bump graphql from 16.14.2 to 17.0.0

chore(deps): bump graphql from 16.14.2 to 17.0.0 #1786

Workflow file for this run

# 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